Files
gstack/test/skill-e2e-plan-ceo-finding-count.test.ts
Garry TanandOpenAI Codex 9f81911136 v1.86.0.0 feat: route outside reviews by harness (#2850)
* feat: add a restricted and supervised Claude Code runner

Preserve configured authentication and models while enforcing tool access, strict completion JSON, bounded output and process cleanup. Cover argv, failure handling, session metadata and Windows process containment.

* feat: route outside reviews by harness and migrate wrapper installs

Use Claude Code from Codex and Codex from other supported hosts, with shared invocation rendering, positive gate validation and per-phase provenance. Rename /claude to /claude-code, repair managed shared and copied installations safely, and generate native Kiro skills. Add installed-workflow, failure-injection and live cross-harness regression coverage.

* test: recognize CEO mode labels without terminal spacing

The paid workflow rendered SCOPEEXPANSION at option 4, but its driver required a literal space. Match the leading mode title without cursor-spacing artifacts and ignore adjacent preview text. Preserve missing-target failures and downstream posture assertions.

* test: isolate plan-count fixtures before starting review workflows

Seed the complete test plan in a private git repository before launching Claude, so a bare slash command cannot review the live workspace while a delayed fixture message remains queued. Preserve count thresholds, parsers and budgets. Add initial-context and installed-discovery tests, and retain startup/terminal diagnostics on failed evaluations.

* test: stabilize review fixtures and Claude eval startup

Preserve source boundaries in workflow judge inputs, isolate CEO mode plans, and wait for interactive trust input readiness. Keep startup failure evidence and retain existing models, budgets, and assertions.

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* test: classify collapsed review modes and isolate seeded findings

Keep review questions out of the setup count when terminal cursor positioning removes spaces. State existing webhook safeguards so the five-finding control measures its seeded defects without accidental extra security and concurrency gaps. Preserve question bands and the paired control.

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* test: isolate browser daemon state across free shards

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* test: stabilize native review counting and interactive navigation

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* chore: prepare v1.82.0.0 release

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix: eliminate browser and process-cleanup test flakes

Pin every CI surface to Bun 1.4.0 to avoid extra-stdio finalizers closing
reused live sockets. Add an isolated GC/listener regression that fails on
Bun 1.3.13, and prevent coordinated rollback to an affected CI runtime.

Check renderer cleanup against the render's own staging directory so
concurrent renders cannot invalidate the assertion. Make the no-pgrep
process-tree walk tolerate disappearing /proc entries, and synchronize
its test fixture through child readiness and pipe EOF instead of sleeps.

Validation: 9,157 passed, 31 skipped, zero failures across 556 files with
retries disabled. Build, all-host generation freshness, and skill checks
passed. All three races have failing-before/passing-after regressions.

* fix: count completed native review questions in evals

* fix: drive review navigation from confirmed native choices

* fix: require complete section-loading eval reports

* test: isolate telemetry HTTP transport from local assertions

* fix: keep review input on the active native question

* test: let tunnel revocation daemon choose an available port

* test: allocate available ports for pairing and watchdog fixtures

* fix: stabilize planning eval navigation and phase reporting

* test: isolate installed runtime paths in planning evals

* test: stabilize review evidence and concurrent refresh fixtures

* fix: resolve design findings before editing the plan

* fix: honor and persist disabled outside plan reviews

* fix: preserve planning decisions and terminal evidence

Load installed host reviews at autoplan phase entry and wait for completed
reviewers and saved artifacts. Reuse approved remedies while preserving
individual finding decisions.

Drive interactive evals from the current terminal viewport, bind native
questions across scrolling, and require complete native report evidence.
Cover captured stale menus, permission lifecycles, setup classification,
and disabled-review tool availability with deterministic regressions.

Advance release metadata and the upgrade migration to the unclaimed
1.83.0.0 slot.

* fix: drive native review questions and preserve current plans

Use the native single-choice keyboard protocol and current terminal viewport,
with per-question navigation inside packets and completed-call coverage.
Keep permissions, multi-select menus, and Submit controls distinct.

Send Autoplan reviewers the amended implementation plan, keep its review record
separate, and supply retained application contracts in the chain fixture.
Clarify individual DevEx decisions and complete CEO fix options; use one active
plan destination for the section-loading report.

* fix: preserve complete plan-review decisions

* fix: recognize native plan dialogs and reviewer controls

* fix: preserve review decisions and phase completion

* fix: recognize completed reviews without losing findings

* fix: preserve review continuity and native eval completion

* test: fix native review completion and eval retry isolation

* test: handle native review menus and complete eval fixtures

* test: fix native review setup, completion, and isolation failures

* test: limit native skill discovery to runtime assets

* fix: bind Autoplan reviews to full ordered phase inputs

* test: fix planning eval routing, counting, and timeout handling

* chore: advance queued release to v1.84.0.0

* fix: preserve complete review inputs and planning decisions

* fix: reconcile review approvals and preserve phase obligations

* fix: preserve review obligations and unblock eval permissions

Carry recorded Autoplan requirements into blind phase inputs, require Eng
review approvals before exit, and exercise combined asynchronous flows in
CEO reviews. Correct native finding and handoff classification and unblock
repeated report edits using scoped request identities.

* fix: retain plan requirements and complete native review dialogs

* fix: complete native review prompts and retain plan references

* fix: preserve review inputs and classify native eval evidence

* fix: check competing completion orders in CEO reviews

* fix: recognize review decisions and require phase methodology

Require the current phase methodology before Autoplan snapshots. Correct
substantive decision, closed handoff, and cache-finding classification, and
honor the recommended implementation approach in native review dialogs.

Add captured-transcript regressions without changing review thresholds,
provider models, retries, or deadlines.

* test: bind native review decisions and close completed handoffs

* fix: complete review dialogs and verify methodology delivery

* fix: preserve review evidence and unblock native eval prompts

* fix: handle native review question completions

* fix: recognize native review narration and controls

* fix: count native review decisions and isolate eval fixtures

* test: verify seeded review coverage and current artifact permissions

* test: isolate model and brain-aware skill renders

* fix: repair native workflow evaluation and clarify review steps

* fix: stabilize workflow eval evidence and review guidance

* test: repair native workflow observation and fixture isolation

* fix: recognize completed workflow evidence and owned skill reads

* test: repair seeded workflow delivery and completion evidence

* test: recognize current review evidence across native forms

* test: handle native review variants and permission redraws

* fix: honor review preferences and recognize native eval evidence

* test: recognize completed review decisions and queued permissions

* test: match current review contracts and partial-line edits

* test: recognize completed workflow evidence and bounded human waits

* fix: preserve review entry gates and native eval interactions

* fix: recognize native workflow evidence and preserve review gates

* test: recognize current review evidence and preconfigure workflow fixtures

* test: recognize completed review findings and scoped artifact permissions

* fix: stabilize native workflow review and permission evidence

* fix: recognize current review evidence and scoped edit confirmations

Clarify Design and engineering review entry instructions and Design scoring.
Recognize required legacy coverage and public Autoplan completion recaps.
Bind the pending Edit confirmation to its exact file, ordered digest, and
one-request approval when a preceding command display remains visible.
Keep reviews within their existing size limits and preserve scope gates
when extracting workflow fixtures from either supported preamble header.

Keep failure outcomes, review thresholds, provider choices, and eval budgets.

* fix: recover review workflow progress and eval evidence

* fix: recognize valid review evidence and scope selection

* test: fix review evidence parsing and repeated artifact prompts

* test: recognize valid review decisions and pending native cards

* fix(plan-eng-review): keep final navigation consistent with approved tasks

* test: recognize valid review evidence and bind legacy diff requests

* fix: stabilize review eval evidence and harness repair guidance

* docs: update project documentation for v1.85.0.0

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* test: fix Windows CI fixtures and credential scan

Rebase captured JSON values and filesystem evidence using the appropriate
path convention. Compile native fake CLIs on Windows and synchronize pipe
holder readiness, with cleanup retained when assertions fail.

Assemble synthetic credential fixtures at runtime so the added-line scan
keeps enforcing the same gate without flagging its own rejection controls.

Discover generated skills directly for the empty-find regression check,
avoiding a recursive scan through saved evaluation artifacts and dependencies.

* fix: preserve source renders on Windows

Compare canonical generator paths using native separators so an output
sidecar pointing at the source cannot overwrite its skill or metadata.
Keep the regression fixture isolated from the real checkout and expose
freshness diagnostics before asserting subprocess status.

Detach Windows drain-test pipe holders from the fake provider's automatic
child cleanup while preserving the enclosing runner job and its assertions.

* fix: clarify outside review fallback and CEO decisions

Render one applicable own-harness fallback path and retain native review,
disabled policy, and missing-coverage semantics. Align report field names
and mode labels, and make the existing per-cut scope approval explicit.

Regenerate skill outputs and keep the workflow judge's model, thresholds,
and retry policy unchanged.

* chore: move release to free version slot (v1.86.0.0)

PR #2852 now claims v1.85.0.0. Align the release metadata and
rename migration so upgrades from that version still receive it.

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix: include engineering review prerequisites and restore branch context

* fix: recognize coverage diagrams and clarify design review instructions

* fix: preserve file identities and join Windows test processes

---------

Co-authored-by: OpenAI Codex <noreply@openai.com>
2026-09-14 14:32:45 -07:00

385 lines
20 KiB
TypeScript

/**
* /plan-ceo-review per-finding AskUserQuestion count (periodic, paid, real-PTY).
*
* Asserts the load-bearing rule "One issue = one AskUserQuestion call" by
* driving /plan-ceo-review against a 5-finding seeded plan and counting
* distinct review-phase AUQs. Passes when count is in [N-1, N+2].
*
* Two tests in this file:
* - 5-finding distinct fixture: count band assertion + D19 review-report-at-bottom.
* - 2-finding paired control (D12 positive control): related findings still
* produce 2 distinct AUQs, not 1 batched, when the rule is honored.
*
* Tier: periodic. Each run drives Step 0 + 11 review sections end-to-end
* (~25 min, ~$5/run). Sequential by default per plan §D15. See
* test/helpers/claude-pty-runner.ts for runPlanSkillCounting internals.
*/
import { test } from 'bun:test';
import { describeE2ETier } from './helpers/e2e-gate';
import { isCeoCompletionHandoff } from './helpers/ceo-completion-handoff';
import { pickCeoCountQuestion } from './helpers/ceo-approach-pick';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import {
runPlanSkillCounting,
ceoStep0Boundary,
ceoFirstReviewAUQ,
assertReviewReportAtBottom,
type AskUserQuestionFingerprint,
} from './helpers/claude-pty-runner';
/**
* /plan-ceo-review's first AUQ asks "what scope?" with options like
* 1. Branch diff vs main
* 2. A specific plan file or design doc
* 3. An idea you'll describe inline
* ...
* 7. Skip interview and plan immediately
*
* The default pick (1) routes to "branch diff vs main" — the wrong target
* for our seeded fixture (the agent would review the gstack PR itself,
* recursively). Picking "Skip interview and plan immediately" bypasses
* Step 0 and routes the agent to review the fixture request, which is
* already present in its initial project context.
*/
function pickSkipInterview(fp: AskUserQuestionFingerprint): number {
const skipOpt = fp.options.find((o) =>
/skip\s+interview|plan\s+immediately/i.test(o.label),
);
if (skipOpt) return skipOpt.index;
// Fallback: "describe inline" also routes to using our pasted plan.
const inlineOpt = fp.options.find((o) =>
/describe.*inline|inline.*idea/i.test(o.label),
);
if (inlineOpt) return inlineOpt.index;
return 1;
}
const describeE2E = describeE2ETier('periodic');
const N_DISTINCT = 5;
const FLOOR_DISTINCT = N_DISTINCT - 1; // 4 (D11)
const CEILING_DISTINCT = N_DISTINCT + 2; // 7 (D11)
const N_PAIRED = 2;
const FLOOR_PAIRED = 2;
const CEILING_PAIRED = 4;
// Keep the five seeded defects distinct from already-satisfied surrounding
// contracts. Live controls correctly found extra ingress, missing-user,
// observability, and rollout gaps when those baseline facts were unspecified.
const planCeo5Findings = (planPath: string) => [
`Please review this plan thoroughly in HOLD SCOPE mode. As you go, write your plan-mode plan to ${planPath} (use Edit/Write to that exact path).`,
'Proceed directly to the requested CEO review; skip the optional /office-hours prerequisite.',
'Finish after this CEO review; I will handle subsequent reviews manually.',
'',
'# Plan: Payment Processing Integration',
'',
'## Existing contracts retained',
'The approved motivation is to move payment orchestration out of the prior',
'library-adapter handler into application-owned code while retaining the',
'existing payment and receipt product behavior. The shared dispatcher remains',
'available; the proposed bypass below is still an architectural choice to review.',
'The existing ingress middleware verifies the Stripe signature against the',
'raw request body and rejects invalid signatures before invoking handlers.',
'The existing ingress forwards only `payment_intent.succeeded` events to',
'this handler; other Stripe event types are acknowledged without invoking it.',
'The existing payload adapter exposes `event.data.object.metadata.user_id`',
'as `request.params.userId`. This params object is the parsed body-data map,',
'not URL query/path parameters; all users share one webhook URL.',
'The adapter acknowledges missing, nil, or empty user_id metadata with',
'HTTP 200 and an event-correlated warning before invoking this handler.',
'For every nonempty external string it performs no SQL-format validation.',
'The adapter forwards that external string unchanged. It does not cast,',
'escape, or SQL-sanitize it; a valid signature does not make it safe for SQL.',
'User IDs are opaque TEXT values, including punctuation and Unicode. The',
'lookup has no integer/UUID cast or ID-format restriction; every nonempty',
'string is a valid identifier representation.',
'An existing ingress ownership guard checks the PaymentIntent ID against',
'its stored opaque user-ID binding before invoking the handler. A mismatch',
'is acknowledged with HTTP 200 and an event-correlated warning. This is an',
'identity comparison, not SQL-format validation; the adapter still forwards',
'the original string unchanged.',
'The existing webhook event guard deduplicates deliveries by Stripe event ID,',
'and an existing per-user lock serializes payment updates.',
'The event guard acquires the existing per-user lock before checking the',
'committed completion marker, and rechecks after any lock wait. It holds',
'that lock through the handler and completion bookkeeping; an overlapping',
'completed duplicate does not invoke the handler.',
'The new handler runs inside those unchanged guards; this plan does not',
'replace signature verification, event deduplication, or update locking.',
'The existing user update assigns payment_status=paid and the payment intent',
'ID; it does not increment a balance or counter. Repeating the same payment',
'intent assigns the same values, independently of the event-ID guard.',
'The existing lookup-result guard acknowledges unknown/deleted users with',
'HTTP 200, logs the event, and stops before user updates or email fan-out.',
'The retained recipient-policy helper treats a nil or empty email address as',
'skipped_missing_address: payment processing continues normally, and no mail',
'client call is attempted. It persists an event/user/PaymentIntent-correlated',
'skip record, emits a structured warning, and increments the existing counter.',
'The existing notification runbook already covers that skip result: correct',
'the account address, then retry only its recorded notification using the',
'same PaymentIntent idempotency key. It never replays the payment for this case.',
'That recipient policy does not catch failures from sends to nonempty addresses;',
'the shared mail client still rethrows those exceptions to this handler.',
'Account deletion uses the same per-user lock. The handler holds it from',
'lookup through update and inline email, so deletion either precedes lookup',
'(the existing unknown/deleted-user path) or follows the handler; it cannot',
'remove the user between lookup and update.',
'The ingress wrapper already logs event IDs, outcomes, and durations, with',
'alerts for failed webhook processing. Those controls remain in place.',
'The existing DB and mail clients attach the adapter user ID and event ID',
'to outcome traces, including update success and email delivery success or',
'failure. These shared clients rethrow exceptions unchanged; tracing does',
'not rescue email errors or change the inline email call below.',
'The shared mail client also publishes its delivery failure rate to the',
'existing dashboard and tested on-call alert, including caught exceptions.',
'The existing incident runbook uses the correlated DB and mail outcomes to',
'distinguish committed payments from failed notifications. It directs on-call',
'to check provider status and retry only the failed notification through the',
'existing notification retry procedure, never replay the payment blindly.',
'DB lookup/update exceptions propagate to that ingress wrapper, which logs',
'the failure and returns HTTP 500 so Stripe retries the event. The existing',
'event-ID dedup guard records completion only after the database transaction',
'commits; failed or rolled-back database attempts remain retryable.',
'The deployment already has a handler feature flag and a documented, tested',
'rollback to the prior handler; this change uses that existing rollout path.',
'That documented manual rollout checklist already requires a staging',
'payment-event replay for this handler and verification of the user update,',
'email delivery, and correlated outcome trace before enabling it broadly.',
'This is manual deployment verification, not automated handler regression',
'coverage; no new automated tests are planned in the Tests section below.',
'The existing notification contract sends one payment receipt per PaymentIntent,',
'including a summary of the user orders. With zero orders it still sends one',
'receipt with an empty order summary; the order loop is data loading, never',
'one email or payment update per order. These product semantics are retained.',
'The shared mail client already derives a provider idempotency key from that',
'PaymentIntent ID. The provider durably suppresses duplicate successful sends',
'for the same key across process crashes, webhook retries, and manual retries.',
'Before rethrowing a failed or timed-out send, that client durably records the',
'notification attempt for the existing retry procedure. The dashboard and',
'on-call alert already monitor failed-notification age and backlog after an',
'outage clears, as well as failure rate; the runbook retries those records.',
'The existing mail-client deadline is one second, enforced by cancellation',
'of the provider request with no inline retries. It raises MailTimeout on',
'expiry. The retained DB/ingress deadlines bound their combined work to two',
'seconds, leaving headroom inside the existing ten-second webhook deadline.',
'Neither deadlines nor retry records catch the mail exception for this handler;',
'the shared client still rethrows it to the inline caller described below.',
'Every existing event-correlated outcome trace includes the active handler',
'identity (prior or new), so rollout attribution is already available.',
'If a separate handler class is retained, its already-approved name is',
'`Webhooks::StripePaymentWebhookHandler` in the application-owned namespace,',
'never the Stripe library namespace. This naming choice is settled; whether',
'to add a separate implementation or reuse WebhookDispatcher remains open.',
'',
'## Architecture',
"We're adding a new `StripePaymentWebhookHandler` class that will handle Stripe webhooks.",
'This bypasses the existing `WebhookDispatcher` module — we want a clean',
'namespace separation.',
'',
'## Database access',
'The new endpoint reads `request.params.userId` directly into a raw SQL',
'fragment for the lookup query.',
'',
'## Webhook fan-out',
'On payment success we update the user record AND fire a notification email.',
'Both happen inline; no error handling on the email leg.',
'',
'## Tests',
"None planned. We'll rely on the existing integration suite catching regressions.",
'',
'## Performance',
'Each webhook lookup hits the database for the user, then fetches each',
'order in a loop.',
].join('\n');
const planCeo2PairedFindings = (planPath: string) => [
`Please review this plan thoroughly in HOLD SCOPE mode. As you go, write your plan-mode plan to ${planPath} (use Edit/Write to that exact path).`,
'Proceed directly to the requested CEO review; skip the optional /office-hours prerequisite.',
'Finish after this CEO review; I will handle subsequent reviews manually.',
'',
'# Plan: Payment Processing — Test Coverage',
'',
'## Existing coverage and test infrastructure retained',
'This changes unit tests only; processPayment() production behavior stays as-is.',
'The Stripe adapter suite already covers network timeouts, card declines (402),',
'rate limits (429), and recovery when an initial 502 is followed by a successful',
'charge. Receipt-builder failure behavior has its own passing regression tests.',
'The payment test factory explicitly configures max_retries=1 and exposes the',
'Stripe mock call history. Its injected virtual sleeper records backoff without',
'real delays, so an exhausted 502 operation makes exactly two charge attempts.',
'These existing helpers and regression suites remain in use for this change.',
'',
'## Existing behavior retained',
'A successful charge returns a receipt with chargeId copied from Stripe,',
'amountCents equal to the requested integer amount, and currency equal to',
'the requested currency. For a 1000-cent USD charge returning id ch_paid,',
'the receipt is { chargeId: "ch_paid", amountCents: 1000, currency: "USD" }.',
'On repeated 502 responses, max_retries=1 means two total charge attempts',
'separated by one recorded 100 ms backoff, followed by PaymentUnavailable.',
'These contracts are already implemented; this plan adds their unit coverage.',
'',
'## Proposed tests',
'Add two tests in the existing processPayment suite using its current factory,',
'Stripe mock and virtual sleeper. Other tests and production code stay as-is.',
'',
'1. Successful charge: arrange the Stripe mock to return id ch_paid, call',
' processPayment with amountCents=1000 and currency=USD, and assert only',
' that the returned receipt is truthy. This is the complete planned assertion.',
'2. Repeated 502: arrange two consecutive Stripe 502 responses, call',
' processPayment, and assert only that it rejects with PaymentUnavailable.',
' No assertion about the mock call history or virtual sleeper record',
' is planned for this test.',
].join('\n');
describeE2E('/plan-ceo-review per-finding AskUserQuestion count (periodic)', () => {
test(
`5-finding plan emits ${FLOOR_DISTINCT}-${CEILING_DISTINCT} review-phase AskUserQuestions`,
async () => {
// Per-run artifact dir: a hardcoded shared /tmp path collides under
// --retry, EVALS_JOBS>1, or concurrent worktrees (a sibling's finally-
// rmSync deletes this run's artifact → spurious D19 failure).
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-e2e-plan-ceo-'));
const planPath = path.join(tmpDir, 'gstack-test-plan-ceo.md');
try {
const obs = await runPlanSkillCounting({
skillName: 'plan-ceo-review',
slashCommand: '/plan-ceo-review',
followUpPrompt: planCeo5Findings(planPath),
expectedPlanPath: planPath,
isLastStep0AUQ: ceoStep0Boundary,
isFirstReviewAUQ: ceoFirstReviewAUQ,
isCompletionHandoffAUQ: isCeoCompletionHandoff,
pickAUQ: pickCeoCountQuestion,
reviewCountCeiling: CEILING_DISTINCT + 1, // hard cap above assertion ceiling
firstAUQPick: pickSkipInterview, // bypass scope-selection, route to review
timeoutMs: 1_500_000, // 25 min
env: { QUESTION_TUNING: 'false', EXPLAIN_LEVEL: 'default' },
});
if (!['plan_ready', 'completion_summary', 'ceiling_reached'].includes(obs.outcome)) {
throw new Error(
`plan-ceo-review finding-count FAILED: outcome=${obs.outcome}\n` +
`step0=${obs.step0Count} review=${obs.reviewCount} elapsed=${obs.elapsedMs}ms\n` +
`fingerprints (last 8):\n` +
obs.fingerprints
.slice(-8)
.map(
(f, i) =>
` ${i}. preReview=${f.preReview} sig=${f.signature.slice(0, 12)} prompt="${f.promptSnippet.slice(0, 60)}"`,
)
.join('\n') +
`\n--- evidence (last 3KB) ---\n${obs.evidence}`,
);
}
if (obs.reviewCount < FLOOR_DISTINCT) {
throw new Error(
`BAND FAIL (below floor): reviewCount=${obs.reviewCount} < FLOOR=${FLOOR_DISTINCT}.\n` +
`Likely batching regression — agent collapsed multiple findings into fewer questions.\n` +
`Fingerprints (review-phase only):\n` +
obs.fingerprints
.filter((f) => !f.preReview && !f.administrative)
.map((f) => ` - "${f.promptSnippet.slice(0, 80)}"`)
.join('\n'),
);
}
if (obs.reviewCount > CEILING_DISTINCT) {
throw new Error(
`BAND FAIL (above ceiling): reviewCount=${obs.reviewCount} > CEILING=${CEILING_DISTINCT}.\n` +
`Captured observation:\n${JSON.stringify(obs, null, 2)}`,
);
}
// D19: review report at bottom of plan file.
if (!fs.existsSync(planPath)) {
throw new Error(
`D19 FAIL: agent did not produce expected plan file at ${planPath}.\n` +
`Either the agent ignored the path instruction in the follow-up prompt, or\n` +
`the helper exited before the agent wrote the file. ` +
`outcome=${obs.outcome} review=${obs.reviewCount}`,
);
}
const planContent = fs.readFileSync(planPath, 'utf-8');
const verdict = assertReviewReportAtBottom(planContent);
if (!verdict.ok) {
throw new Error(
`D19 FAIL: plan file at ${planPath} ${verdict.reason}\n` +
(verdict.trailingHeadings
? `Trailing headings: ${verdict.trailingHeadings.join(' | ')}\n`
: '') +
`--- plan content (last 1KB) ---\n${planContent.slice(-1024)}`,
);
}
} finally {
try {
fs.rmSync(tmpDir, { recursive: true, force: true });
} catch {
/* best-effort */
}
}
},
1_500_000 /* physical ceiling: the 25-min CI job + 1800s shard wall cap what can actually execute */,
);
test(
`paired-finding positive control: ${N_PAIRED} related findings produce ${FLOOR_PAIRED}-${CEILING_PAIRED} AskUserQuestions`,
async () => {
// Per-run artifact dir — see the distinct-findings test above.
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-e2e-plan-ceo-paired-'));
const planPath = path.join(tmpDir, 'gstack-test-plan-ceo-paired.md');
try {
const obs = await runPlanSkillCounting({
skillName: 'plan-ceo-review',
slashCommand: '/plan-ceo-review',
followUpPrompt: planCeo2PairedFindings(planPath),
expectedPlanPath: planPath,
isLastStep0AUQ: ceoStep0Boundary,
isFirstReviewAUQ: ceoFirstReviewAUQ,
isCompletionHandoffAUQ: isCeoCompletionHandoff,
pickAUQ: pickCeoCountQuestion,
reviewCountCeiling: CEILING_PAIRED + 1,
timeoutMs: 1_500_000,
env: { QUESTION_TUNING: 'false', EXPLAIN_LEVEL: 'default' },
});
if (!['plan_ready', 'completion_summary', 'ceiling_reached'].includes(obs.outcome)) {
throw new Error(
`paired-finding control FAILED: outcome=${obs.outcome}\n` +
`step0=${obs.step0Count} review=${obs.reviewCount}\n` +
`--- evidence (last 3KB) ---\n${obs.evidence}`,
);
}
if (obs.reviewCount < FLOOR_PAIRED) {
throw new Error(
`PAIRED CONTROL FAIL: reviewCount=${obs.reviewCount} < FLOOR=${FLOOR_PAIRED}.\n` +
`Two deliberately related findings were batched into <2 questions — the rule failed under D12.\n` +
`Review-phase fingerprints:\n` +
obs.fingerprints
.filter((f) => !f.preReview && !f.administrative)
.map((f) => ` - "${f.promptSnippet.slice(0, 80)}"`)
.join('\n'),
);
}
if (obs.reviewCount > CEILING_PAIRED) {
throw new Error(
`PAIRED CONTROL FAIL: reviewCount=${obs.reviewCount} > CEILING=${CEILING_PAIRED} (over-asking on a 2-finding fixture).\n` +
`Captured observation:\n${JSON.stringify(obs, null, 2)}`,
);
}
} finally {
try {
fs.rmSync(tmpDir, { recursive: true, force: true });
} catch {
/* best-effort */
}
}
},
1_500_000 /* physical ceiling: the 25-min CI job + 1800s shard wall cap what can actually execute */,
);
});