From 470ac550c273e8a4e36ee360cfd1099f6a04bd8f Mon Sep 17 00:00:00 2001 From: garrytan Date: Mon, 26 Oct 2026 14:39:02 +0000 Subject: [PATCH] fix: pre-landing review fixes offersDefer: the wait/hold-off-for-now paraphrase must open the option or a sentence and end it, so a drive option's "I wait for now while you sign in" or a manual option's "wait for now and do it yourself" no longer reads as the defer option (red-team finding on #3472). Negative controls added. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- test/helpers/third-party-actions.ts | 5 +++-- test/third-party-actions.test.ts | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/test/helpers/third-party-actions.ts b/test/helpers/third-party-actions.ts index d994b550c..9b81079f9 100644 --- a/test/helpers/third-party-actions.ts +++ b/test/helpers/third-party-actions.ts @@ -49,11 +49,12 @@ 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". + * 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. */ 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/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)); /** 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/third-party-actions.test.ts b/test/third-party-actions.test.ts index 7a06d8322..71325b1e1 100644 --- a/test/third-party-actions.test.ts +++ b/test/third-party-actions.test.ts @@ -558,7 +558,10 @@ describe('Opus consent renders (gate captures)', () => { 'D) I drive it later in Aside.', 'I will defer this step for now.', 'Wait for now.\nA) I drive it in your Aside browser', 'A) I drive it in your Aside browser; at the sign-in wall I wait for now while you sign in, then continue.', "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.']) expect(offersDefer(text), text).toBe(false); + '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."]) + expect(offersDefer(text), text).toBe(false); }); test('the Aside drive offer is found in every tpa-present render, including first-person and blockquoted options', () => {