feat: apply validated graph update proposals

This commit is contained in:
2026-08-02 08:24:56 +01:00
parent f3cdfce0b0
commit cb77f955ed
4 changed files with 869 additions and 0 deletions
+120
View File
@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { validateGraphReferences } from "@/lib/graph/utils.js";
import { makeGraph, makeNode } from "@/lib/graph/schema.js";
const mockAnalyseScenario = vi.fn();
@@ -526,4 +527,123 @@ describe("lib/graph/orchestrator startCase", () => {
expect(result.nextQuestion).toBeUndefined();
expect(result.proposal.nextQuestion).toBeUndefined();
});
it("defaults to proposal-only mode", async () => {
const { updateCase } = await import("@/lib/graph/orchestrator.js");
const applyValidatedProposal = vi.fn();
const provider = {
generateReconstruction: vi.fn().mockResolvedValue(makeProposal()),
};
const result = await updateCase(makeUpdateRequest(), {
provider,
config: MOCK_CONFIG,
applyValidatedProposal,
});
expect(result.success).toBe(true);
expect(result.stage).toBe("proposal_ready");
expect(applyValidatedProposal).not.toHaveBeenCalled();
});
it("applies the proposal only when explicitly enabled", async () => {
const { updateCase } = await import("@/lib/graph/orchestrator.js");
const request = makeUpdateRequest({
situationGraph: makeGraph({
centralStatement:
"Complaint counts increased while production also increased.",
nodes: [
makeNode({
id: "n-rate",
label: "Complaint rate",
description: "Need complaint rate",
kind: "unknown",
status: "unknown",
confidence: "high",
affects: ["n-conclusion"],
}),
makeNode({
id: "n-other-unknown",
label: "Other unknown",
description: "Another unresolved unknown",
kind: "unknown",
status: "unknown",
confidence: "medium",
}),
makeNode({
id: "n-conclusion",
label: "Quality deterioration",
description: "Quality conclusion",
kind: "conclusion",
status: "supported",
confidence: "medium",
dependsOn: ["n-rate"],
}),
],
edges: [],
activeUnknownNodeId: "n-rate",
resolvedNodeIds: [],
currentSummary: "Initial summary",
}),
});
const provider = {
generateReconstruction: vi.fn().mockResolvedValue({
addedNodes: [],
updatedNodes: [
{
nodeId: "n-rate",
previousStatus: "unknown",
newStatus: "resolved",
previousValue: "2.0 complaints per 100 units",
newValue: "1.9 complaints per 100 units",
reason: "The answer provides the updated rate.",
},
{
nodeId: "n-conclusion",
previousStatus: "supported",
newStatus: "weakened",
previousValue: null,
newValue: null,
reason: "The updated rate weakens the conclusion.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: ["n-rate"],
affectedNodeIds: ["n-conclusion"],
}),
};
const result = await updateCase(request, {
provider,
config: MOCK_CONFIG,
applyProposal: true,
});
expect(result).toMatchObject({
success: true,
stage: "update_applied",
affectedNodeIds: expect.arrayContaining(["n-rate", "n-conclusion"]),
resolvedUnknownNodeIds: ["n-rate"],
previousActiveUnknownNodeId: "n-rate",
newActiveUnknownNodeId: "n-other-unknown",
});
expect(validateGraphReferences(result.updatedSituationGraph)).toEqual({
valid: true,
errors: [],
});
});
it("startCase behaviour remains unchanged", async () => {
mockAnalyseScenario.mockResolvedValue(makeAnalysisResult());
const { startCase } = await import("@/lib/graph/orchestrator.js");
const result = await startCase({ scenario: "Scenario text" });
expect(result.success).toBe(true);
expect(result.selectedQuestion).toEqual({
id: "q-1",
question: "What denominator is being used for the complaint rate?",
});
});
});