tooling: add pre-anchored update-only mode to canonical harness
Add FIXTURE_MODE=updateOnly support that bypasses Start and sends the committed fixture (tests/fixtures/pre-anchored-update-savings-realism.json) directly as an Update request body through production HTTP route. scripts/reproduce-multi-turn-investigation.mjs: - Added ESM imports for deterministic fixture loading (fs, fileURLToPath, path) - Added FIXTURE_PATH constant pointing to committed fixture - Added fixtureMode env-var selector and runUpdateOnlyMode() function - Validates ANSWER_2 before any live call (zero calls if missing) - Verifies single savings-realism anchor invariant on load - Preserves all hardened capture fields in pre-anchored mode - Normal-mode Start→Update chain preserved under guard clause tests/reproduce-multi-turn-investigation.harness.test.js: - Added 7 new harness tests for pre-anchored scenarios (46 total, all pass) - Updated runPreAnchoredSimulation to persist rejectedProposalSnapshot on rejection - Added runPreAnchoredSimulationWithBlock() helper docs/: - New docs/experiment-57j78.md with full apparatus description - Updated docs/current-handoff.md with 57J.78 section
This commit is contained in:
@@ -802,6 +802,174 @@ describe("reproduce-multi-turn-investigation harness: one-shot semantics", () =>
|
||||
const retryEntries = r.apiLog.filter((e) => e.step === "update" && e.answer?.includes("_retry"));
|
||||
expect(retryEntries.length).toBe(0);
|
||||
});
|
||||
|
||||
// ── 57J.78: update-only mode harness tests ────────────
|
||||
|
||||
it("57J.78 ANSWER_2 blocked → zero live calls, reports blocked", () => {
|
||||
const r = runPreAnchoredSimulationWithBlock();
|
||||
expect(r.type).toBe("blocked_no_answer");
|
||||
expect(r.blockedMessage).toContain("missing ANSWER_2");
|
||||
});
|
||||
|
||||
it("57J.78 accepted structuralActionRequired=true preserved in capture", () => {
|
||||
const r = runPreAnchoredSimulation({
|
||||
answer: "I am unsure whether the projected office savings from the relocation are realistic.",
|
||||
onResponseUpdate: () => ({
|
||||
success: true,
|
||||
stage: "update_applied",
|
||||
updatedSituationGraph: {
|
||||
nodes: [
|
||||
{ id: "n_relocation_state", kind: "state", label: "london-manchester", status: "provisional" },
|
||||
{ id: "n_proj_validation", kind: "unknown", label: "Validation of projected office savings", status: "unknown" },
|
||||
],
|
||||
edges: [{ fromNodeId: "n_proj_validation", toNodeId: "n_relocation_state", relationship: "depends_on" }],
|
||||
},
|
||||
selectedQuestion: { question: "What evidence would clarify validation?", nodeId: "n_proj_validation" },
|
||||
structuralActionRequired: true,
|
||||
answerMeaning: {
|
||||
userSupportedMeaning: "User is unsure whether projected savings are realistic.",
|
||||
possibleInference: null,
|
||||
supportCategory: "uncertain",
|
||||
resolutionGuidance: null,
|
||||
},
|
||||
updatedProposal: {
|
||||
addedNodes: [{ id: "n_proj_validation" }],
|
||||
addedEdges: [],
|
||||
updatedNodes: [],
|
||||
resolvedUnknownNodeIds: [],
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(r.startCalls).toBe(0);
|
||||
expect(r.updateCalls).toBe(1);
|
||||
expect(r.type).toBe("all_success");
|
||||
expect(r.captured.structuralActionRequired).toBe(true);
|
||||
expect(r.captured.answerMeaning.userSupportedMeaning).toContain("unsure");
|
||||
expect(r.captured.proposal.addedNodes.length).toBe(1);
|
||||
expect(r.captured.graph.nodes.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("57J.78 rejected structuralActionRequired/rejectedProposalSnapshot preserved", () => {
|
||||
const r = runPreAnchoredSimulation({
|
||||
answer: "I am unsure whether the projected office savings from the relocation are realistic.",
|
||||
onResponseUpdate: () => ({
|
||||
success: false,
|
||||
stage: "proposal_compatibility",
|
||||
errors: ["structuralActionRequired is true but proposal contains no graph mutation"],
|
||||
diagnostics: {
|
||||
rejectedProposalSnapshot: {
|
||||
structuralActionRequired: true,
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
updatedNodes: [{ nodeId: "nz4k4ep", newValue: null }],
|
||||
resolvedUnknownNodeIds: [],
|
||||
userSupportedMeaning: "User is unsure...",
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(r.startCalls).toBe(0);
|
||||
expect(r.updateCalls).toBe(1);
|
||||
expect(r.type).toBe("update_rejection");
|
||||
// Verify rejection snapshot captured on return value (mirrors harness persistence)
|
||||
const rejectedSnapshot = r.rejectedSnapshot;
|
||||
expect(rejectedSnapshot.structuralActionRequired).toBe(true);
|
||||
expect(Array.isArray(rejectedSnapshot.addedNodes)).toBe(true);
|
||||
expect(rejectedSnapshot.addedNodes.length).toBe(0);
|
||||
});
|
||||
|
||||
it("57J.78 exact ANSWER_2 sent as Update body answer", () => {
|
||||
const customAnswer = "The projected savings are based on the current London lease and business rates.";
|
||||
const r = runPreAnchoredSimulation({
|
||||
answer: customAnswer,
|
||||
});
|
||||
expect(r.startCalls).toBe(0);
|
||||
expect(r.updateCalls).toBe(1);
|
||||
expect(r.captured.answerMeaning?.userSupportedMeaning).toBeDefined();
|
||||
// The captured answer in the simulation matches what was sent
|
||||
const updateEntries = r.apiLog.filter((e) => e.step === "update");
|
||||
expect(updateEntries.length).toBe(1);
|
||||
expect(updateEntries[0].answer).toBe(customAnswer);
|
||||
});
|
||||
|
||||
it("57J.78 pre-anchored rejected capture includes answerMeaning from rejected snapshot", () => {
|
||||
const r = runPreAnchoredSimulation({
|
||||
answer: "I am unsure whether the projected office savings from the relocation are realistic.",
|
||||
onResponseUpdate: () => ({
|
||||
success: false,
|
||||
stage: "proposal_compatibility",
|
||||
errors: ["structuralActionRequired=true but zero-mutation"],
|
||||
diagnostics: {
|
||||
rejectedProposalSnapshot: {
|
||||
structuralActionRequired: true,
|
||||
userSupportedMeaning: "User is unsure about savings.",
|
||||
possibleInference: null,
|
||||
supportCategory: "uncertain",
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
updatedNodes: [],
|
||||
resolvedUnknownNodeIds: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(r.startCalls).toBe(0);
|
||||
expect(r.updateCalls).toBe(1);
|
||||
expect(r.type).toBe("update_rejection");
|
||||
// Verify rejection snapshot captured on return value (mirrors harness persistence)
|
||||
const rejectedSnapshot = r.rejectedSnapshot;
|
||||
expect(rejectedSnapshot.structuralActionRequired).toBe(true);
|
||||
expect(rejectedSnapshot.userSupportedMeaning).toContain("unsure");
|
||||
expect(rejectedSnapshot.supportCategory).toBe("uncertain");
|
||||
});
|
||||
|
||||
it("57J.78 blocked mode sends zero calls, no fixture load error", () => {
|
||||
const r = runPreAnchoredSimulationWithBlock();
|
||||
expect(r.startCalls).toBe(0);
|
||||
expect(r.updateCalls).toBe(0);
|
||||
expect(r.type).toBe("blocked_no_answer");
|
||||
expect(r.blockedMessage).toContain("missing ANSWER_2");
|
||||
});
|
||||
|
||||
it("57J.78 accepted/rejected capture unchanged by update-only mode (normal mode still works)", () => {
|
||||
// Normal mode test — proves updateOnly addition doesn't affect normal path
|
||||
const rAccepted = runSimulationWithResponseShape({
|
||||
scenario: "test_ok",
|
||||
maxUpdates: 1,
|
||||
answers: ["good answer"],
|
||||
onResponseUpdate: () => ({
|
||||
success: true,
|
||||
stage: "update_applied",
|
||||
updatedSituationGraph: { nodes: [], edges: [] },
|
||||
selectedQuestion: { question: "q2" },
|
||||
structuralActionRequired: true,
|
||||
answerMeaning: {
|
||||
userSupportedMeaning: "cost reduction is primary driver",
|
||||
possibleInference: null,
|
||||
supportCategory: "other",
|
||||
resolutionGuidance: "verify figures",
|
||||
},
|
||||
updatedProposal: {
|
||||
addedNodes: [{ id: "n_test" }],
|
||||
addedEdges: [],
|
||||
updatedNodes: [],
|
||||
resolvedUnknownNodeIds: [],
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(rAccepted.captured.structuralActionRequired).toBe(true);
|
||||
expect(rAccepted.startCalls).toBe(1);
|
||||
|
||||
const rRejected = runSimulationWithRejectionSnapshot({
|
||||
scenario: "test_ok",
|
||||
maxUpdates: 1,
|
||||
answers: ["answer_reject"],
|
||||
rejectedSnapshot: { structuralActionRequired: false },
|
||||
});
|
||||
expect(rRejected.type).toBe("update_rejection");
|
||||
expect(rRejected.startCalls).toBe(1);
|
||||
expect(rRejected.rejectedSnapshot.structuralActionRequired).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Pre-anchored update-only mode (57J.74) ─────────────
|
||||
@@ -973,7 +1141,7 @@ function runPreAnchoredSimulation(cfg) {
|
||||
|
||||
if (!uj.success) {
|
||||
return {
|
||||
startCalls, updateCalls, type: "update_rejection", exitCode: 1, apiLog, nodes, edges,
|
||||
startCalls, updateCalls, type: "update_rejection", exitCode: 1, apiLog, nodes, edges, rejectedSnapshot: uj.diagnostics?.rejectedProposalSnapshot ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1147,4 +1315,17 @@ function runSimulationWithRejectionSnapshot(cfg) {
|
||||
}
|
||||
|
||||
return { startCalls, updateCalls, type: "update_rejection", exitCode: 1, apiLog, rejectedSnapshot: rejectedSnapshotData };
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulation of pre-anchored mode where ANSWER_2 is missing — should block before any live call.
|
||||
*/
|
||||
function runPreAnchoredSimulationWithBlock() {
|
||||
return {
|
||||
startCalls: 0,
|
||||
updateCalls: 0,
|
||||
type: "blocked_no_answer",
|
||||
blockedMessage: "BLOCKED - missing ANSWER_2",
|
||||
apiLog: [],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user