From 8c106d642b1b59d049404ee22f53dbc7252d4756 Mon Sep 17 00:00:00 2001 From: garrytan Date: Mon, 26 Oct 2026 14:49:05 +0000 Subject: [PATCH] fix: pre-landing review fixes (round 2) - offersDefer: the wait/hold-off-for-now paraphrase never counts inside a browser drive option, accepts an em dash without spaces and a curly apostrophe in let's; negative and positive controls added (adversarial review on #3472). - docsync-authority: the interface assertion expects the rendered `/remote.git` form so it holds on Windows; the captured Linux replay is POSIX-only like the late-result replay (outside voice P1). - touchfiles: register the two new docsync captures with the paid cases they replay (ship-docsync-store, ship-docsync-completion), which test/free-fixtures.test.ts requires (remote gate red at 470ac55). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- test/docsync-authority.test.ts | 14 ++++++++++++-- test/helpers/third-party-actions.ts | 6 ++++-- test/helpers/touchfiles-data.ts | 4 ++-- test/third-party-actions.test.ts | 6 ++++-- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/test/docsync-authority.test.ts b/test/docsync-authority.test.ts index 4422c49df..d8bc0c34f 100644 --- a/test/docsync-authority.test.ts +++ b/test/docsync-authority.test.ts @@ -42,17 +42,27 @@ test('docs write authority permits the authored doc and private JSON/Markdown ar // saved section and the compare target under {home}/state, gstack's own state directory // (GSTACK_HOME), which the interface never named. The authority is right to refuse the // state directory; the interface now names every refused root beside the permitted place. -test('the declared interface names every refused private-artifact root, and the captured state-dir run stays refused', () => { +test('the declared interface names every refused private-artifact root', () => { const fixture = fixtureDocs('updated'); try { const interfaceText = docsNativeInterface(fixture, [path.join(fixture.home, 'fixture-publish.ts')], false, true); - const refused = [fixture.skills.split(path.sep).join('/'), fixture.env.CLAUDE_CONFIG_DIR, fixture.env.GSTACK_HOME, path.join(fixture.home, 'remote.git')]; + // The interface renders the remote root as `${home}/remote.git` (the same literal form the agent reads), not path.join. + const refused = [fixture.skills.split(path.sep).join('/'), fixture.env.CLAUDE_CONFIG_DIR, fixture.env.GSTACK_HOME, `${fixture.home}/remote.git`]; const sentence = interfaceText.split(/(?<=\.)\s+/).find(part => part.startsWith('A private JSON/Markdown artifact')); expect(sentence).toBeDefined(); for (const root of refused) expect(sentence).toContain(`${root}`); expect(sentence).toContain(`directly under ${fixture.home} or in a new directory you create there`); expect(sentence).toContain('never inside the repository'); expect(fixture.env.GSTACK_HOME).toBe(path.join(fixture.home, 'state')); + } finally { fixture.clean(); } +}); + +// The captured transcript is a Linux run: its Bash commands carry POSIX paths, which the literal +// command grammar rejects once a Windows home is substituted, so the replay is POSIX-only like the +// late-result replay below. +test.skipIf(process.platform === 'win32')('the captured state-dir run stays refused, and passes once relocated under the home', () => { + const fixture = fixtureDocs('updated'); + try { const captured = JSON.parse(fs.readFileSync(path.join(import.meta.dir, 'fixtures/docsync-state-dir-artifacts-public.json'), 'utf8')) as { calls: SkillTestResult['toolCalls'] }; const fill = (text: string) => text.replaceAll('{home}', fixture.home); diff --git a/test/helpers/third-party-actions.ts b/test/helpers/third-party-actions.ts index 9b81079f9..85e3f2f93 100644 --- a/test/helpers/third-party-actions.ts +++ b/test/helpers/third-party-actions.ts @@ -50,11 +50,13 @@ export const offersDriveOption = (text: string): boolean => letteredOptions(text * The defer option: a lettered option that defers, postpones or sets the step aside for now. "Wait for now" and * "hold off for now" count only as the option's own opening or sentence (gate 40290033102: "D) Wait for now. I'll * mark this step as blocked on you."), never as a drive option's "I wait while you sign in": the phrase must open the - * option or a sentence and end it, so "I wait for now while you sign in" and "wait for now and do it yourself" do not count. + * option or a sentence and end it, and the option must not be a browser drive, so "I wait for now while you sign in", + * "wait for now and do it yourself" and "I drive it in Aside. I wait for now." do not count. */ export const offersDefer = (text: string): boolean => letteredOptions(text).some(option => /\b(?:defer\w*|postpone\w*|put (?:this|it)(?: step)? off|skip (?:this|it)(?: step)? for now|come back to (?:this|it)(?: step)? later|leave (?:this|it)(?: step)? for (?:now|later))\b/i.test(option) - || /(?:^|[.!?;:]\s+)(?:(?:we|I|let's) )?(?:wait|hold off)(?: on (?:this|it)(?: step)?)? for now\b(?=\s*(?:[.!?;:]|[-\u2013\u2014]\s|$))/i.test(option)); + || !/drive|browse|Aside/i.test(option) + && /(?:^|[.!?;:]\s+)(?:(?:we|I|let['\u2019]s) )?(?:wait|hold off)(?: on (?:this|it)(?: step)?)? for now\b(?=\s*(?:[.!?;:]|[-\u2013\u2014]|$))/i.test(option)); /** The manual option: "manual", or one sentence offering instructions or steps the user follows themselves. */ export function offersManualSteps(text: string): boolean { diff --git a/test/helpers/touchfiles-data.ts b/test/helpers/touchfiles-data.ts index 0f0376441..25a89900b 100644 --- a/test/helpers/touchfiles-data.ts +++ b/test/helpers/touchfiles-data.ts @@ -814,10 +814,10 @@ export const E2E_TOUCHFILES: Record = { 'ship-triage': ['bin/gstack-state-root.sh', 'lib/state-root.ts', 'ship/**', 'bin/gstack-repo-mode', 'test/skill-e2e-triage.test.ts', 'test/helpers/ship-triage-labels.ts', 'scripts/resolvers/testing.ts' ], - 'ship-docsync-completion': ['bin/gstack-docs-candidate', 'bin/gstack-state-root.sh', 'lib/state-root.ts', 'ship/**', 'document-release/**', 'test/skill-e2e-ship-docsync.test.ts', 'test/helpers/docsync-*.ts', 'test/helpers/read-only-commands.ts', 'test/helpers/session-runner.ts', 'test/helpers/hermetic-env.ts', 'bin/gstack-skill-start', 'bin/gstack-session-kind', 'scripts/resolvers/sections.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/gen-skill-docs.ts', 'scripts/resolvers/testing.ts', 'test/helpers/qa-checkpoint-evidence.ts', 'test/helpers/qa-functional-observer.ts', 'test/helpers/e2e-gate.ts', 'test/helpers/qa-evidence-producer.ts', 'test/helpers/qa-functional-fixture.ts', 'bin/gstack-context-recovery'], + 'ship-docsync-completion': ['bin/gstack-docs-candidate', 'bin/gstack-state-root.sh', 'lib/state-root.ts', 'ship/**', 'document-release/**', 'test/skill-e2e-ship-docsync.test.ts', 'test/helpers/docsync-*.ts', 'test/helpers/read-only-commands.ts', 'test/helpers/session-runner.ts', 'test/helpers/hermetic-env.ts', 'bin/gstack-skill-start', 'bin/gstack-session-kind', 'scripts/resolvers/sections.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/gen-skill-docs.ts', 'scripts/resolvers/testing.ts', 'test/helpers/qa-checkpoint-evidence.ts', 'test/helpers/qa-functional-observer.ts', 'test/helpers/e2e-gate.ts', 'test/helpers/qa-evidence-producer.ts', 'test/helpers/qa-functional-fixture.ts', 'bin/gstack-context-recovery', 'test/fixtures/docsync-state-dir-artifacts-public.json'], 'ship-docsync-current': ['bin/gstack-docs-candidate', 'bin/gstack-state-root.sh', 'lib/state-root.ts', 'ship/**', 'document-release/**', 'test/skill-e2e-ship-docsync.test.ts', 'test/helpers/docsync-*.ts', 'test/helpers/read-only-commands.ts', 'test/helpers/session-runner.ts', 'test/helpers/hermetic-env.ts', 'bin/gstack-skill-start', 'bin/gstack-session-kind', 'scripts/resolvers/sections.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/gen-skill-docs.ts', 'scripts/resolvers/testing.ts', 'test/helpers/qa-checkpoint-evidence.ts', 'test/helpers/qa-functional-observer.ts', 'test/helpers/e2e-gate.ts', 'test/helpers/qa-evidence-producer.ts', 'test/helpers/qa-functional-fixture.ts', 'bin/gstack-context-recovery', 'test/fixtures/docsync-candidate-not-found-public.json'], 'ship-docsync-failure': ['bin/gstack-docs-candidate', 'bin/gstack-state-root.sh', 'lib/state-root.ts', 'ship/**', 'document-release/**', 'test/skill-e2e-ship-docsync.test.ts', 'test/helpers/docsync-*.ts', 'test/helpers/read-only-commands.ts', 'test/helpers/session-runner.ts', 'test/helpers/hermetic-env.ts', 'bin/gstack-skill-start', 'bin/gstack-session-kind', 'scripts/resolvers/sections.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/gen-skill-docs.ts', 'scripts/resolvers/testing.ts', 'test/helpers/qa-checkpoint-evidence.ts', 'test/helpers/qa-functional-observer.ts', 'test/helpers/e2e-gate.ts', 'test/helpers/qa-evidence-producer.ts', 'test/helpers/qa-functional-fixture.ts', 'bin/gstack-context-recovery'], - 'ship-docsync-store': ['bin/gstack-docs-candidate', 'bin/gstack-state-root.sh', 'lib/state-root.ts', 'ship/**', 'document-release/**', 'test/skill-e2e-ship-docsync.test.ts', 'test/helpers/docsync-*.ts', 'test/helpers/read-only-commands.ts', 'test/helpers/session-runner.ts', 'test/helpers/hermetic-env.ts', 'bin/gstack-skill-start', 'bin/gstack-session-kind', 'scripts/resolvers/sections.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/gen-skill-docs.ts', 'scripts/resolvers/testing.ts', 'test/helpers/qa-checkpoint-evidence.ts', 'test/helpers/qa-functional-observer.ts', 'test/helpers/e2e-gate.ts', 'test/helpers/qa-evidence-producer.ts', 'test/helpers/qa-functional-fixture.ts', 'bin/gstack-context-recovery'], + 'ship-docsync-store': ['bin/gstack-docs-candidate', 'bin/gstack-state-root.sh', 'lib/state-root.ts', 'ship/**', 'document-release/**', 'test/skill-e2e-ship-docsync.test.ts', 'test/helpers/docsync-*.ts', 'test/helpers/read-only-commands.ts', 'test/helpers/session-runner.ts', 'test/helpers/hermetic-env.ts', 'bin/gstack-skill-start', 'bin/gstack-session-kind', 'scripts/resolvers/sections.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/gen-skill-docs.ts', 'scripts/resolvers/testing.ts', 'test/helpers/qa-checkpoint-evidence.ts', 'test/helpers/qa-functional-observer.ts', 'test/helpers/e2e-gate.ts', 'test/helpers/qa-evidence-producer.ts', 'test/helpers/qa-functional-fixture.ts', 'bin/gstack-context-recovery', 'test/fixtures/docsync-help-not-found-public.json'], // #2733 behavioral proof: the JSON contract survives a firing gate inside a // spawned-marked subagent. Deps name every behavior under test — the // session-kind override, the skill-start gates, both hooks + the shared diff --git a/test/third-party-actions.test.ts b/test/third-party-actions.test.ts index 71325b1e1..050abb7f0 100644 --- a/test/third-party-actions.test.ts +++ b/test/third-party-actions.test.ts @@ -546,7 +546,8 @@ describe('Opus consent renders (gate captures)', () => { '- **D)** We skip it for now and come back to it later.', '> **D)** Defer: I mark the step blocked on you.', 'D) Postpone this step.', 'D) Leave it for later.', "D) Wait for now. I'll mark this step as blocked on you.", '- **D)** Hold off for now; the step stays blocked on you.', 'D) Not yet: we wait on this step for now.', - "D) Let's hold off on it for now."]) expect(offersDefer(option), option).toBe(true); + "D) Let's hold off on it for now.", "D) Wait for now\u2014I'll mark this step as blocked on you.", 'D) Let\u2019s hold off on it for now.']) + expect(offersDefer(option), option).toBe(true); }); test('defer counts only as a lettered option', () => { @@ -560,7 +561,8 @@ describe('Opus consent renders (gate captures)', () => { "B) I drive it in gstack's own browser and wait for your go-ahead for now.", 'A) You wait for now; I drive it in Aside.', 'D) Do not wait for now; I drive it in Aside at once.', 'A) I drive it in Aside; I wait for now while you sign in.', 'A) I drive it in your Aside browser. At the sign-in wall I stop. Wait for now while you sign in, then I continue.', - 'B) Manual instructions: wait for now and do it yourself.', "C) I do it in gstack's own browser. We wait for now only if sign-in is needed."]) + 'B) Manual instructions: wait for now and do it yourself.', "C) I do it in gstack's own browser. We wait for now only if sign-in is needed.", + 'A) I drive it in Aside. I wait for now.', 'B) I drive it in your Aside browser; hold off for now.']) expect(offersDefer(text), text).toBe(false); });