diff --git a/test/fixtures/shared-libs-plan-callers-parity-36776104571.json b/test/fixtures/shared-libs-plan-callers-parity-36776104571.json new file mode 100644 index 000000000..6efde334e --- /dev/null +++ b/test/fixtures/shared-libs-plan-callers-parity-36776104571.json @@ -0,0 +1 @@ +{"questions":[{"question":"D1 — Parser source for the two new Retry-After callers\nProject/branch/task: repo on main; plan review of PLAN.md for the future src/import-worker.ts and src/sync-route.ts.\nELI10: Both new callers need to turn a Retry-After header into a wait time, capped at an hour, with a fallback when the header is missing or junk, behaving exactly like the scheduler. The scheduler already gets that from a tested shared function in lib/retry-after.ts. The plan instead writes a fresh copy of that parser inside each new file. Copies drift: probing the real parser shows quirks a fresh copy would probably handle differently (for example \"-5\" and \"1.5\" are treated as dates and cap at 3600 instead of falling back).\nStakes if we pick wrong: two new parsers that silently disagree with the scheduler on edge cases, with no test that would notice.\nRecommendation: A because it reuses proven code with zero semantic risk, and the extra contract assertions cost a few lines while closing the gaps the callers actually depend on.\nCompleteness: A=10/10, B=7/10, C=4/10\nPros / cons:\nA) Reuse lib, extend contract test (recommended) (human: ~1.5 h / CC: ~5 min)\n ✅ Callers import retrySeconds(value, now, fallback) exactly as src/scheduler.ts does, so scheduler parity holds by construction rather than by test.\n ✅ Adds about seven assertions to test/retry-after.test.ts for empty/whitespace, '0', the 3600/3601 ceiling, past date → 0, unsafe integer, and sub-second ceil; each catches a real regression.\n ✅ Saves about 24 implementation lines versus two local copies (about 4 added instead of 28).\n ❌ A defect in lib/retry-after.ts now reaches three callers instead of one; the contract test is the mitigation.\nB) Reuse lib, existing test only (human: ~1 h / CC: ~3 min)\n ✅ Same import wiring and line savings as A with no change to the shared test file.\n ✅ Smallest possible diff: two imports plus the two integration tests already planned.\n ❌ The five existing assertions leave empty string, past dates, the exact ceiling boundary and unsafe integers unasserted, so a later helper edit could break the callers unnoticed.\nC) Local parser per caller (human: ~3 h / CC: ~10 min)\n ✅ Each caller is self-contained; a change to the shared helper cannot affect it.\n ✅ Matches the current draft, so the plan text needs no rewrite.\n ❌ Adds two more ~14-line copies (src/retry-route.ts and src/retry-worker.ts are already copies) and needs parity tests to hold the fixed contract, which the probe shows is easy to get subtly wrong.\nNet: Reuse trades a slightly larger shared blast radius for guaranteed scheduler parity and a smaller diff; the only real question is whether to close the contract-test gaps now (A) or leave them (B).","header":"Parser reuse","multiSelect":false,"options":[{"label":"Reuse lib, extend contract test (recommended)","description":"✅ Callers import retrySeconds(value, now, fallback) exactly as src/scheduler.ts does, so scheduler parity holds by construction rather than by test.\n✅ Adds about seven assertions to test/retry-after.test.ts for empty/whitespace, '0', the 3600/3601 ceiling, past date → 0, unsafe integer, and sub-second ceil; each catches a real regression.\n✅ Saves about 24 implementation lines versus two local copies (about 4 added instead of 28).\n❌ A defect in lib/retry-after.ts now reaches three callers instead of one; the contract test is the mitigation.\nCompleteness 10/10. Effort human: ~1.5 h / CC: ~5 min. Integration tests per caller prove import wiring, injected now, fallback pass-through and the 3600 ceiling. Existing copies and helper hardening stay unchanged."},{"label":"Reuse lib, existing test only","description":"✅ Same import wiring and line savings as A with no change to the shared test file.\n✅ Smallest possible diff: two imports plus the two integration tests already planned.\n❌ The five existing assertions leave empty string, past dates, the exact ceiling boundary and unsafe integers unasserted, so a later helper edit could break the callers unnoticed.\nCompleteness 7/10. Effort human: ~1 h / CC: ~3 min. Same integration-test obligations as A. Existing copies and helper hardening stay unchanged."},{"label":"Local parser per caller","description":"✅ Each caller is self-contained; a change to the shared helper cannot affect it.\n✅ Matches the current draft, so the plan text needs no rewrite.\n❌ Adds two more ~14-line copies (src/retry-route.ts and src/retry-worker.ts are already copies) and needs parity tests to hold the fixed contract, which the probe shows is easy to get subtly wrong.\nCompleteness 4/10. Effort human: ~3 h / CC: ~10 min. Each caller needs a parity test against lib/retry-after.ts over the same edge-case list plus its integration test. Existing copies and helper hardening stay unchanged."}]}]} \ No newline at end of file diff --git a/test/helpers/shared-libs-plan-actor.ts b/test/helpers/shared-libs-plan-actor.ts index 1254c8fb5..21d6d736c 100644 --- a/test/helpers/shared-libs-plan-actor.ts +++ b/test/helpers/shared-libs-plan-actor.ts @@ -81,13 +81,17 @@ export function createSharedPlanReuseSelector(): SharedQuestionSelector { // The supplied PLAN owns the two future caller identities and fixed scope. // Native questions may refer to them without repeating file names, and an // option may inherit unchanged semantics from its complete decision brief. - if (/\b(?:not|never|no longer)\s+(?:identical|the same|unchanged|preserv\w*|match\w*)\b/i.test(commitment) || - !/\b(?:identical|same|unchanged|preserv\w*|match\w*|keep\w*)\b[^.!?\n]{0,120}\b(?:scheduler|semantics|behavior|contract)\b|\b(?:scheduler|semantics|behavior|contract)\b[^.!?\n]{0,120}\b(?:identical|same|unchanged|preserv\w*|match\w*|keep\w*)\b/i.test(affirmativeCommitments(context + '\n' + commitment))) { + if (/\b(?:not|never|no longer)\s+(?:identical|the same|unchanged|preserv\w*|match\w*|exact\w*)\b|\b(?:no|not|without|break\w*|los(?:e|es|ing))\s+(?:\w+\s+){0,2}parity\b/i.test(commitment) || + !/\b(?:identical|same|unchanged|preserv\w*|match\w*|keep\w*|exactly|parity)\b[^.!?\n]{0,120}\b(?:scheduler|semantics|behavior|contract)\b|\b(?:scheduler|semantics|behavior|contract)\b[^.!?\n]{0,120}\b(?:identical|same|unchanged|preserv\w*|match\w*|keep\w*|exactly|parity)\b/i.test(affirmativeCommitments(context + '\n' + commitment))) { refuse('the selected option must explicitly preserve the current scheduler contract'); } // Inspect the question as well as the selected option: a harmless label must // not authorize an extra commitment hidden in its brief or description. - const proposed = affirmativeCommitments(context + '\n' + commitment); + // "Existing copies and helper hardening stay unchanged" names excluded work. + // Only a bare list of those nouns qualifies; a verb such as "Harden" does not. + const item = String.raw`(?:(?:the|existing|current|its|all|both|helper|parser|lib|shared|caller|scheduler)\s+)*(?:copies|callers|hardening|migrations?|semantics|behaviou?r|contract|helper|parser)`; + const unchangedScope = new RegExp(String.raw`^${item}(?:\s*,\s*${item})*(?:,?\s+and\s+${item})?\s+(?:stays?|remains?)\s+(?:unchanged|untouched)[.!]?$`, 'i'); + const proposed = affirmativeCommitments(context + '\n' + commitment).split('\n').filter(clause => !unchangedScope.test(clause.trim())).join('\n'); const expansions = [ /\b(?:harden\w*|tighten\w*|strict(?:er)?|saniti[sz]\w*|coerc\w*)\b/i, /\b(?:add(?:s|ing)?|insert(?:s|ing)?|introduc(?:e|es|ing)|implement(?:s|ing)?|appl(?:y|ies|ying)|enabl(?:e|es|ing)|creat(?:e|es|ing))\s+(?:(?:a|an|the|one|new|shared|extra|explicit|validation|numeric|malformed|input|parser)\s+)*(?:guard|validator|validation|normalization)\b/i, diff --git a/test/shared-libs-plan-actor.test.ts b/test/shared-libs-plan-actor.test.ts index f93104f11..61a7a3af5 100644 --- a/test/shared-libs-plan-actor.test.ts +++ b/test/shared-libs-plan-actor.test.ts @@ -3,6 +3,7 @@ import { describe, expect, test } from 'bun:test'; import { createSharedPlanReuseSelector } from './helpers/shared-libs-plan-actor'; import { createSharedInteractiveToolHandler } from './helpers/shared-libs-eval-fixture'; import capturedNoHardening from './fixtures/shared-libs-plan-callers-no-hardening-36633323521.json'; +import capturedParity from './fixtures/shared-libs-plan-callers-parity-36776104571.json'; // Exact native R1 from the September 22 timeout. R2 was saved in an Edit, but // never sent as a native AUQ; its public draft fields are reconstructed below. @@ -348,4 +349,39 @@ describe('bounded shared-code planning actor', () => { expect(run.answers).toEqual([]); expect(run.controller.signal.aborted).toBe(true); }); + + test('actual census parity wording preserves the contract; an unchanged-scope list is an exclusion', async () => { + // Exact native question from census 36776104571: "behaving exactly like the scheduler", + // "scheduler parity holds by construction", "Existing copies and helper hardening stay unchanged". + const run = actor(); + const result = await run.callback('AskUserQuestion', capturedParity); + expect(result.updatedInput.answers).toEqual({ [capturedParity.questions[0].question]: capturedParity.questions[0].options[0].label }); + expect(run.refusals).toEqual([]); + }); + + test.each([ + ['exactly like the scheduler', 'not exactly like the scheduler', 'scheduler parity holds', 'breaks scheduler parity'], + ['exactly as src/scheduler.ts does', 'differently from src/scheduler.ts', 'scheduler parity holds', 'without scheduler parity'], + ] as const)('negated parity wording cannot supply preservation: %s', async (a, b, c, d) => { + const input = structuredClone(capturedParity); + const q = input.questions[0]; + q.question = q.question.replaceAll('behaving exactly like the scheduler', 'behaving ' + b.replace('exactly as src/scheduler.ts does', '')).replaceAll(a, b).replaceAll(c, d).replaceAll('guaranteed scheduler parity', 'lost scheduler parity'); + for (const option of q.options) option.description = option.description.replaceAll(a, b).replaceAll(c, d); + const run = actor(); + await expect(run.callback('AskUserQuestion', input)).rejects.toThrow('shared-libs-plan-callers actor:'); + expect(run.answers).toEqual([]); + }); + + test.each([ + 'Harden helper parsing so behavior stays unchanged.', + 'Existing copies stay unchanged and tighten helper validation.', + 'Migrate existing retry-worker.ts; helper hardening stays unchanged.', + ])('an unchanged-scope clause cannot hide an expansion: %s', async extra => { + const input = structuredClone(capturedParity); + input.questions[0].options[0].description += '\n' + extra; + const run = actor(); + await expect(run.callback('AskUserQuestion', input)).rejects.toThrow('question expands'); + expect(run.answers).toEqual([]); + }); }); +