/** * Regression: focused deconstruct targetNodeId identity boundary. * * Verifies the deterministic enforcement invariant: * request.targetNodeId (original graph node ID) must be the final * API response targetNodeId regardless of what the model returns. */ import { afterEach, beforeEach, describe, it, expect, vi } from "vitest"; import { focusedDeconstructJsonSchema, validateFocusedDeconstructSchema } from "@/lib/graph/focused-investigation"; // ── helpers ────────────────────────────────────────────────────────────── function makeMockProvider(inventedTargetNodeId) { return { generateReconstruction: vi.fn().mockResolvedValue({ response: { targetNodeId: inventedTargetNodeId, observations: ["doc is minimal", "processes in founder's head"], uncertainties: ["whether formal docs can capture tacit knowledge"], assumptions: ["documentation is primary mechanism for knowledge transfer"], relationships: [ { from: "founder", to: "processes", type: "holds" }, { from: "ops-context", to: "docs-infra", type: "depends_on" }, ], possibleFollowUpQuestions: [ "What processes does the founder hold tacitly?", "How is knowledge transferred when founder is unavailable?", ], }, providerApiPath: "/api/chat", providerExecution: { chatRequestAttempted: true }, }), }; } // ── Boundary test ──────────────────────────────────────────────────────── describe("focused-deconstruct targetNodeId identity boundary", () => { beforeEach(() => { vi.resetModules(); }); afterEach(() => { vi.doUnmock("@/lib/llm/provider"); }); it("enforces the canonical focused relationship structure", () => { const base = { targetNodeId: "node-id", observations: [], uncertainties: [], assumptions: [], possibleFollowUpQuestions: [], }; expect(validateFocusedDeconstructSchema({ ...base, relationships: [] })).toEqual([]); expect(validateFocusedDeconstructSchema({ ...base, relationships: [{ from: "supplier changed", to: "defect rate increased", type: "associated with" }], })).toEqual([]); const invalidRelationships = [ "not an array", [{}], [{ from: "a", type: "links" }], [{ from: "a", to: "b" }], [{ from: "", to: "b", type: "links" }], [{ from: "a", to: "", type: "links" }], [{ from: "a", to: "b", type: "" }], [{ from: "a", to: "b", type: "links", rationale: "extra" }], [{ from: "a", to: "b", type: "links", extra: "extra" }], ]; invalidRelationships.forEach((relationships) => { expect(validateFocusedDeconstructSchema({ ...base, relationships }).length).toBeGreaterThan(0); }); }); it("request targetNodeId overrides model-invented targetNodeId", async () => { const requestTargetNodeId = "nk04xvk"; // original graph node ID const inventedModelId = "invented-model-id"; vi.doMock("@/lib/llm/provider", () => ({ getProvider: () => makeMockProvider(inventedModelId), getProviderModelName: () => "configured-model", })); const { POST } = await import("../app/api/focused-investigation/deconstruct/route.js"); const requestBody = { targetNodeId: requestTargetNodeId, targetLabel: "Whether unclear or uneven distribution of responsibilities is preventing autonomy in key areas.", targetDescription: "Original open question node label", centralStatement: "Current operational context and documentation state", question: "What was the comparable state before whether unclear or uneven distribution of responsibilities is preventing autonomy in key areas?", answer: "Documentation is minimal, most processes are in the head of the founder.", }; const request = new Request("http://localhost/api/focused-investigation/deconstruct", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestBody), }); const response = await POST(request); expect(response.status).toBe(200); const json = await response.json(); expect(json.success).toBe(true); // THE INVARIANT: final API targetNodeId = request targetNodeId (authoritative) expect(json.targetNodeId).toBe(requestTargetNodeId); expect(json.targetNodeId).not.toBe(inventedModelId); }); it("supplies the focused-deconstruction schema through the provider seam", async () => { const generateReconstruction = vi.fn().mockResolvedValue({ response: { targetNodeId: "model-id", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [], }, providerApiPath: "/api/chat", providerExecution: { chatRequestAttempted: true }, }); vi.doMock("@/lib/llm/provider", () => ({ getProvider: () => ({ generateReconstruction }), getProviderModelName: () => "gpt-5.6-terra", })); const { POST } = await import("../app/api/focused-investigation/deconstruct/route.js"); const response = await POST(new Request("http://localhost/api/focused-investigation/deconstruct", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ targetNodeId: "node-id", targetLabel: "label", targetDescription: "description", centralStatement: "central statement", question: "question?", answer: "answer.", }), })); expect(response.status).toBe(200); expect(generateReconstruction).toHaveBeenCalledWith( expect.any(String), "gpt-5.6-terra", focusedDeconstructJsonSchema, ); }); it("semantic fields pass through unchanged from model", async () => { const mockObs = ["doc is minimal", "processes in founder's head"]; const mockUnc = ["whether formal docs can capture tacit knowledge"]; const mockAssm = ["documentation is primary mechanism for knowledge transfer"]; const mockRel = [ { from: "founder", to: "processes", type: "holds" }, { from: "ops-context", to: "docs-infra", type: "depends_on" }, ]; const mockFuq = [ "What processes does the founder hold tacitly?", "How is knowledge transferred when founder is unavailable?", ]; vi.doMock("@/lib/llm/provider", () => ({ getProvider: () => ({ generateReconstruction: vi.fn().mockResolvedValue({ response: { targetNodeId: "some-invented-id", observations: mockObs, uncertainties: mockUnc, assumptions: mockAssm, relationships: mockRel, possibleFollowUpQuestions: mockFuq, }, providerApiPath: "/api/chat", providerExecution: { chatRequestAttempted: true }, }), }), getProviderModelName: () => "configured-model", })); const { POST } = await import("../app/api/focused-investigation/deconstruct/route.js"); const request = new Request("http://localhost/api/focused-investigation/deconstruct", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ targetNodeId: "nk04xvk", targetLabel: "label", targetDescription: "desc", centralStatement: "central", question: "question?", answer: "answer.", }), }); const response = await POST(request); const json = await response.json(); // Semantic fields unchanged expect(json.observations).toEqual(mockObs); expect(json.uncertainties).toEqual(mockUnc); expect(json.assumptions).toEqual(mockAssm); expect(json.relationships).toEqual(mockRel); expect(json.possibleFollowUpQuestions).toEqual(mockFuq); }); it("contribution append preserves authoritative targetNodeId", () => { // Simulates reasoning-workspace.jsx:1178-1189 after the fix: // onFocusedContribution calls with body.targetNodeId (the original graph node) const requestTargetNodeId = "nk04xvk"; const inventedModelId = "invented-model-id"; const contribution = { targetNodeId: requestTargetNodeId, targetLabel: "label", targetDescription: "desc", question: "question?", answer: "answer.", observations: ["obs1"], uncertainties: ["unc1"], assumptions: ["asm1"], relationships: [{ from: "a", to: "b", type: "depends_on" }], possibleFollowUpQuestions: ["fuq1"], }; expect(contribution.targetNodeId).toBe(requestTargetNodeId); expect(contribution.targetNodeId).not.toBe(inventedModelId); // Simulates ThreadContributionsBadge filter: contributions.filter(c => c.targetNodeId === nodeId) const threadContribs = [contribution].filter((c) => c.targetNodeId === requestTargetNodeId); expect(threadContribs.length).toBe(1); }); it("full identity path: request → response → contribution", async () => { const originalNodeId = "nk04xvk"; const modelInventedId = "investigation_node_responsibility_distribution_autonomy"; vi.doMock("@/lib/llm/provider", () => ({ getProvider: () => ({ generateReconstruction: vi.fn().mockResolvedValue({ response: { targetNodeId: modelInventedId, observations: ["Documentation is minimal."], uncertainties: [], assumptions: [ "That formal documentation is the primary mechanism for capturing or transferring the founder's tacit knowledge of processes.", ], relationships: [ { from: "Founder", to: "Processes", type: "holds" }, { from: "Operational Context", to: "Documentation Infrastructure", type: "affects" }, ], possibleFollowUpQuestions: [], }, providerApiPath: "/api/chat", providerExecution: { chatRequestAttempted: true }, }), }), getProviderModelName: () => "configured-model", })); const { POST } = await import("../app/api/focused-investigation/deconstruct/route.js"); const request = new Request("http://localhost/api/focused-investigation/deconstruct", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ targetNodeId: originalNodeId, targetLabel: "Whether unclear or uneven distribution of responsibilities is preventing autonomy in key areas.", targetDescription: "Original open question node description", centralStatement: "Central statement", question: "What was the comparable state before whether unclear or uneven distribution of responsibilities is preventing autonomy in key areas?", answer: "Documentation is minimal, most processes are in the head of the founder.", }), }); const response = await POST(request); expect(response.status).toBe(200); const json = await response.json(); // Identity path verification: // 1. Request targetNodeId expect(json.targetNodeId).toBe(originalNodeId); // 2. Response carries authoritative identity (not model-invented) expect(json.targetNodeId).not.toBe(modelInventedId); // 3. Semantic fields from the model remain unchanged expect(json.observations).toEqual(["Documentation is minimal."]); expect(json.uncertainties).toEqual([]); expect(json.assumptions).toEqual([ "That formal documentation is the primary mechanism for capturing or transferring the founder's tacit knowledge of processes.", ]); expect(json.relationships).toEqual([ { from: "Founder", to: "Processes", type: "holds" }, { from: "Operational Context", to: "Documentation Infrastructure", type: "affects" }, ]); expect(json.possibleFollowUpQuestions).toEqual([]); // 4. Stored contribution would use originalNodeId (not modelInventedId) const stored = { ...json }; expect(stored.targetNodeId).toBe(originalNodeId); }); it("provider envelope fields do not leak into API response", async () => { vi.doMock("@/lib/llm/provider", () => ({ getProvider: () => ({ generateReconstruction: vi.fn().mockResolvedValue({ response: { targetNodeId: "nk04xvk", observations: ["obs"], uncertainties: ["unc"], assumptions: ["asm"], relationships: [], possibleFollowUpQuestions: ["fuq"], }, providerApiPath: "/api/chat", providerExecution: { chatRequestAttempted: true, chatRequestSucceeded: true }, }), }), getProviderModelName: () => "configured-model", })); const { POST } = await import("../app/api/focused-investigation/deconstruct/route.js"); const response = await POST( new Request("http://localhost/api/focused-investigation/deconstruct", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ targetNodeId: "nk04xvk", targetLabel: "label", targetDescription: "desc", centralStatement: "central", question: "q?", answer: "a.", }), }), ); expect(response.status).toBe(200); const json = await response.json(); // Semantic fields present expect(json.success).toBe(true); expect(json.targetNodeId).toBe("nk04xvk"); expect(json.observations).toEqual(["obs"]); expect(json.possibleFollowUpQuestions).toEqual(["fuq"]); // Provider envelope fields must NOT appear in the response expect(json.providerApiPath).toBeUndefined(); expect(json.providerExecution).toBeUndefined(); }); it("preserves the 500 provider-failure contract while logging structural diagnostics", async () => { const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); vi.doMock("@/lib/llm/provider", () => ({ getProvider: () => ({ generateReconstruction: vi.fn().mockRejectedValue(Object.assign(new Error("provider failed"), { providerApiPath: "/v1/responses", statusCode: 400, })), }), getProviderModelName: () => "gpt-5.6-terra", })); const { POST } = await import("../app/api/focused-investigation/deconstruct/route.js"); try { const response = await POST(new Request("http://localhost/api/focused-investigation/deconstruct", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ targetNodeId: "node-id", targetLabel: "label", targetDescription: "description", centralStatement: "central", question: "question?", answer: "answer.", }), })); expect(response.status).toBe(500); await expect(response.json()).resolves.toEqual({ error: "provider failed" }); expect(errorSpy).toHaveBeenCalledWith( "[api/focused-investigation/deconstruct] provider failure", expect.objectContaining({ targetNodeId: "node-id", providerApiPath: "/v1/responses" }), ); } finally { errorSpy.mockRestore(); } }); it("preserves the 502 validation-failure contract with diagnostics", async () => { vi.doMock("@/lib/llm/provider", () => ({ getProvider: () => ({ generateReconstruction: vi.fn().mockResolvedValue({ response: {}, providerApiPath: "/v1/responses", }), }), getProviderModelName: () => "gpt-5.6-terra", })); const { POST } = await import("../app/api/focused-investigation/deconstruct/route.js"); const response = await POST(new Request("http://localhost/api/focused-investigation/deconstruct", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ targetNodeId: "node-id", targetLabel: "label", targetDescription: "description", centralStatement: "central", question: "question?", answer: "answer.", }), })); expect(response.status).toBe(502); await expect(response.json()).resolves.toMatchObject({ success: false, error: "Focused deconstruction result did not match expected schema", targetNodeId: "node-id", }); }); });