327 lines
12 KiB
JavaScript
327 lines
12 KiB
JavaScript
/**
|
|
* 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 } 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", rationale: "tacit" },
|
|
{ from: "ops-context", to: "docs-infra", type: "depends_on", rationale: "formal docs required" },
|
|
],
|
|
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("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),
|
|
}));
|
|
|
|
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 }),
|
|
}));
|
|
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),
|
|
process.env.OLLAMA_MODEL,
|
|
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", rationale: "tacit" },
|
|
{ from: "ops-context", to: "docs-infra", type: "depends_on", rationale: "formal docs required" },
|
|
];
|
|
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 },
|
|
}),
|
|
}),
|
|
}));
|
|
|
|
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 },
|
|
}),
|
|
}),
|
|
}));
|
|
|
|
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 },
|
|
}),
|
|
}),
|
|
}));
|
|
|
|
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();
|
|
});
|
|
});
|