fix(confidence-engine): persist done-for-now episode closure
This commit is contained in:
@@ -129,9 +129,16 @@ async function handleEpisodeMode(situationGraph, body) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolvedNodeIds = new Set(application.updatedSituationGraph.resolvedNodeIds ?? []);
|
||||||
|
resolvedNodeIds.add(body.targetNodeId);
|
||||||
|
const updatedSituationGraph = {
|
||||||
|
...application.updatedSituationGraph,
|
||||||
|
resolvedNodeIds: [...resolvedNodeIds],
|
||||||
|
};
|
||||||
|
|
||||||
return Response.json({
|
return Response.json({
|
||||||
success: true,
|
success: true,
|
||||||
updatedSituationGraph: application.updatedSituationGraph,
|
updatedSituationGraph,
|
||||||
proposal: reasoning.proposal,
|
proposal: reasoning.proposal,
|
||||||
}, { status: 200 });
|
}, { status: 200 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ If YES, the next live experiment is one timed/costed OpenAI UI investigation mea
|
|||||||
- Terra synthesis now supplies its own output schema and unwraps `providerResult.response` before validation; deterministic synthesis coverage is 62/62 PASS and the saved isolated Terra synthesis POST returned HTTP 200.
|
- Terra synthesis now supplies its own output schema and unwraps `providerResult.response` before validation; deterministic synthesis coverage is 62/62 PASS and the saved isolated Terra synthesis POST returned HTTP 200.
|
||||||
- Real Terra completed-episode reconsideration can legitimately return no additional meaningful graph change. Completed episodes now tolerate only that exact compatibility outcome; ordinary no-op updates and invalid completed-episode proposals remain rejected, without fake graph mutation or new next-question steering.
|
- Real Terra completed-episode reconsideration can legitimately return no additional meaningful graph change. Completed episodes now tolerate only that exact compatibility outcome; ordinary no-op updates and invalid completed-episode proposals remain rejected, without fake graph mutation or new next-question steering.
|
||||||
- The full apply-proposal owner suite remains known-red in independent pre-existing 60B.43 tests, so the changed Done-for-now boundary was verified through exact isolated owner tests. Zero live calls occurred during closeout. Next boundary: reuse the existing investigation and click Done for now once, observing cases/update and subsequent synthesis.
|
- The full apply-proposal owner suite remains known-red in independent pre-existing 60B.43 tests, so the changed Done-for-now boundary was verified through exact isolated owner tests. Zero live calls occurred during closeout. Next boundary: reuse the existing investigation and click Done for now once, observing cases/update and subsequent synthesis.
|
||||||
|
- Live UI proved Done for now briefly clarified the question, then server graph replacement reopened it: the client sends `preDoneGraph`, and the server previously had no deterministic closure owner. Episode-mode update now adds the selected `targetNodeId` to `resolvedNodeIds` only after successful semantic application; semantic no-ops and meaningful mutations remain valid, while failed episodes do not resolve the target and ordinary updates are unchanged. Zero live calls occurred during implementation. Next boundary: one live Done-for-now check on the existing investigation.
|
||||||
|
|
||||||
## Repository checkpoint
|
## Repository checkpoint
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,45 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { makeGraph, makeNode } from "@/lib/graph/schema.js";
|
||||||
|
|
||||||
const mockUpdateCase = vi.fn();
|
const mockUpdateCase = vi.fn();
|
||||||
|
const mockReconsiderCompletedEpisode = vi.fn();
|
||||||
|
const mockApplyValidatedProposal = vi.fn();
|
||||||
|
const mockPrepareCompletedEpisode = vi.fn();
|
||||||
|
|
||||||
vi.mock("@/lib/graph/orchestrator.js", () => ({
|
vi.mock("@/lib/graph/orchestrator.js", () => ({
|
||||||
updateCase: (...args) => mockUpdateCase(...args),
|
updateCase: (...args) => mockUpdateCase(...args),
|
||||||
|
reconsiderCompletedEpisode: (...args) => mockReconsiderCompletedEpisode(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/graph/apply-proposal.js", () => ({
|
||||||
|
applyValidatedProposal: (...args) => mockApplyValidatedProposal(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/graph/episode-preparation.js", () => ({
|
||||||
|
prepareCompletedEpisode: (...args) => mockPrepareCompletedEpisode(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function makeEpisodeGraph() {
|
||||||
|
return makeGraph({
|
||||||
|
centralStatement: "Episode scenario",
|
||||||
|
currentSummary: "Episode graph",
|
||||||
|
nodes: [makeNode({ id: "target", label: "Target question", kind: "unknown", status: "unknown" })],
|
||||||
|
edges: [],
|
||||||
|
activeUnknownNodeId: "target",
|
||||||
|
resolvedNodeIds: ["already-resolved"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockSuccessfulEpisodeApplication(graph, overrides = {}) {
|
||||||
|
mockPrepareCompletedEpisode.mockReturnValue({ turns: [{ question: "Q?", answer: "A." }] });
|
||||||
|
mockReconsiderCompletedEpisode.mockResolvedValue({ success: true, proposal: {} });
|
||||||
|
mockApplyValidatedProposal.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
updatedSituationGraph: graph,
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function makeSuccessResult() {
|
function makeSuccessResult() {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -76,6 +110,67 @@ describe("app/api/cases/update route", () => {
|
|||||||
expect(mockUpdateCase.mock.calls[0][0]).not.toHaveProperty("modelName");
|
expect(mockUpdateCase.mock.calls[0][0]).not.toHaveProperty("modelName");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("authoritatively resolves the selected target after a successful no-op episode", async () => {
|
||||||
|
const graph = makeEpisodeGraph();
|
||||||
|
mockSuccessfulEpisodeApplication(graph);
|
||||||
|
const { POST } = await import("@/app/api/cases/update/route.js");
|
||||||
|
|
||||||
|
const response = await POST(new Request("http://localhost/api/cases/update", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
episodeMode: true,
|
||||||
|
situationGraph: graph,
|
||||||
|
targetNodeId: "target",
|
||||||
|
contributions: [{ targetNodeId: "target" }],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const body = await response.json();
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(body.updatedSituationGraph.resolvedNodeIds).toEqual(
|
||||||
|
expect.arrayContaining(["already-resolved", "target"]),
|
||||||
|
);
|
||||||
|
expect(body.updatedSituationGraph.nodes).toEqual(graph.nodes);
|
||||||
|
expect(body.updatedSituationGraph.edges).toEqual(graph.edges);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves meaningful episode mutations while authoritatively resolving the target", async () => {
|
||||||
|
const graph = makeEpisodeGraph();
|
||||||
|
const mutatedGraph = {
|
||||||
|
...graph,
|
||||||
|
nodes: [...graph.nodes, makeNode({ id: "meaningful", label: "Meaningful change", kind: "observation", status: "known" })],
|
||||||
|
};
|
||||||
|
mockSuccessfulEpisodeApplication(mutatedGraph);
|
||||||
|
const { POST } = await import("@/app/api/cases/update/route.js");
|
||||||
|
|
||||||
|
const response = await POST(new Request("http://localhost/api/cases/update", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ episodeMode: true, situationGraph: graph, targetNodeId: "target", contributions: [{ targetNodeId: "target" }] }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.updatedSituationGraph.nodes).toEqual(mutatedGraph.nodes);
|
||||||
|
expect(body.updatedSituationGraph.resolvedNodeIds).toContain("target");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not return an authoritative closure when completed-episode reconsideration fails", async () => {
|
||||||
|
const graph = makeEpisodeGraph();
|
||||||
|
mockPrepareCompletedEpisode.mockReturnValue({ turns: [{ question: "Q?", answer: "A." }] });
|
||||||
|
mockReconsiderCompletedEpisode.mockResolvedValue({ success: false, stage: "proposal_compatibility", error: "invalid" });
|
||||||
|
const { POST } = await import("@/app/api/cases/update/route.js");
|
||||||
|
|
||||||
|
const response = await POST(new Request("http://localhost/api/cases/update", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ episodeMode: true, situationGraph: graph, targetNodeId: "target", contributions: [{ targetNodeId: "target" }] }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(response.status).toBe(422);
|
||||||
|
await expect(response.json()).resolves.not.toHaveProperty("updatedSituationGraph");
|
||||||
|
});
|
||||||
|
|
||||||
it("invalid JSON returns 400", async () => {
|
it("invalid JSON returns 400", async () => {
|
||||||
const { POST } = await import("@/app/api/cases/update/route.js");
|
const { POST } = await import("@/app/api/cases/update/route.js");
|
||||||
const request = {
|
const request = {
|
||||||
|
|||||||
Reference in New Issue
Block a user