From e6ce1dca7040b094f7a9d67332eded658bec964d Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Tue, 28 Apr 2026 09:05:11 -0700 Subject: [PATCH] test: extract MODE_RE + optionsSignature into PTY runner exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor prep for the upcoming per-finding AskUserQuestion count test across plan-{ceo,eng,design,devex}-review. Both new tests and the existing mode-routing test need the same mode regex and the same option-list fingerprint dedupe — pulling them into one source of truth in test/helpers/claude-pty-runner.ts so a fifth mode (or a tweak to the fingerprint shape) updates everywhere instead of drifting per-test. Mechanical: no behavior change in the mode-routing test. Co-Authored-By: Claude Opus 4.7 (1M context) --- test/helpers/claude-pty-runner.ts | 29 ++++++++++++++++++++ test/skill-e2e-plan-ceo-mode-routing.test.ts | 8 +++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/test/helpers/claude-pty-runner.ts b/test/helpers/claude-pty-runner.ts index dcdbdfd7..92209d3e 100644 --- a/test/helpers/claude-pty-runner.ts +++ b/test/helpers/claude-pty-runner.ts @@ -284,6 +284,35 @@ export function parseNumberedOptions( return found; } +/** + * The four /plan-ceo-review modes. Used by `skill-e2e-plan-ceo-mode-routing` + * to detect Step 0F mode-selection AskUserQuestions, and by the upcoming + * finding-count tests as a Step-0 boundary signal: an AUQ whose options + * match this regex IS the mode pick (the last Step-0 question for plan-ceo). + * + * Lifted out of the mode-routing test so multiple PTY tests can share one + * source of truth — when /plan-ceo-review adds a fifth mode, one regex updates + * everywhere instead of drifting per-test. + */ +export const MODE_RE = /HOLD SCOPE|SCOPE EXPANSION|SELECTIVE EXPANSION|SCOPE REDUCTION/i; + +/** + * Stable signature for a parsed numbered-option list — used by tests to detect + * "is this AUQ the same as the last poll, or has the agent advanced to a new + * one?" Joins each option as `${index}:${label}` after sorting by index. + * + * Defensive sort means the signature is order-independent at the input level, + * even though `parseNumberedOptions` already returns indices in ascending order. + */ +export function optionsSignature( + opts: Array<{ index: number; label: string }>, +): string { + return [...opts] + .sort((a, b) => a.index - b.index) + .map((o) => `${o.index}:${o.label}`) + .join('|'); +} + /** * Pure classifier for the visible TTY buffer. Decides which outcome the * polling loop should return on this tick, or `null` to keep polling. diff --git a/test/skill-e2e-plan-ceo-mode-routing.test.ts b/test/skill-e2e-plan-ceo-mode-routing.test.ts index e06e705d..0199413b 100644 --- a/test/skill-e2e-plan-ceo-mode-routing.test.ts +++ b/test/skill-e2e-plan-ceo-mode-routing.test.ts @@ -37,6 +37,8 @@ import { isPermissionDialogVisible, parseNumberedOptions, isPlanReadyVisible, + MODE_RE, + optionsSignature, TAIL_SCAN_BYTES, type ClaudePtySession, } from './helpers/claude-pty-runner'; @@ -44,8 +46,6 @@ import { const shouldRun = !!process.env.EVALS && process.env.EVALS_TIER === 'periodic'; const describeE2E = shouldRun ? describe : describe.skip; -const MODE_RE = /HOLD SCOPE|SCOPE EXPANSION|SELECTIVE EXPANSION|SCOPE REDUCTION/i; - interface ModeCase { mode: 'HOLD SCOPE' | 'SCOPE EXPANSION'; /** Regex applied to visible-since-mode-pick text. At least one must match. */ @@ -96,8 +96,8 @@ async function navigateToModeAskUserQuestion( // Has the rendered list changed since last poll? If not, we're seeing // the same prompt and shouldn't double-press. - const sig = opts.map(o => `${o.index}:${o.label}`).join('|'); - const lastSig = lastSeenList.map(o => `${o.index}:${o.label}`).join('|'); + const sig = optionsSignature(opts); + const lastSig = optionsSignature(lastSeenList); if (sig === lastSig) continue; lastSeenList = opts;