mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 00:03:15 +01:00
fix: integration repairs across the wave (Step 6.5 fence, printed helper paths on Windows, probe usage without a heredoc) and tests pinned to the moved title and gstack-post contracts
This commit is contained in:
1 parent
068a3099ba
commit
a740353105
16 files changed
+135
-75
No files matched your search
+13
-14
@@ -556,20 +556,19 @@ _gstack_codex_log_hang() {
|
||||
# --- Executed subcommands ----------------------------------------------------
|
||||
|
||||
_gstack_codex_usage() {
|
||||
cat <<'EOF'
|
||||
Usage: gstack-codex-probe <subcommand> [args]
|
||||
select-model <exec|review> [--model <id>] print CODEX_SEL:, CODEX_SEL_KIND: and CODEX_SANDBOX:; exit 0 or 1
|
||||
check-auth exit 0 when Codex credentials exist, 1 otherwise
|
||||
show-sandbox print CODEX_SANDBOX: read-only (or danger-full-access)
|
||||
check-sandbox free sandbox check; exit 0, or 3 with the reason on stderr
|
||||
probe-model <exec|review> [--model <id>] paid round trip; print CODEX_PROBE_STATE:; exit 0, 1, 2 or 4
|
||||
check-version warn on stderr about known-bad Codex CLI versions; exit 0
|
||||
show-first-use-notice once per machine, say where reviews go; exit 0
|
||||
run-with-timeout <secs> <cmd...> run cmd under a deadline; its exit code, 124 on timeout
|
||||
log-event <event> [detail] record a telemetry event when telemetry is on; exit 0
|
||||
log-hang <skill> <prompt-size> record a timeout learning; exit 0
|
||||
help this text
|
||||
EOF
|
||||
printf '%s\n' \
|
||||
'Usage: gstack-codex-probe <subcommand> [args]' \
|
||||
' select-model <exec|review> [--model <id>] print CODEX_SEL:, CODEX_SEL_KIND: and CODEX_SANDBOX:; exit 0 or 1' \
|
||||
' check-auth exit 0 when Codex credentials exist, 1 otherwise' \
|
||||
' show-sandbox print CODEX_SANDBOX: read-only (or danger-full-access)' \
|
||||
' check-sandbox free sandbox check; exit 0, or 3 with the reason on stderr' \
|
||||
' probe-model <exec|review> [--model <id>] paid round trip; print CODEX_PROBE_STATE:; exit 0, 1, 2 or 4' \
|
||||
' check-version warn on stderr about known-bad Codex CLI versions; exit 0' \
|
||||
' show-first-use-notice once per machine, say where reviews go; exit 0' \
|
||||
' run-with-timeout <secs> <cmd...> run cmd under a deadline; its exit code, 124 on timeout' \
|
||||
' log-event <event> [detail] record a telemetry event when telemetry is on; exit 0' \
|
||||
' log-hang <skill> <prompt-size> record a timeout learning; exit 0' \
|
||||
' help this text'
|
||||
}
|
||||
|
||||
# Parses "<exec|review> [--model <id>]" into _GSTACK_CODEX_ARG_KIND and
|
||||
|
||||
@@ -112,7 +112,9 @@ function storedConsent(env: EarlyEnv): "yes" | "no" | undefined {
|
||||
return v === "yes" || v === "no" ? v : undefined;
|
||||
}
|
||||
|
||||
const config = (env: EarlyEnv) => path.join(env.binDir, "gstack-config");
|
||||
/** A gstack helper path as printed for the user's shell: forward slashes on every platform (Git Bash on Windows). */
|
||||
const helper = (env: EarlyEnv, name: string) => `${env.binDir.replace(/\\/g, "/").replace(/\/+$/, "")}/${name}`;
|
||||
const config = (env: EarlyEnv) => helper(env, "gstack-config");
|
||||
const SIGNAL_TEXT: Record<Exclude<Signal, "none">, string> = {
|
||||
"config-folder": "a .greptile/ config folder",
|
||||
"greptile.json": "greptile.json",
|
||||
|
||||
@@ -18,12 +18,14 @@ Steps 7–9 run; Step 10 collects its comments. Without Greptile nothing changes
|
||||
A) Push early for this repo (remembered) · B) Keep today's order for this repo (remembered).
|
||||
Run `~/.claude/skills/gstack/bin/gstack-greptile-early consent yes` (A) or `consent no` (B).
|
||||
B continues to Step 7.
|
||||
- **`GREPTILE_EARLY: on`**, or after A — show the user the printed `Greptile: found …` line, then:
|
||||
1. Push: `git push -u origin <branch-name>`. If it fails, record
|
||||
"Greptile early review: not run (push failed)" and continue to Step 7; Step 17 pushes later.
|
||||
2. Write a provisional `<type>: <summary>` title (Step 18 replaces it) into a private file:
|
||||
- **`GREPTILE_EARLY: on`**, or after A — show the user the printed `Greptile: found …` line, then follow 1-3 below.
|
||||
|
||||
```bash
|
||||
**1.** Push: `git push -u origin <branch-name>`. If it fails, record
|
||||
"Greptile early review: not run (push failed)" and continue to Step 7; Step 17 pushes later.
|
||||
|
||||
**2.** Write a provisional `<type>: <summary>` title (Step 18 replaces it) into a private file:
|
||||
|
||||
```bash
|
||||
_GT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp"
|
||||
mkdir -p "$_GT" && chmod 700 "$_GT" || { echo "Not sent: cannot create $_GT for the text file." >&2; exit 1; }
|
||||
_EX=$(git rev-parse --git-path info/exclude 2>/dev/null) && mkdir -p "$(dirname "$_EX")" && { grep -qxF '/.gstack/tmp/' "$_EX" 2>/dev/null || echo '/.gstack/tmp/' >> "$_EX"; }
|
||||
@@ -31,18 +33,21 @@ TITLE_FILE=$(mktemp "${_GT:?}/early-title.XXXXXX") || { echo "Not sent: mktemp f
|
||||
```
|
||||
|
||||
Write the text into each printed file with your file-write tool (Claude Code's Write tool needs a Read of the empty file first), exactly as it should appear. The text never goes into a shell command, heredoc or quoted argument. If a write fails or is refused, do not send: print the cause, the file path and the command below for sending by hand.
|
||||
3. Open the draft, substituting the printed name. Add `--ready` only when the user asked
|
||||
for a non-draft PR:
|
||||
```bash
|
||||
TITLE_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/<title-file-name>"
|
||||
~/.claude/skills/gstack/bin/gstack-greptile-early open --base <base> --title-file "$TITLE_FILE"
|
||||
```
|
||||
It opens the PR through `gstack-post` with a body saying /ship opened it early, and
|
||||
posts one `@greptileai` comment when Greptile skips drafts. Save the printed
|
||||
`EARLY_PR` and `EARLY_PR_OPENED_AT` in the invocation record. `EARLY_PR: none` means a
|
||||
PR was already open: Step 10 triages it as before. Exit 1 or 2 means `gstack-post`
|
||||
flagged the provisional title: write a plainer one and rerun. Exit 3: record the error
|
||||
and continue without early review.
|
||||
|
||||
**3.** Open the draft, substituting the printed name. Add `--ready` only when the user asked
|
||||
for a non-draft PR:
|
||||
|
||||
```bash
|
||||
TITLE_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/<title-file-name>"
|
||||
~/.claude/skills/gstack/bin/gstack-greptile-early open --base <base> --title-file "$TITLE_FILE"
|
||||
```
|
||||
|
||||
It opens the PR through `gstack-post` with a body saying /ship opened it early, and
|
||||
posts one `@greptileai` comment when Greptile skips drafts. Save the printed
|
||||
`EARLY_PR` and `EARLY_PR_OPENED_AT` in the invocation record. `EARLY_PR: none` means a
|
||||
PR was already open: Step 10 triages it as before. Exit 1 or 2 means `gstack-post`
|
||||
flagged the provisional title: write a plainer one and rerun. Exit 3: record the error
|
||||
and continue without early review.
|
||||
|
||||
**If /ship stops after the early PR opened and before Step 19 publishes**, leave the
|
||||
draft open (never close or force-push it) and post one comment saying /ship stopped
|
||||
|
||||
@@ -16,24 +16,29 @@ Steps 7–9 run; Step 10 collects its comments. Without Greptile nothing changes
|
||||
A) Push early for this repo (remembered) · B) Keep today's order for this repo (remembered).
|
||||
Run `~/.claude/skills/gstack/bin/gstack-greptile-early consent yes` (A) or `consent no` (B).
|
||||
B continues to Step 7.
|
||||
- **`GREPTILE_EARLY: on`**, or after A — show the user the printed `Greptile: found …` line, then:
|
||||
1. Push: `git push -u origin <branch-name>`. If it fails, record
|
||||
"Greptile early review: not run (push failed)" and continue to Step 7; Step 17 pushes later.
|
||||
2. Write a provisional `<type>: <summary>` title (Step 18 replaces it) into a private file:
|
||||
- **`GREPTILE_EARLY: on`**, or after A — show the user the printed `Greptile: found …` line, then follow 1-3 below.
|
||||
|
||||
{{FREE_TEXT_FILE:TITLE_FILE=early-title}}
|
||||
3. Open the draft, substituting the printed name. Add `--ready` only when the user asked
|
||||
for a non-draft PR:
|
||||
```bash
|
||||
TITLE_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/<title-file-name>"
|
||||
~/.claude/skills/gstack/bin/gstack-greptile-early open --base <base> --title-file "$TITLE_FILE"
|
||||
```
|
||||
It opens the PR through `gstack-post` with a body saying /ship opened it early, and
|
||||
posts one `@greptileai` comment when Greptile skips drafts. Save the printed
|
||||
`EARLY_PR` and `EARLY_PR_OPENED_AT` in the invocation record. `EARLY_PR: none` means a
|
||||
PR was already open: Step 10 triages it as before. Exit 1 or 2 means `gstack-post`
|
||||
flagged the provisional title: write a plainer one and rerun. Exit 3: record the error
|
||||
and continue without early review.
|
||||
**1.** Push: `git push -u origin <branch-name>`. If it fails, record
|
||||
"Greptile early review: not run (push failed)" and continue to Step 7; Step 17 pushes later.
|
||||
|
||||
**2.** Write a provisional `<type>: <summary>` title (Step 18 replaces it) into a private file:
|
||||
|
||||
{{FREE_TEXT_FILE:TITLE_FILE=early-title}}
|
||||
|
||||
**3.** Open the draft, substituting the printed name. Add `--ready` only when the user asked
|
||||
for a non-draft PR:
|
||||
|
||||
```bash
|
||||
TITLE_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/<title-file-name>"
|
||||
~/.claude/skills/gstack/bin/gstack-greptile-early open --base <base> --title-file "$TITLE_FILE"
|
||||
```
|
||||
|
||||
It opens the PR through `gstack-post` with a body saying /ship opened it early, and
|
||||
posts one `@greptileai` comment when Greptile skips drafts. Save the printed
|
||||
`EARLY_PR` and `EARLY_PR_OPENED_AT` in the invocation record. `EARLY_PR: none` means a
|
||||
PR was already open: Step 10 triages it as before. Exit 1 or 2 means `gstack-post`
|
||||
flagged the provisional title: write a plainer one and rerun. Exit 3: record the error
|
||||
and continue without early review.
|
||||
|
||||
**If /ship stops after the early PR opened and before Step 19 publishes**, leave the
|
||||
draft open (never close or force-push it) and post one comment saying /ship stopped
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, describe, expect, jest, test } from 'bun:test';
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, jest, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
@@ -12,7 +12,18 @@ import consumption from './fixtures/autoplan-phase-consumption-491.json';
|
||||
|
||||
const ROOT = fs.realpathSync(path.join(import.meta.dir, '..'));
|
||||
// Every guarded decision appends to the guard log; keep it out of the real state root.
|
||||
process.env.GSTACK_HOME = fs.mkdtempSync(path.join(tmpdir(), 'autoplan-guard-log-'));
|
||||
let previousGstackHome: string | undefined;
|
||||
let guardLogHome = '';
|
||||
beforeAll(() => {
|
||||
previousGstackHome = process.env.GSTACK_HOME;
|
||||
guardLogHome = fs.mkdtempSync(path.join(tmpdir(), 'autoplan-guard-log-'));
|
||||
process.env.GSTACK_HOME = guardLogHome;
|
||||
});
|
||||
afterAll(() => {
|
||||
if (previousGstackHome === undefined) delete process.env.GSTACK_HOME;
|
||||
else process.env.GSTACK_HOME = previousGstackHome;
|
||||
fs.rmSync(guardLogHome, { recursive: true, force: true });
|
||||
});
|
||||
const dirs: string[] = [];
|
||||
afterEach(() => { for (const dir of dirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true }); });
|
||||
const phaseNumber = { ceo: 1, design: 2, dx: 2.5, eng: 3 };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
@@ -8,7 +8,18 @@ import { runPublicationHook } from '../autoplan/bin/phase-publication-hook.ts';
|
||||
|
||||
const ROOT = path.join(import.meta.dir, '..');
|
||||
// Every guarded decision appends to the guard log; keep it out of the real state root.
|
||||
process.env.GSTACK_HOME = fs.mkdtempSync(path.join(tmpdir(), 'autoplan-guard-log-'));
|
||||
let previousGstackHome: string | undefined;
|
||||
let guardLogHome = '';
|
||||
beforeAll(() => {
|
||||
previousGstackHome = process.env.GSTACK_HOME;
|
||||
guardLogHome = fs.mkdtempSync(path.join(tmpdir(), 'autoplan-guard-log-'));
|
||||
process.env.GSTACK_HOME = guardLogHome;
|
||||
});
|
||||
afterAll(() => {
|
||||
if (previousGstackHome === undefined) delete process.env.GSTACK_HOME;
|
||||
else process.env.GSTACK_HOME = previousGstackHome;
|
||||
fs.rmSync(guardLogHome, { recursive: true, force: true });
|
||||
});
|
||||
const SHIM = path.join(ROOT, 'autoplan/bin/phase-publication-hook');
|
||||
const dirs: string[] = [];
|
||||
afterEach(() => { for (const dir of dirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true }); });
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { guardFixture, section, denial, codeOf, type GuardFixture } from './helpers/autoplan-guard-fixture';
|
||||
import { AGENT_KEYS, CHECKED_CLAUDE_CODE, nativeToolInput, newerThanChecked } from '../autoplan/bin/phase-publication-hook.ts';
|
||||
import { nativePathSpelling } from '../lib/claude-public-transcript';
|
||||
import captured from './fixtures/claude-agent-payload-2.1.292.json';
|
||||
|
||||
const fixtures: GuardFixture[] = [];
|
||||
@@ -62,7 +63,8 @@ describe('Cause A: the Agent schema strips run_in_background', () => {
|
||||
for (const payload of captured.agentPayloads) for (const key of Object.keys(payload.tool_input)) expect(AGENT_KEYS).toContain(key);
|
||||
expect(CHECKED_CLAUDE_CODE).toBe(captured.claudeCodeVersion);
|
||||
expect(nativeToolInput({ prompt: 'p', run_in_background: 'false' }, '/', 'Agent')).toEqual({ prompt: 'p' });
|
||||
expect(nativeToolInput({ file_path: 'x', run_in_background: true }, '/', 'Read')).toEqual({ file_path: '/x', run_in_background: true });
|
||||
expect(nativeToolInput({ file_path: 'x', run_in_background: true }, '/', 'Read'))
|
||||
.toEqual({ file_path: nativePathSpelling(path.resolve('/', 'x')), run_in_background: true });
|
||||
});
|
||||
|
||||
test('UC1: a pure strip by a newer, unchecked Claude Code is unverified; the same strip on a checked version is denied', async () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** CEO-13: the canary's comparison passes the documented strip and fails an injected one (free). */
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { schemaDrift, type HookPayload } from './helpers/schema-canary';
|
||||
import { SCHEMA_STRIPPED, nativeToolInput } from '../autoplan/bin/phase-publication-hook.ts';
|
||||
import captured from './fixtures/claude-agent-payload-2.1.292.json';
|
||||
|
||||
const cwd = '/repo';
|
||||
@@ -10,6 +11,13 @@ const journal = (agentInput: Record<string, unknown>) =>
|
||||
new Map<string, Record<string, unknown>>([['toolu_read', { file_path: 'note.txt' }], ['toolu_agent', agentInput]]);
|
||||
|
||||
describe('autoplan-schema-canary comparison', () => {
|
||||
test('the guard documents exactly one strip, and its comparison drops only that key from the captured inputs', () => {
|
||||
expect(SCHEMA_STRIPPED).toEqual({ Agent: ['run_in_background'] });
|
||||
const { payloadInput, journalInput } = captured.forcedRunInBackground;
|
||||
expect(nativeToolInput(journalInput, cwd, 'Agent')).toEqual(nativeToolInput(payloadInput, cwd, 'Agent'));
|
||||
expect(nativeToolInput(journalInput, cwd, 'Read')).not.toEqual(nativeToolInput(payloadInput, cwd, 'Read'));
|
||||
});
|
||||
|
||||
test('2.1.292 as captured: run_in_background stripped from the payload is the documented strip', () => {
|
||||
const { payloadInput, journalInput } = captured.forcedRunInBackground;
|
||||
expect(schemaDrift([read, agent(payloadInput)], journal(journalInput))).toEqual([]);
|
||||
|
||||
+3
@@ -126,6 +126,9 @@
|
||||
"sections/changelog.md": [
|
||||
"## Step 13: CHANGELOG (auto-generate)"
|
||||
],
|
||||
"sections/greptile-early.md": [
|
||||
"## Step 6.5: Early Greptile PR (after the free tests pass)"
|
||||
],
|
||||
"sections/greptile.md": [
|
||||
"## Step 10: Address Greptile review comments (if PR exists)"
|
||||
],
|
||||
|
||||
@@ -131,21 +131,23 @@ export const CARVE_GUARDS: Record<string, CarveGuard> = {
|
||||
// contain "dispatch the" — so update anchors in lockstep with any
|
||||
// touchpoint rewording.
|
||||
mustStayInSkeleton: [
|
||||
'v$NEW_VERSION',
|
||||
'gstack-pr-title-rewrite',
|
||||
'The pr-body section prepares the title from that result',
|
||||
'## Step 14.5: Documentation audit (every ship)',
|
||||
'No documentation writer runs after push',
|
||||
'dispatches the /document-release subagent',
|
||||
],
|
||||
// ...while the full create/update procedure stays carved into pr-body.md
|
||||
// (out of the skeleton, present in the union). Asserts BOTH PR paths
|
||||
// survive: the create path and the idempotent update path. The Step 18
|
||||
// survive: the create path and the idempotent update path, both posted
|
||||
// through gstack-post, and the version-prefixed title preparation that
|
||||
// moved into the section with them (Oct 7 wave B4). The Step 18
|
||||
// dispatch imperative stays carved too — pasting that literal into the
|
||||
// skeleton (correctly) fails this guard; the skeleton speaks of "the
|
||||
// /document-release subagent", never the carved imperative.
|
||||
mustMoveToSection: [
|
||||
'gh pr create --base',
|
||||
'gh pr edit --title',
|
||||
'gstack-post pr-create --base',
|
||||
'gstack-post pr-title',
|
||||
'gstack-pr-title-rewrite.sh <new-version> --stdin',
|
||||
'Dispatch /document-release as a subagent',
|
||||
],
|
||||
// ship is operational (multi-STOP, not a plan review); no single post-STOP gate.
|
||||
@@ -596,7 +598,7 @@ do not launch the downstream skill or open a browser.`,
|
||||
mustMoveToSection: [
|
||||
'<<<USER_SPEC>>>',
|
||||
'SEMANTIC_REVIEW: clean',
|
||||
'gh issue create --title',
|
||||
'gstack-post issue-create',
|
||||
'PIN_SHA=$(git rev-parse HEAD)',
|
||||
],
|
||||
gateAfterStop: undefined,
|
||||
|
||||
@@ -30,6 +30,9 @@ const LATER_PREAMBLE_EDITS: Array<[string, string]> = [
|
||||
const LATER_WAVE_EDITS: Array<[string, string]> = [
|
||||
['tmpfile=$(mktemp "${TMPDIR:-/tmp}/bun-install.XXXXXX")', 'tmpfile=$(mktemp)'],
|
||||
['BUN_VERSION="1.4.2"', 'BUN_VERSION="1.3.10"'],
|
||||
// Oct 7 wave: the artifacts-sync preamble block surfaces attention lines (one line longer).
|
||||
["Skill-start already ran artifacts sync. GBrain hint text (if any) says\nwhen to prefer `gbrain` over Grep. `ARTIFACTS_SYNC:` reports sync health\n(`off`, `mode=... | queue=N`, `remote-mode`, or a `gstack-brain-restore`\nhint). On an `attention:` line, tell the user in one sentence what\nit says and the command it names, then continue.\n\nThe one-time privacy stop-gate arrives as a `GSTACK_INSTRUCTION` block\nfrom skill-start when consent is pending; fire it via AskUserQuestion\nexactly as instructed.",
|
||||
"The skill-start output above already ran artifacts sync. Act on its lines:\nGBrain hint text (if present) tells you when to prefer `gbrain` over Grep;\n`ARTIFACTS_SYNC:` reports sync health (`off`, `mode=... | queue=N`,\n`remote-mode`, or a restore hint naming `gstack-brain-restore`).\n\nThe one-time privacy stop-gate (artifacts-sync consent) arrives as a\n`GSTACK_INSTRUCTION` block from skill-start when consent is actually pending\n\u2014 fire it via AskUserQuestion exactly as the block instructs."],
|
||||
];
|
||||
|
||||
export function approvedCookieWorkflowSource(source: string): string {
|
||||
|
||||
@@ -11,6 +11,8 @@ const ptyIds = [
|
||||
'office-hours-section-loading', 'office-hours-design-draft', 'plan-ceo-section-loading', 'ship-section-loading',
|
||||
'plan-ceo-finding-floor', 'plan-design-finding-floor', 'plan-devex-finding-floor',
|
||||
'plan-eng-multi-finding-batching', 'plan-ceo-split-overflow', 'plan-eng-review-artifact', 'plan-eng-review-artifact-full',
|
||||
// The /autoplan guard's live cases drive the same PTY runner (Oct 7 wave).
|
||||
'autoplan-guard-pty', 'autoplan-long-session',
|
||||
].sort();
|
||||
|
||||
test('PTY supervision controls select every current runner consumer', () => {
|
||||
|
||||
@@ -286,8 +286,9 @@ describe("each input fails on its own (CEO-23, DX-9)", () => {
|
||||
expect(found[0]).toContain("CODEOWNERS:2");
|
||||
});
|
||||
|
||||
// stubGitLog puts a POSIX shell `git` wrapper first on PATH; Windows CreateProcess cannot exec it.
|
||||
for (const behavior of ["fail", "hang"] as const) {
|
||||
test(`a history read that ${behavior === "fail" ? "errors" : "times out"} still honors the pusher's own address and says suppression was limited`, () => {
|
||||
test.skipIf(process.platform === "win32")(`a history read that ${behavior === "fail" ? "errors" : "times out"} still honors the pusher's own address and says suppression was limited`, () => {
|
||||
const fx = fixture({ selfEmail: at("me") });
|
||||
commitFile(fx, fx.repo, "lib.txt", "code\n", at("colleague"));
|
||||
expect(push(fx, ["origin", "main"]).code).toBe(0);
|
||||
|
||||
@@ -123,16 +123,17 @@ describe('pre-publication documentation lifecycle', () => {
|
||||
|
||||
test('PR creation and reruns keep current and blocked audits visible', () => {
|
||||
const body = read('ship/sections/pr-body.md.tmpl');
|
||||
expect(body).toContain("Use Step 18's `NEW_TITLE`");
|
||||
expect(body).toContain('`NEW_TITLE` unchanged; its version prefix is already present');
|
||||
expect(body).toContain('printf \'%s\' "$NEW_TITLE" |');
|
||||
expect(body).toContain('gh pr create --base <base> --title "$NEW_TITLE"');
|
||||
expect(body).toContain('gh pr edit --title "$NEW_TITLE"');
|
||||
expect(body).toContain('glab mr create -b <base> -t "$NEW_TITLE"');
|
||||
// Oct 7 wave B4: the title travels in Step 18's agent-written file and both
|
||||
// PR paths publish through gstack-post, which scans the exact bytes it sends.
|
||||
expect(body).toContain('`grep -Eq \'^v<new-version>( |$)\' "$TITLE_FILE"` and never publish an unprefixed title');
|
||||
expect(body).toContain('gstack-pr-title-rewrite.sh <new-version> --stdin > "$TITLE_FILE"');
|
||||
expect(body).toContain('gstack-post pr-create --base <base> --title-file "$TITLE_FILE"');
|
||||
expect(body).toContain('gstack-post pr-title <pr-number> --title-file "$TITLE_FILE"');
|
||||
expect(body).toContain('`gstack-post` detects GitHub or GitLab from the remote');
|
||||
expect(body).not.toContain('Dispatch /document-release');
|
||||
expectMentions(body, [['never', 'section', 'another']], 'body');
|
||||
expect(body).toContain('gh pr edit --body-file "$PR_BODY_FILE"');
|
||||
expect(body).toContain('gstack-redact --from-file "$PR_BODY_FILE"');
|
||||
expect(body).toContain('gstack-post pr-body <pr-number> --body-file "${PR_BODY_FILE:?restore the composed body path}"');
|
||||
expect(body).toContain('gstack-redact --from-file "$PR_BODY_FILE" --auto-redact');
|
||||
expect(read('ship/SKILL.md.tmpl')).toContain('existing PRs and docs-only changes');
|
||||
expect(read('ship/sections/apple-release.md.tmpl')).toContain('read-only');
|
||||
expect(read('ship/sections/apple-release.md.tmpl')).toContain('ship/sections/documentation.md');
|
||||
|
||||
@@ -75,8 +75,12 @@ function composed(root: string, opts: { title?: string; top?: string } = {}) {
|
||||
return { ...repo, bodyFile, publish: (block: string) => repo.run(fill(block)) };
|
||||
}
|
||||
|
||||
// The posting cases drive a POSIX stub gh that gstack-post spawns; Windows CreateProcess cannot
|
||||
// exec its shebang. gstack-post's argument-array and token logic run in-process on Windows in
|
||||
// test/gstack-post.test.ts.
|
||||
const postsThroughStubGh = (failure: string) => process.platform === 'win32' && failure === 'none';
|
||||
for (const failure of ['none', 'body', 'title'] as const) {
|
||||
test(`publication posts only scanned bytes through gstack-post: ${failure}`, () => {
|
||||
test.skipIf(postsThroughStubGh(failure))(`publication posts only scanned bytes through gstack-post: ${failure}`, () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'ship-post-'));
|
||||
try {
|
||||
const secret = 'AKIA' + '1234567890ABCDEF';
|
||||
@@ -100,7 +104,7 @@ for (const failure of ['none', 'body', 'title'] as const) {
|
||||
});
|
||||
}
|
||||
|
||||
test('MEDIUM findings publish nothing until the block reruns with the printed token', () => {
|
||||
test.skipIf(process.platform === 'win32')('MEDIUM findings publish nothing until the block reruns with the printed token', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'ship-post-medium-'));
|
||||
try {
|
||||
const c = composed(root, { top: '## Summary\nThanks jane.roe@acme-corp.io\n\n## Documentation' });
|
||||
|
||||
@@ -107,11 +107,12 @@ describe('workflow judge excerpts', () => {
|
||||
const text = readWorkflowExcerpt('ship/SKILL.md', '# Ship:', '## Important Rules');
|
||||
const publish = text.slice(text.indexOf('## Step 19:'), text.indexOf('## Step 20:'));
|
||||
const compose = publish.indexOf('PR_BODY_FILE=$(mktemp');
|
||||
const scan = publish.indexOf('gstack-redact --from-file "$PR_BODY_FILE"');
|
||||
const edit = publish.indexOf('gh pr edit --body-file');
|
||||
// gstack-post owns the scan of the exact bytes it sends (Oct 7 wave B4).
|
||||
const scan = publish.indexOf('`gstack-post` scans the exact title');
|
||||
const edit = publish.indexOf('gstack-post pr-body <pr-number> --body-file');
|
||||
expect(compose).toBeGreaterThan(0);
|
||||
expect(scan).toBeGreaterThan(compose);
|
||||
expect(edit).toBeGreaterThan(scan);
|
||||
expect(scan).toBeGreaterThan(0);
|
||||
expect(edit).toBeGreaterThan(compose);
|
||||
expect(publish.indexOf('Print the existing URL')).toBeGreaterThan(edit);
|
||||
expect(text).not.toContain('Phase 8e.5');
|
||||
expectMentions(text, [['never', 'create', 'commit']], 'text');
|
||||
|
||||
Reference in new issue
Block a user