feat(experiment): checkpoint focused investigation boundaries
This commit is contained in:
@@ -0,0 +1,89 @@
|
|||||||
|
import { getProvider } from "@/lib/llm/provider";
|
||||||
|
import { buildFocusedDeconstructPrompt, validateFocusedDeconstructSchema } from "@/lib/graph/focused-investigation";
|
||||||
|
|
||||||
|
export async function POST(request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
if (!body.targetNodeId || typeof body.targetNodeId !== "string") {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Request must include a 'targetNodeId' string field" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!body.targetLabel || typeof body.targetLabel !== "string") {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Request must include a 'targetLabel' string field" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!body.targetDescription || typeof body.targetDescription !== "string") {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Request must include a 'targetDescription' string field" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!body.centralStatement || typeof body.centralStatement !== "string") {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Request must include a 'centralStatement' string field" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!body.question || typeof body.question !== "string") {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Request must include a 'question' string field" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!body.answer || typeof body.answer !== "string") {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Request must include an 'answer' string field" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = buildFocusedDeconstructPrompt({
|
||||||
|
targetLabel: body.targetLabel,
|
||||||
|
targetDescription: body.targetDescription,
|
||||||
|
centralStatement: body.centralStatement,
|
||||||
|
question: body.question,
|
||||||
|
answer: body.answer,
|
||||||
|
});
|
||||||
|
|
||||||
|
const provider = getProvider();
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const raw = await provider.generateReconstruction(prompt, process.env.OLLAMA_MODEL);
|
||||||
|
const elapsedMs = Date.now() - startedAt;
|
||||||
|
|
||||||
|
// Validate schema (required fields present, no graph-mutation fields)
|
||||||
|
const validationErrors = validateFocusedDeconstructSchema(raw);
|
||||||
|
if (validationErrors.length > 0) {
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: "Focused deconstruction result did not match expected schema",
|
||||||
|
validationErrors,
|
||||||
|
targetNodeId: body.targetNodeId,
|
||||||
|
elapsedMs,
|
||||||
|
},
|
||||||
|
{ status: 502 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
targetNodeId: raw.targetNodeId,
|
||||||
|
observations: raw.observations,
|
||||||
|
uncertainties: raw.uncertainties,
|
||||||
|
assumptions: raw.assumptions,
|
||||||
|
relationships: raw.relationships,
|
||||||
|
possibleFollowUpQuestions: raw.possibleFollowUpQuestions,
|
||||||
|
elapsedMs,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return Response.json(
|
||||||
|
{ error: e.message || "Unknown server error" },
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { formulateQuestionForTarget } from "@/lib/graph/focused-investigation";
|
||||||
|
|
||||||
|
export async function POST(request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
if (!body.targetNodeId || typeof body.targetNodeId !== "string") {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Request must include a 'targetNodeId' string field" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.situationGraph || typeof body.situationGraph !== "object") {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Request must include a 'situationGraph' object field" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = formulateQuestionForTarget({
|
||||||
|
situationGraph: body.situationGraph,
|
||||||
|
targetNodeId: body.targetNodeId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, error: result.error },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
targetNodeId: result.targetNodeId,
|
||||||
|
question: result.question,
|
||||||
|
strategy: result.strategy,
|
||||||
|
reasoningPattern: result.reasoningPattern,
|
||||||
|
reasoningPatternReason: result.reasoningPatternReason,
|
||||||
|
reason: result.reason,
|
||||||
|
questionFamily: result.questionFamily,
|
||||||
|
selectedQuestionTemplate: result.selectedQuestionTemplate,
|
||||||
|
allowedQuestionFamilies: result.allowedQuestionFamilies,
|
||||||
|
rejectedQuestionFamilies: result.rejectedQuestionFamilies,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return Response.json(
|
||||||
|
{ error: e.message || "Unknown server error" },
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { formulateQuestion } from "@/lib/graph/question-formulator.js";
|
||||||
|
|
||||||
|
const FOCUSED_ANSWER_SCHEMA_FIELDS = [
|
||||||
|
"targetNodeId",
|
||||||
|
"observations",
|
||||||
|
"uncertainties",
|
||||||
|
"assumptions",
|
||||||
|
"relationships",
|
||||||
|
"possibleFollowUpQuestions",
|
||||||
|
];
|
||||||
|
|
||||||
|
const FORBIDDEN_GRAPH_MUTATION_FIELDS = [
|
||||||
|
"addedNodes",
|
||||||
|
"updatedNodes",
|
||||||
|
"removedNodes",
|
||||||
|
"addedEdges",
|
||||||
|
"removedEdges",
|
||||||
|
"resolvedNodeIds",
|
||||||
|
"activeUnknownNodeId",
|
||||||
|
"selectedQuestion",
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formulate a question for the explicitly user-selected unresolved node.
|
||||||
|
*
|
||||||
|
* Input: { situationGraph, targetNodeId }
|
||||||
|
* Validates that target exists, is an unknown, and is not resolved.
|
||||||
|
* Calls formulateQuestion with only factual explicit-user-targeting context.
|
||||||
|
* Does NOT invoke selectActiveUnknownCandidate, determineGraphBackedQuestion,
|
||||||
|
* global ranking, or global recommendation.
|
||||||
|
* Does NOT mutate activeUnknownNodeId, selectedQuestion, SituationGraph, or resolvedNodeIds.
|
||||||
|
*/
|
||||||
|
export function formulateQuestionForTarget({ situationGraph, targetNodeId }) {
|
||||||
|
const nodesById = new Map(situationGraph?.nodes?.map((n) => [n.id, n]) || []);
|
||||||
|
const targetNode = nodesById.get(targetNodeId);
|
||||||
|
|
||||||
|
if (!targetNode) {
|
||||||
|
return { success: false, error: `Target node ${targetNodeId} not found in graph.` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetNode.kind !== "unknown") {
|
||||||
|
return { success: false, error: `Target node ${targetNodeId} is not an unknown (kind=${targetNode.kind}).` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetNode.status === "resolved") {
|
||||||
|
return { success: false, error: `Target node ${targetNodeId} is already resolved.` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build explicit user-targeting context — only factual fields from the targeting request.
|
||||||
|
// No activeUnknownNodeId override, no selectedQuestion mutation, no global selection state.
|
||||||
|
const context = {
|
||||||
|
resolvedValues: [],
|
||||||
|
suppressDecisionSufficiencyConfirmation: false,
|
||||||
|
_explicitTargetNodeId: targetNodeId,
|
||||||
|
_explicitTargetLabel: targetNode.label,
|
||||||
|
_explicitTargetDescription: targetNode.description,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = formulateQuestion({ node: targetNode, graph: situationGraph, context });
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
targetNodeId,
|
||||||
|
question: result.question,
|
||||||
|
strategy: result.strategy ?? null,
|
||||||
|
reasoningPattern: result.reasoningPattern,
|
||||||
|
reasoningPatternReason: result.reasoningPatternReason,
|
||||||
|
reason: result.reason,
|
||||||
|
questionFamily: result.questionFamily,
|
||||||
|
selectedQuestionTemplate: result.selectedQuestionTemplate,
|
||||||
|
allowedQuestionFamilies: result.allowedQuestionFamilies,
|
||||||
|
rejectedQuestionFamilies: result.rejectedQuestionFamilies,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a focused answer deconstruction prompt for one explicitly user-chosen investigation.
|
||||||
|
*/
|
||||||
|
export function buildFocusedDeconstructPrompt({ targetLabel, targetDescription, centralStatement, question, answer }) {
|
||||||
|
return `You are performing focused answer deconstruction for one explicitly user-chosen investigation.
|
||||||
|
|
||||||
|
Return exactly one JSON object. Return JSON only.
|
||||||
|
|
||||||
|
This is NOT a graph update task.
|
||||||
|
Do NOT output graph mutations.
|
||||||
|
Do NOT output selection, ranking, ownership, recommendation, confidence, or next-best-question semantics.
|
||||||
|
Do NOT include any of these fields: addedNodes, updatedNodes, removedNodes, addedEdges, removedEdges, resolvedNodeIds, activeUnknownNodeId, selectedQuestion.
|
||||||
|
|
||||||
|
Required top-level fields:
|
||||||
|
- targetNodeId
|
||||||
|
- observations
|
||||||
|
- uncertainties
|
||||||
|
- assumptions
|
||||||
|
- relationships
|
||||||
|
- possibleFollowUpQuestions
|
||||||
|
|
||||||
|
Field rules:
|
||||||
|
- targetNodeId must be included as a string identifying this investigation node
|
||||||
|
- observations: only statements directly supported by the answer
|
||||||
|
- uncertainties: only things the answer explicitly leaves unknown or unclear
|
||||||
|
- assumptions: include only if the answer itself relies on an assumption
|
||||||
|
- relationships: only direct supported relationships among extracted items, each with { from, to, type, rationale }
|
||||||
|
- possibleFollowUpQuestions: unresolved questions genuinely exposed by this answer, unranked
|
||||||
|
|
||||||
|
Focused case context:
|
||||||
|
- target label: ${targetLabel}
|
||||||
|
- target description: ${targetDescription}
|
||||||
|
- central case statement: ${centralStatement}
|
||||||
|
|
||||||
|
Question:
|
||||||
|
${question}
|
||||||
|
|
||||||
|
Answer:
|
||||||
|
${answer}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that a focused-answer deconstruction result contains only the expected fields.
|
||||||
|
* Returns an array of errors (empty = valid).
|
||||||
|
*/
|
||||||
|
export function validateFocusedDeconstructSchema(result) {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
for (const field of FOCUSED_ANSWER_SCHEMA_FIELDS) {
|
||||||
|
if (!(field in result)) {
|
||||||
|
errors.push(`Missing required field: ${field}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const field of FORBIDDEN_GRAPH_MUTATION_FIELDS) {
|
||||||
|
if (field in result) {
|
||||||
|
errors.push(`Forbidden graph-mutation field present: ${field}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { FOCUSED_ANSWER_SCHEMA_FIELDS, FORBIDDEN_GRAPH_MUTATION_FIELDS };
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { makeNode, makeGraph } from "@/lib/graph/schema.js";
|
||||||
|
import { formulateQuestionForTarget, buildFocusedDeconstructPrompt, validateFocusedDeconstructSchema } from "@/lib/graph/focused-investigation.js";
|
||||||
|
|
||||||
|
// ── helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function makeTestGraph() {
|
||||||
|
return makeGraph({
|
||||||
|
centralStatement: "Should we launch the product now?",
|
||||||
|
currentSummary: "initial summary",
|
||||||
|
nodes: [
|
||||||
|
makeNode({ id: "a", label: "Market demand signal", description: "Evidence that customers want this.", kind: "unknown", status: "unknown" }),
|
||||||
|
makeNode({ id: "b", label: "Competitor activity", description: "What others are doing.", kind: "observation", status: "supported" }),
|
||||||
|
],
|
||||||
|
edges: [],
|
||||||
|
resolvedNodeIds: ["b"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Boundary 1: explicit formulation ─────────────────────────────────────
|
||||||
|
|
||||||
|
describe("formulateQuestionForTarget — explicit node selection", () => {
|
||||||
|
it("validates that the target node exists in the graph", () => {
|
||||||
|
const graph = makeTestGraph();
|
||||||
|
const result = formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "nonexistent" });
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toContain("not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates that the target node is an unknown", () => {
|
||||||
|
const graph = makeTestGraph();
|
||||||
|
const result = formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "b" });
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toContain("not an unknown");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates that the target node is not resolved", () => {
|
||||||
|
const graph = makeTestGraph();
|
||||||
|
// Create a resolved unknown node
|
||||||
|
graph.nodes.push(makeNode({ id: "c", label: "Done", description: "Completed item", kind: "unknown", status: "resolved" }));
|
||||||
|
const result = formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "c" });
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toContain("resolved");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the supplied targetNodeId in the formulation output", () => {
|
||||||
|
const graph = makeTestGraph();
|
||||||
|
const result = formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "a" });
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.targetNodeId).toBe("a");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not mutate activeUnknownNodeId on the graph", () => {
|
||||||
|
const graph = makeTestGraph();
|
||||||
|
const before = graph.activeUnknownNodeId;
|
||||||
|
formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "a" });
|
||||||
|
expect(graph.activeUnknownNodeId).toBe(before);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not mutate SituationGraph in any way", () => {
|
||||||
|
const graph = makeTestGraph();
|
||||||
|
const snapshot = JSON.stringify(graph);
|
||||||
|
formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "a" });
|
||||||
|
expect(JSON.stringify(graph)).toBe(snapshot);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not invoke global selection (selectActiveUnknownCandidate / determineGraphBackedQuestion)", () => {
|
||||||
|
const graph = makeTestGraph();
|
||||||
|
const result = formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "a" });
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.targetNodeId).toBe("a");
|
||||||
|
expect(result.question).toBeDefined();
|
||||||
|
expect(typeof result.question).toBe("string");
|
||||||
|
expect(result.question.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Boundary 2: focused answer deconstruction ────────────────────────────
|
||||||
|
|
||||||
|
describe("buildFocusedDeconstructPrompt", () => {
|
||||||
|
it("produces a prompt containing the target label", () => {
|
||||||
|
const prompt = buildFocusedDeconstructPrompt({
|
||||||
|
targetLabel: "Market demand signal",
|
||||||
|
targetDescription: "Evidence that customers want this.",
|
||||||
|
centralStatement: "Should we launch?",
|
||||||
|
question: "What evidence would clarify market demand?",
|
||||||
|
answer: "Some evidence suggests demand.",
|
||||||
|
});
|
||||||
|
expect(prompt).toContain("Market demand signal");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("produces a prompt containing the answer text", () => {
|
||||||
|
const prompt = buildFocusedDeconstructPrompt({
|
||||||
|
targetLabel: "X",
|
||||||
|
targetDescription: "Y",
|
||||||
|
centralStatement: "Z",
|
||||||
|
question: "Q?",
|
||||||
|
answer: "Some evidence suggests demand.",
|
||||||
|
});
|
||||||
|
expect(prompt).toContain("Some evidence suggests demand.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT output graph-mutation fields (only lists them as forbidden in the instructions)", () => {
|
||||||
|
const prompt = buildFocusedDeconstructPrompt({
|
||||||
|
targetLabel: "X",
|
||||||
|
targetDescription: "Y",
|
||||||
|
centralStatement: "Z",
|
||||||
|
question: "Q?",
|
||||||
|
answer: "A.",
|
||||||
|
});
|
||||||
|
// The prompt instructs the model NOT to output these fields.
|
||||||
|
// It mentions them only as forbidden outputs, not as required outputs.
|
||||||
|
expect(prompt).toContain("Do NOT include any of these fields");
|
||||||
|
expect(prompt).not.toContain("- addedNodes");
|
||||||
|
expect(prompt).not.toContain("- updatedNodes");
|
||||||
|
expect(prompt).not.toContain("- removedNodes");
|
||||||
|
expect(prompt).not.toContain("- resolvedNodeIds");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("validateFocusedDeconstructSchema", () => {
|
||||||
|
it("passes when all required fields are present", () => {
|
||||||
|
const result = { targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [] };
|
||||||
|
expect(validateFocusedDeconstructSchema(result)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails when a required field is missing", () => {
|
||||||
|
const result = { targetNodeId: "a", observations: [], uncertainties: [], assumptions: [] };
|
||||||
|
const errors = validateFocusedDeconstructSchema(result);
|
||||||
|
expect(errors.length).toBeGreaterThan(0);
|
||||||
|
expect(errors.some((e) => e.includes("relationships") || e.includes("possibleFollowUpQuestions"))).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects results that contain graph-mutation fields", () => {
|
||||||
|
const result = {
|
||||||
|
targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [],
|
||||||
|
addedNodes: [{ id: "x" }],
|
||||||
|
};
|
||||||
|
const errors = validateFocusedDeconstructSchema(result);
|
||||||
|
expect(errors.some((e) => e.includes("addedNodes"))).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects results that contain resolvedNodeIds", () => {
|
||||||
|
const result = {
|
||||||
|
targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [],
|
||||||
|
resolvedNodeIds: ["x"],
|
||||||
|
};
|
||||||
|
const errors = validateFocusedDeconstructSchema(result);
|
||||||
|
expect(errors.some((e) => e.includes("resolvedNodeIds"))).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects results that contain activeUnknownNodeId", () => {
|
||||||
|
const result = {
|
||||||
|
targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [],
|
||||||
|
activeUnknownNodeId: "x",
|
||||||
|
};
|
||||||
|
const errors = validateFocusedDeconstructSchema(result);
|
||||||
|
expect(errors.some((e) => e.includes("activeUnknownNodeId"))).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects results that contain selectedQuestion", () => {
|
||||||
|
const result = {
|
||||||
|
targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [],
|
||||||
|
selectedQuestion: "some question",
|
||||||
|
};
|
||||||
|
const errors = validateFocusedDeconstructSchema(result);
|
||||||
|
expect(errors.some((e) => e.includes("selectedQuestion"))).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("all required fields are observations, uncertainties, assumptions, relationships, possibleFollowUpQuestions", () => {
|
||||||
|
const fullResult = { targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [] };
|
||||||
|
const errors = validateFocusedDeconstructSchema(fullResult);
|
||||||
|
expect(errors.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("focused result contains no graph-mutation fields when valid", () => {
|
||||||
|
const fullResult = { targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [] };
|
||||||
|
const errors = validateFocusedDeconstructSchema(fullResult);
|
||||||
|
expect(errors.length).toBe(0);
|
||||||
|
// Confirm no forbidden keys are in the result
|
||||||
|
const forbidden = ["addedNodes", "updatedNodes", "removedNodes", "addedEdges", "removedEdges", "resolvedNodeIds", "activeUnknownNodeId", "selectedQuestion"];
|
||||||
|
for (const key of forbidden) {
|
||||||
|
expect(key in fullResult).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user