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;