Browse Source
- Test buildEditSystemPrompt basic structure (all major sections present) - Test with workflow context including node listing and connection formatting - Test scoped selection with restSummary (node count, type breakdown, boundary connections) - Test empty restSummary does not show SELECTED SUBSET section - Test createChatTools returns 3 properly structured tools - Test execute behavior for answerQuestion, createWorkflow, and editWorkflow - 21 test cases covering prompt building and tool creation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>handoff-20260429-1057
1 changed files with 344 additions and 0 deletions
@ -0,0 +1,344 @@ |
|||
/** |
|||
* Tests for tools.ts |
|||
* |
|||
* Validates buildEditSystemPrompt (basic structure, with context, |
|||
* with/without selection scope) and createChatTools (structure, |
|||
* execute behavior for all 3 tools). |
|||
*/ |
|||
|
|||
import { describe, it, expect, vi } from "vitest"; |
|||
import type { WorkflowNode } from "@/types"; |
|||
import type { WorkflowEdge } from "@/types/workflow"; |
|||
|
|||
// Mock nodeDefaults (transitively imported via editOperations)
|
|||
vi.mock("@/store/utils/nodeDefaults", () => ({ |
|||
createDefaultNodeData: (type: string): Record<string, unknown> => { |
|||
switch (type) { |
|||
case "prompt": |
|||
return { prompt: "" }; |
|||
case "nanoBanana": |
|||
return { inputImages: [], status: "idle" }; |
|||
default: |
|||
return {}; |
|||
} |
|||
}, |
|||
defaultNodeDimensions: { |
|||
imageInput: { width: 300, height: 280 }, |
|||
annotation: { width: 300, height: 280 }, |
|||
prompt: { width: 320, height: 220 }, |
|||
promptConstructor: { width: 340, height: 280 }, |
|||
nanoBanana: { width: 300, height: 300 }, |
|||
generateVideo: { width: 300, height: 300 }, |
|||
llmGenerate: { width: 320, height: 360 }, |
|||
splitGrid: { width: 300, height: 320 }, |
|||
output: { width: 320, height: 320 }, |
|||
outputGallery: { width: 320, height: 360 }, |
|||
imageCompare: { width: 400, height: 360 }, |
|||
}, |
|||
})); |
|||
|
|||
import { buildEditSystemPrompt, createChatTools } from "./tools"; |
|||
import { buildWorkflowContext, WorkflowContext } from "./contextBuilder"; |
|||
import type { SubgraphResult } from "./subgraphExtractor"; |
|||
|
|||
// --- Test helpers ---
|
|||
|
|||
function createTestNode( |
|||
id: string, |
|||
type: string, |
|||
data: Record<string, unknown>, |
|||
position = { x: 100, y: 200 } |
|||
): WorkflowNode { |
|||
return { |
|||
id, |
|||
type: type as WorkflowNode["type"], |
|||
position, |
|||
data: data as WorkflowNode["data"], |
|||
}; |
|||
} |
|||
|
|||
function createTestEdge( |
|||
id: string, |
|||
source: string, |
|||
target: string, |
|||
sourceHandle = "image", |
|||
targetHandle = "image" |
|||
): WorkflowEdge { |
|||
return { id, source, target, sourceHandle, targetHandle }; |
|||
} |
|||
|
|||
function buildSimpleContext(): WorkflowContext { |
|||
const nodes: WorkflowNode[] = [ |
|||
createTestNode("prompt-1", "prompt", { prompt: "sunset" }), |
|||
createTestNode("gen-1", "nanoBanana", { |
|||
inputImages: [], |
|||
outputImage: null, |
|||
aspectRatio: "1:1", |
|||
resolution: "1K", |
|||
status: "idle", |
|||
error: null, |
|||
imageHistory: [], |
|||
selectedHistoryIndex: -1, |
|||
}), |
|||
]; |
|||
const edges: WorkflowEdge[] = [ |
|||
createTestEdge("e1", "prompt-1", "gen-1", "text", "text"), |
|||
]; |
|||
return buildWorkflowContext(nodes, edges); |
|||
} |
|||
|
|||
// --- Tests ---
|
|||
|
|||
describe("buildEditSystemPrompt", () => { |
|||
describe("basic prompt structure", () => { |
|||
it("returns string containing Node Banana domain expertise", () => { |
|||
const context = buildWorkflowContext([], []); |
|||
const prompt = buildEditSystemPrompt(context); |
|||
|
|||
expect(prompt).toContain("Node Banana"); |
|||
}); |
|||
|
|||
it("contains CURRENT WORKFLOW section", () => { |
|||
const context = buildWorkflowContext([], []); |
|||
const prompt = buildEditSystemPrompt(context); |
|||
|
|||
expect(prompt).toContain("## CURRENT WORKFLOW"); |
|||
}); |
|||
|
|||
it("contains TOOL USAGE RULES section", () => { |
|||
const context = buildWorkflowContext([], []); |
|||
const prompt = buildEditSystemPrompt(context); |
|||
|
|||
expect(prompt).toContain("## TOOL USAGE RULES"); |
|||
}); |
|||
|
|||
it("contains EDITABLE NODE PROPERTIES section", () => { |
|||
const context = buildWorkflowContext([], []); |
|||
const prompt = buildEditSystemPrompt(context); |
|||
|
|||
expect(prompt).toContain("## EDITABLE NODE PROPERTIES"); |
|||
}); |
|||
|
|||
it("contains node type descriptions", () => { |
|||
const context = buildWorkflowContext([], []); |
|||
const prompt = buildEditSystemPrompt(context); |
|||
|
|||
expect(prompt).toContain("### Image Input"); |
|||
expect(prompt).toContain("### Prompt"); |
|||
expect(prompt).toContain("### Generate Image"); |
|||
expect(prompt).toContain("### LLM Text Generation"); |
|||
expect(prompt).toContain("### Split Grid"); |
|||
expect(prompt).toContain("### Annotation"); |
|||
expect(prompt).toContain("### Output"); |
|||
}); |
|||
}); |
|||
|
|||
describe("with workflow context (no selection)", () => { |
|||
it("includes formatted workflow context for non-empty workflow", () => { |
|||
const context = buildSimpleContext(); |
|||
const prompt = buildEditSystemPrompt(context); |
|||
|
|||
expect(prompt).toContain("prompt-1"); |
|||
expect(prompt).toContain("gen-1"); |
|||
expect(prompt).toContain("2 node(s)"); |
|||
}); |
|||
|
|||
it("includes binary data metadata note when no restSummary", () => { |
|||
const context = buildSimpleContext(); |
|||
const prompt = buildEditSystemPrompt(context); |
|||
|
|||
expect(prompt).toContain("Binary data (images, videos) has been replaced with metadata descriptions"); |
|||
}); |
|||
|
|||
it("shows empty canvas message when workflow is empty", () => { |
|||
const context = buildWorkflowContext([], []); |
|||
const prompt = buildEditSystemPrompt(context); |
|||
|
|||
expect(prompt).toContain("The canvas is currently empty."); |
|||
}); |
|||
}); |
|||
|
|||
describe("with scoped selection (restSummary provided)", () => { |
|||
it("includes SELECTED SUBSET section when restSummary has nodes", () => { |
|||
const context = buildSimpleContext(); |
|||
const restSummary: SubgraphResult["restSummary"] = { |
|||
nodeCount: 3, |
|||
typeBreakdown: { prompt: 1, nanoBanana: 2 }, |
|||
boundaryConnections: [ |
|||
{ |
|||
direction: "incoming", |
|||
selectedNodeId: "gen-1", |
|||
otherNodeId: "prompt-2", |
|||
handleType: "text", |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
const prompt = buildEditSystemPrompt(context, restSummary); |
|||
|
|||
expect(prompt).toContain("SELECTED SUBSET"); |
|||
}); |
|||
|
|||
it("mentions rest node count and type breakdown", () => { |
|||
const context = buildSimpleContext(); |
|||
const restSummary: SubgraphResult["restSummary"] = { |
|||
nodeCount: 5, |
|||
typeBreakdown: { imageInput: 2, output: 3 }, |
|||
boundaryConnections: [], |
|||
}; |
|||
|
|||
const prompt = buildEditSystemPrompt(context, restSummary); |
|||
|
|||
expect(prompt).toContain("5 other node(s)"); |
|||
expect(prompt).toContain("2 imageInput"); |
|||
expect(prompt).toContain("3 output"); |
|||
}); |
|||
|
|||
it("mentions boundary connections when present", () => { |
|||
const context = buildSimpleContext(); |
|||
const restSummary: SubgraphResult["restSummary"] = { |
|||
nodeCount: 2, |
|||
typeBreakdown: { output: 2 }, |
|||
boundaryConnections: [ |
|||
{ |
|||
direction: "outgoing", |
|||
selectedNodeId: "gen-1", |
|||
otherNodeId: "out-1", |
|||
handleType: "image", |
|||
}, |
|||
{ |
|||
direction: "incoming", |
|||
selectedNodeId: "gen-1", |
|||
otherNodeId: "img-1", |
|||
handleType: "image", |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
const prompt = buildEditSystemPrompt(context, restSummary); |
|||
|
|||
expect(prompt).toContain("Connections to selected nodes"); |
|||
expect(prompt).toContain("Output to out-1"); |
|||
expect(prompt).toContain("Input from img-1"); |
|||
}); |
|||
}); |
|||
|
|||
describe("with empty restSummary", () => { |
|||
it("does NOT show SELECTED SUBSET section when nodeCount is 0", () => { |
|||
const context = buildSimpleContext(); |
|||
const restSummary: SubgraphResult["restSummary"] = { |
|||
nodeCount: 0, |
|||
typeBreakdown: {}, |
|||
boundaryConnections: [], |
|||
}; |
|||
|
|||
const prompt = buildEditSystemPrompt(context, restSummary); |
|||
|
|||
expect(prompt).not.toContain("SELECTED SUBSET"); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe("createChatTools", () => { |
|||
const nodeIds = ["prompt-1", "gen-1", "out-1"]; |
|||
|
|||
describe("tool structure", () => { |
|||
it("returns object with exactly 3 keys", () => { |
|||
const tools = createChatTools(nodeIds); |
|||
|
|||
const keys = Object.keys(tools); |
|||
expect(keys).toHaveLength(3); |
|||
expect(keys).toContain("answerQuestion"); |
|||
expect(keys).toContain("createWorkflow"); |
|||
expect(keys).toContain("editWorkflow"); |
|||
}); |
|||
|
|||
it("each tool has description property", () => { |
|||
const tools = createChatTools(nodeIds); |
|||
|
|||
expect(tools.answerQuestion).toHaveProperty("description"); |
|||
expect(tools.createWorkflow).toHaveProperty("description"); |
|||
expect(tools.editWorkflow).toHaveProperty("description"); |
|||
}); |
|||
|
|||
it("each tool has execute property", () => { |
|||
const tools = createChatTools(nodeIds); |
|||
|
|||
expect(tools.answerQuestion).toHaveProperty("execute"); |
|||
expect(tools.createWorkflow).toHaveProperty("execute"); |
|||
expect(tools.editWorkflow).toHaveProperty("execute"); |
|||
}); |
|||
}); |
|||
|
|||
describe("answerQuestion tool", () => { |
|||
it("has description mentioning informational questions", () => { |
|||
const tools = createChatTools(nodeIds); |
|||
|
|||
expect(tools.answerQuestion.description).toMatch(/question/i); |
|||
}); |
|||
|
|||
it("execute returns { answer } when called", async () => { |
|||
const tools = createChatTools(nodeIds); |
|||
|
|||
const result = await tools.answerQuestion.execute( |
|||
{ answer: "Use the Resolution dropdown on the Generate node." }, |
|||
{ toolCallId: "test", messages: [], abortSignal: undefined as any } |
|||
); |
|||
|
|||
expect(result).toEqual({ |
|||
answer: "Use the Resolution dropdown on the Generate node.", |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe("createWorkflow tool", () => { |
|||
it("has description mentioning new workflow", () => { |
|||
const tools = createChatTools(nodeIds); |
|||
|
|||
expect(tools.createWorkflow.description).toMatch(/new.*workflow/i); |
|||
}); |
|||
|
|||
it("execute returns { description } when called", async () => { |
|||
const tools = createChatTools(nodeIds); |
|||
|
|||
const result = await tools.createWorkflow.execute( |
|||
{ description: "A workflow for batch image processing" }, |
|||
{ toolCallId: "test", messages: [], abortSignal: undefined as any } |
|||
); |
|||
|
|||
expect(result).toEqual({ |
|||
description: "A workflow for batch image processing", |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe("editWorkflow tool", () => { |
|||
it("has description mentioning edit/modify", () => { |
|||
const tools = createChatTools(nodeIds); |
|||
|
|||
expect(tools.editWorkflow.description).toMatch(/edit|modify/i); |
|||
}); |
|||
|
|||
it("execute returns { operations, explanation } when called", async () => { |
|||
const tools = createChatTools(nodeIds); |
|||
|
|||
const testOps = [ |
|||
{ type: "addNode" as const, nodeType: "prompt" }, |
|||
{ type: "removeNode" as const, nodeId: "out-1" }, |
|||
]; |
|||
|
|||
const result = await tools.editWorkflow.execute( |
|||
{ |
|||
operations: testOps, |
|||
explanation: "Added prompt and removed output", |
|||
}, |
|||
{ toolCallId: "test", messages: [], abortSignal: undefined as any } |
|||
); |
|||
|
|||
expect(result).toHaveProperty("operations"); |
|||
expect(result).toHaveProperty("explanation"); |
|||
expect(result.explanation).toBe("Added prompt and removed output"); |
|||
expect(result.operations).toHaveLength(2); |
|||
}); |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue