Feature/product platform foundation v0.62 #1
@@ -88,6 +88,14 @@ async function main() {
|
||||
console.log(`\ndiagnostics.rejectedProposalSnapshot: ${JSON.stringify(updateResult.json.diagnostics.rejectedProposalSnapshot, null, 2)}`);
|
||||
}
|
||||
|
||||
// ── Capture structuralActionRequired from rejected snapshot if present ──
|
||||
const rejectedSnapshot = updateResult.json?.diagnostics?.rejectedProposalSnapshot ?? null;
|
||||
if (rejectedSnapshot && "structuralActionRequired" in rejectedSnapshot) {
|
||||
console.log(`structuralActionRequired (from rejected proposal snapshot): ${JSON.stringify(rejectedSnapshot.structuralActionRequired)}`);
|
||||
} else {
|
||||
console.log(`structuralActionRequired: UNAVAILABLE`);
|
||||
}
|
||||
|
||||
reportCallAccounting();
|
||||
console.log("\n*** UPDATE REJECTED — STOPPING (no retry). ***");
|
||||
process.exitCode = 1;
|
||||
@@ -120,6 +128,14 @@ async function main() {
|
||||
console.log(`addedEdges: ${JSON.stringify(proposal.addedEdges ?? [])}`);
|
||||
}
|
||||
|
||||
// ── Capture structuralActionRequired from accepted update ─
|
||||
const sar = updateResult.json.structuralActionRequired;
|
||||
if (sar === undefined || sar === null) {
|
||||
console.log(`structuralActionRequired: null`);
|
||||
} else {
|
||||
console.log(`structuralActionRequired: ${JSON.stringify(sar)}`);
|
||||
}
|
||||
|
||||
// ── Capture selectedQuestion node reference ────────────
|
||||
const sq = updateResult.json.selectedQuestion ?? null;
|
||||
if (sq && typeof sq === "object") {
|
||||
|
||||
@@ -376,6 +376,199 @@ describe("reproduce-multi-turn-investigation harness: one-shot semantics", () =>
|
||||
expect(r3.updateCalls).toBe(0);
|
||||
expect(r3.type).toBe("start_failure");
|
||||
});
|
||||
|
||||
// ── 57J.72: structuralActionRequired capture tests ───────────────────────
|
||||
|
||||
it("accepted update with structuralActionRequired=true reports true", () => {
|
||||
const r = runSimulationWithResponseShape({
|
||||
scenario: "test_ok",
|
||||
maxUpdates: 1,
|
||||
answers: ["good answer"],
|
||||
onResponseUpdate: () => ({
|
||||
success: true,
|
||||
stage: "update_applied",
|
||||
updatedSituationGraph: { nodes: [], edges: [] },
|
||||
selectedQuestion: { question: "q2" },
|
||||
structuralActionRequired: true,
|
||||
updatedProposal: { addedNodes: [], addedEdges: [], updatedNodes: [], resolvedUnknownNodeIds: [] },
|
||||
}),
|
||||
});
|
||||
expect(r.startCalls).toBe(1);
|
||||
expect(r.updateCalls).toBe(1);
|
||||
expect(r.captured.structuralActionRequired).toBe(true);
|
||||
});
|
||||
|
||||
it("accepted update with structuralActionRequired=false reports false", () => {
|
||||
const r = runSimulationWithResponseShape({
|
||||
scenario: "test_ok",
|
||||
maxUpdates: 1,
|
||||
answers: ["good answer"],
|
||||
onResponseUpdate: () => ({
|
||||
success: true,
|
||||
stage: "update_applied",
|
||||
updatedSituationGraph: { nodes: [], edges: [] },
|
||||
selectedQuestion: { question: "q2" },
|
||||
structuralActionRequired: false,
|
||||
updatedProposal: { addedNodes: [], addedEdges: [], updatedNodes: [], resolvedUnknownNodeIds: [] },
|
||||
}),
|
||||
});
|
||||
expect(r.updateCalls).toBe(1);
|
||||
expect(r.captured.structuralActionRequired).toBe(false);
|
||||
});
|
||||
|
||||
it("accepted update with absent structuralActionRequired reports null (not inferred)", () => {
|
||||
const r = runSimulationWithResponseShape({
|
||||
scenario: "test_ok",
|
||||
maxUpdates: 1,
|
||||
answers: ["good answer"],
|
||||
onResponseUpdate: () => ({
|
||||
success: true,
|
||||
stage: "update_applied",
|
||||
updatedSituationGraph: { nodes: [], edges: [] },
|
||||
selectedQuestion: { question: "q2" },
|
||||
// structuralActionRequired intentionally absent from response
|
||||
updatedProposal: { addedNodes: [], addedEdges: [], updatedNodes: [], resolvedUnknownNodeIds: [] },
|
||||
}),
|
||||
});
|
||||
expect(r.updateCalls).toBe(1);
|
||||
expect(r.captured.structuralActionRequired).toBeNull();
|
||||
});
|
||||
|
||||
it("accepted update with explicit null structuralActionRequired reports null", () => {
|
||||
const r = runSimulationWithResponseShape({
|
||||
scenario: "test_ok",
|
||||
maxUpdates: 1,
|
||||
answers: ["good answer"],
|
||||
onResponseUpdate: () => ({
|
||||
success: true,
|
||||
stage: "update_applied",
|
||||
updatedSituationGraph: { nodes: [], edges: [] },
|
||||
selectedQuestion: { question: "q2" },
|
||||
structuralActionRequired: null,
|
||||
updatedProposal: { addedNodes: [], addedEdges: [], updatedNodes: [], resolvedUnknownNodeIds: [] },
|
||||
}),
|
||||
});
|
||||
expect(r.updateCalls).toBe(1);
|
||||
expect(r.captured.structuralActionRequired).toBeNull();
|
||||
});
|
||||
|
||||
it("rejected proposal snapshot with structuralActionRequired=true reports true", () => {
|
||||
const r = runSimulation({ scenario: "test_ok", maxUpdates: 1, answers: ["answer_reject"] });
|
||||
expect(r.type).toBe("update_rejection");
|
||||
// Extend rejected diagnostic capture via custom mock
|
||||
const r2 = runSimulationWithRejectionSnapshot({
|
||||
scenario: "test_ok",
|
||||
maxUpdates: 1,
|
||||
answers: ["answer_reject"],
|
||||
rejectedSnapshot: { structuralActionRequired: true },
|
||||
});
|
||||
expect(r2.startCalls).toBe(1);
|
||||
expect(r2.updateCalls).toBe(1);
|
||||
expect(r2.type).toBe("update_rejection");
|
||||
expect(r2.rejectedSnapshot.structuralActionRequired).toBe(true);
|
||||
});
|
||||
|
||||
it("rejected proposal snapshot with structuralActionRequired=false reports false", () => {
|
||||
const r = runSimulationWithRejectionSnapshot({
|
||||
scenario: "test_ok",
|
||||
maxUpdates: 1,
|
||||
answers: ["answer_reject"],
|
||||
rejectedSnapshot: { structuralActionRequired: false },
|
||||
});
|
||||
expect(r.type).toBe("update_rejection");
|
||||
expect(r.rejectedSnapshot.structuralActionRequired).toBe(false);
|
||||
});
|
||||
|
||||
it("rejected snapshot without structuralActionRequired reports unavailable", () => {
|
||||
const r = runSimulationWithRejectionSnapshot({
|
||||
scenario: "test_ok",
|
||||
maxUpdates: 1,
|
||||
answers: ["answer_reject"],
|
||||
rejectedSnapshot: { userSupportedMeaning: "some meaning" },
|
||||
});
|
||||
expect(r.type).toBe("update_rejection");
|
||||
// Field not in snapshot — harness would report UNAVAILABLE
|
||||
expect("structuralActionRequired" in r.rejectedSnapshot).toBe(false);
|
||||
});
|
||||
|
||||
it("existing answerMeaning capture remains unchanged", () => {
|
||||
const r = runSimulationWithResponseShape({
|
||||
scenario: "test_ok",
|
||||
maxUpdates: 1,
|
||||
answers: ["good answer"],
|
||||
onResponseUpdate: () => ({
|
||||
success: true,
|
||||
stage: "update_applied",
|
||||
updatedSituationGraph: { nodes: [], edges: [] },
|
||||
selectedQuestion: { question: "q2" },
|
||||
answerMeaning: {
|
||||
userSupportedMeaning: "cost reduction is primary driver",
|
||||
possibleInference: null,
|
||||
supportCategory: "other",
|
||||
resolutionGuidance: "verify figures",
|
||||
},
|
||||
updatedProposal: { addedNodes: [], addedEdges: [], updatedNodes: [], resolvedUnknownNodeIds: [] },
|
||||
}),
|
||||
});
|
||||
expect(r.captured.answerMeaning.userSupportedMeaning).toBe("cost reduction is primary driver");
|
||||
expect(r.captured.answerMeaning.possibleInference).toBeNull();
|
||||
expect(r.captured.answerMeaning.supportCategory).toBe("other");
|
||||
expect(r.captured.answerMeaning.resolutionGuidance).toBe("verify figures");
|
||||
});
|
||||
|
||||
it("existing mutation/persistent-graph capture remains unchanged", () => {
|
||||
const r = runSimulationWithResponseShape({
|
||||
scenario: "test_ok",
|
||||
maxUpdates: 1,
|
||||
answers: ["good answer"],
|
||||
onResponseUpdate: () => ({
|
||||
success: true,
|
||||
stage: "update_applied",
|
||||
updatedSituationGraph: {
|
||||
nodes: [{ id: "n_new", kind: "unknown", status: "unknown" }],
|
||||
edges: [{ fromNodeId: "n_new", toNodeId: "n_existing", relationship: "depends_on" }],
|
||||
},
|
||||
selectedQuestion: { question: "q2" },
|
||||
updatedProposal: {
|
||||
addedNodes: [{ id: "n_new" }],
|
||||
addedEdges: [],
|
||||
updatedNodes: [],
|
||||
resolvedUnknownNodeIds: [],
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(r.captured.proposal.addedNodes.length).toBe(1);
|
||||
expect(r.captured.graph.nodes.length).toBe(1);
|
||||
expect(r.captured.graph.edges.length).toBe(1);
|
||||
});
|
||||
|
||||
it("no extra HTTP calls are introduced by structuralActionRequired capture", () => {
|
||||
const r = runSimulation({ scenario: "test_ok", maxUpdates: 1, answers: ["good answer"] });
|
||||
expect(r.startCalls + r.updateCalls).toBe(r.apiLog.length);
|
||||
});
|
||||
|
||||
it("no-retry and exact call accounting preserved after structuralActionRequired capture addition", () => {
|
||||
const rReject = runSimulation({ scenario: "test_ok", maxUpdates: 2, answers: ["answer_reject"] });
|
||||
expect(rReject.updateCalls).toBe(1);
|
||||
expect(rReject.type).toBe("update_rejection");
|
||||
|
||||
const rSuccess = runSimulationWithResponseShape({
|
||||
scenario: "test_ok",
|
||||
maxUpdates: 2,
|
||||
answers: ["good answer 1", "good answer 2"],
|
||||
onResponseUpdate: () => ({
|
||||
success: true,
|
||||
stage: "update_applied",
|
||||
updatedSituationGraph: { nodes: [], edges: [] },
|
||||
selectedQuestion: { question: "q" },
|
||||
structuralActionRequired: true,
|
||||
updatedProposal: { addedNodes: [], addedEdges: [], updatedNodes: [], resolvedUnknownNodeIds: [] },
|
||||
}),
|
||||
});
|
||||
expect(rSuccess.updateCalls).toBe(2);
|
||||
expect(rSuccess.startCalls).toBe(1);
|
||||
expect(rSuccess.apiLog.length).toBe(3); // 1 start + 2 updates, no extras
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -442,7 +635,7 @@ function runSimulationWithResponseShape(cfg) {
|
||||
|
||||
const uj = upResp.json();
|
||||
if (!uj.success) {
|
||||
return { startCalls, updateCalls, type: "update_rejection", exitCode: 1, apiLog };
|
||||
return { startCalls, updateCalls, type: "update_rejection", exitCode: 1, apiLog, rejectedSnapshot: uj.diagnostics?.rejectedProposalSnapshot ?? null };
|
||||
}
|
||||
|
||||
graph = uj.updatedSituationGraph;
|
||||
@@ -455,6 +648,14 @@ function runSimulationWithResponseShape(cfg) {
|
||||
const proposal = uj.updatedProposal ?? uj.proposal ?? null;
|
||||
if (proposal) captured.proposal = { addedNodes: proposal.addedNodes ?? [], addedEdges: proposal.addedEdges ?? [], updatedNodes: proposal.updatedNodes ?? [], resolvedUnknownNodeIds: proposal.resolvedUnknownNodeIds ?? [] };
|
||||
|
||||
// Mirror the harness structuralActionRequired capture for accepted updates
|
||||
const sar = uj.structuralActionRequired;
|
||||
if (sar === undefined || sar === null) {
|
||||
captured.structuralActionRequired = null;
|
||||
} else {
|
||||
captured.structuralActionRequired = sar;
|
||||
}
|
||||
|
||||
const sq = uj.selectedQuestion ?? null;
|
||||
if (sq && typeof sq === "object") captured.selectedQuestion = { question: sq.question, nodeId: sq.nodeId };
|
||||
|
||||
@@ -462,4 +663,63 @@ function runSimulationWithResponseShape(cfg) {
|
||||
}
|
||||
|
||||
return { startCalls, updateCalls, type: "all_success", exitCode: 0, apiLog, captured };
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulation variant that lets us inject a custom rejectedProposalSnapshot on rejection.
|
||||
*/
|
||||
function runSimulationWithRejectionSnapshot(cfg) {
|
||||
let startCalls = 0;
|
||||
let updateCalls = 0;
|
||||
let apiLog = [];
|
||||
let rejectedSnapshotData = cfg.rejectedSnapshot ?? {};
|
||||
|
||||
const api = {
|
||||
post(path, body) {
|
||||
if (path === "/api/cases/start") {
|
||||
apiLog.push({ step: "start" });
|
||||
startCalls = 1;
|
||||
return {
|
||||
status: 200,
|
||||
json: () => ({ success: true, situationGraph: { nodes: [], edges: [] }, selectedQuestion: { question: "q" } }),
|
||||
};
|
||||
}
|
||||
if (path === "/api/cases/update") {
|
||||
apiLog.push({ step: "update", answer: body.answer });
|
||||
updateCalls++;
|
||||
return {
|
||||
status: 422,
|
||||
json: () => ({
|
||||
success: false,
|
||||
stage: "proposal_compatibility",
|
||||
errors: ["rejection"],
|
||||
diagnostics: { rejectedProposalSnapshot: rejectedSnapshotData },
|
||||
}),
|
||||
};
|
||||
}
|
||||
apiLog.push({ step: "unknown", path });
|
||||
return { status: 404, json: () => ({ error: "not found" }) };
|
||||
},
|
||||
};
|
||||
|
||||
const startResp = api.post("/api/cases/start", { scenario: cfg.scenario || "test" });
|
||||
const sj = startResp.json();
|
||||
if (!sj.success) {
|
||||
return { startCalls, updateCalls, type: "start_failure", exitCode: 1, apiLog };
|
||||
}
|
||||
|
||||
let graph = sj.situationGraph;
|
||||
let question = sj.selectedQuestion ? sj.selectedQuestion.question : null;
|
||||
|
||||
const answers = cfg.answers || [];
|
||||
const maxUpdates = typeof cfg.maxUpdates === "number" ? cfg.maxUpdates : 2;
|
||||
const limit = Math.min(maxUpdates, answers.length);
|
||||
|
||||
for (let i = 0; i < limit; i++) {
|
||||
const upResp = api.post("/api/cases/update", { situationGraph: graph, previousQuestion: question, answer: answers[i] });
|
||||
graph = upResp.json().updatedSituationGraph ?? graph;
|
||||
question = upResp.json()?.selectedQuestion ? upResp.json().selectedQuestion.question : null;
|
||||
}
|
||||
|
||||
return { startCalls, updateCalls, type: "update_rejection", exitCode: 1, apiLog, rejectedSnapshot: rejectedSnapshotData };
|
||||
}
|
||||
Reference in New Issue
Block a user