Browse Source

test(quick-002): add comprehensive tests for tools.ts

- 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
shrimbly 5 months ago
parent
commit
afd8226da3
  1. 344
      src/lib/chat/tools.test.ts

344
src/lib/chat/tools.test.ts

@ -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…
Cancel
Save