diff --git a/bin/gstack-codex-probe b/bin/gstack-codex-probe index f52f10952..c3f12c4e6 100755 --- a/bin/gstack-codex-probe +++ b/bin/gstack-codex-probe @@ -556,20 +556,19 @@ _gstack_codex_log_hang() { # --- Executed subcommands ---------------------------------------------------- _gstack_codex_usage() { - cat <<'EOF' -Usage: gstack-codex-probe [args] - select-model [--model ] 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 [--model ] 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 run cmd under a deadline; its exit code, 124 on timeout - log-event [detail] record a telemetry event when telemetry is on; exit 0 - log-hang record a timeout learning; exit 0 - help this text -EOF + printf '%s\n' \ + 'Usage: gstack-codex-probe [args]' \ + ' select-model [--model ] 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 [--model ] 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 run cmd under a deadline; its exit code, 124 on timeout' \ + ' log-event [detail] record a telemetry event when telemetry is on; exit 0' \ + ' log-hang record a timeout learning; exit 0' \ + ' help this text' } # Parses " [--model ]" into _GSTACK_CODEX_ARG_KIND and diff --git a/lib/greptile-early.ts b/lib/greptile-early.ts index 95387a09d..f62b64043 100644 --- a/lib/greptile-early.ts +++ b/lib/greptile-early.ts @@ -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, string> = { "config-folder": "a .greptile/ config folder", "greptile.json": "greptile.json", diff --git a/ship/sections/greptile-early.md b/ship/sections/greptile-early.md index a10bf2c09..3e37731cc 100644 --- a/ship/sections/greptile-early.md +++ b/ship/sections/greptile-early.md @@ -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 `. If it fails, record - "Greptile early review: not run (push failed)" and continue to Step 7; Step 17 pushes later. - 2. Write a provisional `: ` 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 `. If it fails, record +"Greptile early review: not run (push failed)" and continue to Step 7; Step 17 pushes later. + +**2.** Write a provisional `: ` 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/" - ~/.claude/skills/gstack/bin/gstack-greptile-early open --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/" +~/.claude/skills/gstack/bin/gstack-greptile-early open --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 diff --git a/ship/sections/greptile-early.md.tmpl b/ship/sections/greptile-early.md.tmpl index 1a0c2d8e3..a4c13467d 100644 --- a/ship/sections/greptile-early.md.tmpl +++ b/ship/sections/greptile-early.md.tmpl @@ -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 `. If it fails, record - "Greptile early review: not run (push failed)" and continue to Step 7; Step 17 pushes later. - 2. Write a provisional `: ` 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/" - ~/.claude/skills/gstack/bin/gstack-greptile-early open --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 `. If it fails, record +"Greptile early review: not run (push failed)" and continue to Step 7; Step 17 pushes later. + +**2.** Write a provisional `: ` 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/" +~/.claude/skills/gstack/bin/gstack-greptile-early open --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 diff --git a/test/autoplan-publication-guard.test.ts b/test/autoplan-publication-guard.test.ts index 7cea969b4..c174b8c60 100644 --- a/test/autoplan-publication-guard.test.ts +++ b/test/autoplan-publication-guard.test.ts @@ -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 }; diff --git a/test/autoplan-publication-hook.test.ts b/test/autoplan-publication-hook.test.ts index 3c46d7149..d33f2b671 100644 --- a/test/autoplan-publication-hook.test.ts +++ b/test/autoplan-publication-hook.test.ts @@ -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 }); }); diff --git a/test/autoplan-publication-payload.test.ts b/test/autoplan-publication-payload.test.ts index f29993480..01fa26172 100644 --- a/test/autoplan-publication-payload.test.ts +++ b/test/autoplan-publication-payload.test.ts @@ -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 () => { diff --git a/test/autoplan-schema-canary-compare.test.ts b/test/autoplan-schema-canary-compare.test.ts index 7180b9df0..d07e5b913 100644 --- a/test/autoplan-schema-canary-compare.test.ts +++ b/test/autoplan-schema-canary-compare.test.ts @@ -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) => new Map>([['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([]); diff --git a/test/fixtures/workflow-required-steps.json b/test/fixtures/workflow-required-steps.json index 7dfaf8cb7..32e4967ce 100644 --- a/test/fixtures/workflow-required-steps.json +++ b/test/fixtures/workflow-required-steps.json @@ -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)" ], diff --git a/test/helpers/carve-guards.ts b/test/helpers/carve-guards.ts index a06c1c399..5a9b86f84 100644 --- a/test/helpers/carve-guards.ts +++ b/test/helpers/carve-guards.ts @@ -131,21 +131,23 @@ export const CARVE_GUARDS: Record = { // 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 --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: [ '<<>>', 'SEMANTIC_REVIEW: clean', - 'gh issue create --title', + 'gstack-post issue-create', 'PIN_SHA=$(git rev-parse HEAD)', ], gateAfterStop: undefined, diff --git a/test/helpers/manual-judge-review-fixture.ts b/test/helpers/manual-judge-review-fixture.ts index 7ad0f9732..ff4d0bd4d 100644 --- a/test/helpers/manual-judge-review-fixture.ts +++ b/test/helpers/manual-judge-review-fixture.ts @@ -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 { diff --git a/test/qa-supervision-selection.test.ts b/test/qa-supervision-selection.test.ts index 663d684c7..850b01e43 100644 --- a/test/qa-supervision-selection.test.ts +++ b/test/qa-supervision-selection.test.ts @@ -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', () => { diff --git a/test/redact-prepush-emails.test.ts b/test/redact-prepush-emails.test.ts index cbb6c9b39..8f7a90ad6 100644 --- a/test/redact-prepush-emails.test.ts +++ b/test/redact-prepush-emails.test.ts @@ -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); diff --git a/test/ship-document-release-dispatch.test.ts b/test/ship-document-release-dispatch.test.ts index 6073a1171..736709da2 100644 --- a/test/ship-document-release-dispatch.test.ts +++ b/test/ship-document-release-dispatch.test.ts @@ -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 --title "$NEW_TITLE"'); - expect(body).toContain('gh pr edit --title "$NEW_TITLE"'); - expect(body).toContain('glab mr create -b -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( |$)\' "$TITLE_FILE"` and never publish an unprefixed title'); + expect(body).toContain('gstack-pr-title-rewrite.sh --stdin > "$TITLE_FILE"'); + expect(body).toContain('gstack-post pr-create --base --title-file "$TITLE_FILE"'); + expect(body).toContain('gstack-post pr-title --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 --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'); diff --git a/test/ship-publication-gates.test.ts b/test/ship-publication-gates.test.ts index ec6607c21..f5ffd111f 100644 --- a/test/ship-publication-gates.test.ts +++ b/test/ship-publication-gates.test.ts @@ -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' }); diff --git a/test/workflow-excerpt.test.ts b/test/workflow-excerpt.test.ts index e0cbe0672..c53726a47 100644 --- a/test/workflow-excerpt.test.ts +++ b/test/workflow-excerpt.test.ts @@ -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 --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');