forked from molecule-ai/molecule-core
test(canvas): add tests for extractMessageText and providerIdForModel
extractMessageText (ConversationTraceModal): MCP task/task format, params.message.parts, result.parts/root.text, plain string result, priority order, error resilience. providerIdForModel (MissingKeysModal): model match, no match, whitespace trimming, undefined models, no required_env, multi-env sort. Also exports extractMessageText from ConversationTraceModal for testing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
2bc3bea914
commit
d35403d402
@ -13,7 +13,8 @@ interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function extractMessageText(body: Record<string, unknown> | null): string {
|
||||
/** Exported for unit testing — see ConversationTraceModal.test.ts */
|
||||
export function extractMessageText(body: Record<string, unknown> | null): string {
|
||||
if (!body) return "";
|
||||
try {
|
||||
// Simple task format from MCP server: {task: "..."}
|
||||
@ -84,6 +85,7 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
|
||||
});
|
||||
}, [open, nodes]);
|
||||
|
||||
/** Exported for unit testing — see ConversationTraceModal.test.ts */
|
||||
const isA2A = (e: ActivityEntry) =>
|
||||
e.activity_type === "a2a_receive" || e.activity_type === "a2a_send";
|
||||
|
||||
|
||||
156
canvas/src/components/__tests__/ConversationTraceModal.test.tsx
Normal file
156
canvas/src/components/__tests__/ConversationTraceModal.test.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* Tests for ConversationTraceModal's extractMessageText helper.
|
||||
*
|
||||
* Covers: MCP simple task format, request params.message.parts extraction,
|
||||
* response result.parts extraction, result.root.text extraction, plain string
|
||||
* result, null input, malformed input, empty strings.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { extractMessageText } from "../ConversationTraceModal";
|
||||
|
||||
describe("extractMessageText — MCP simple task format", () => {
|
||||
it("extracts text from body.task field", () => {
|
||||
const body = { task: "Deploy the agent to production" };
|
||||
expect(extractMessageText(body)).toBe("Deploy the agent to production");
|
||||
});
|
||||
|
||||
it("returns empty string when body is null", () => {
|
||||
expect(extractMessageText(null)).toBe("");
|
||||
});
|
||||
|
||||
it("returns empty string when body is undefined", () => {
|
||||
expect(extractMessageText(undefined as unknown as null)).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractMessageText — request params.message format", () => {
|
||||
it("extracts text from params.message.parts[].text", () => {
|
||||
const body = {
|
||||
params: {
|
||||
message: {
|
||||
parts: [{ text: "Hello world" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(extractMessageText(body)).toBe("Hello world");
|
||||
});
|
||||
|
||||
it("joins multiple parts with newlines", () => {
|
||||
const body = {
|
||||
params: {
|
||||
message: {
|
||||
parts: [
|
||||
{ text: "First part" },
|
||||
{ text: "Second part" },
|
||||
{ text: "Third part" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(extractMessageText(body)).toBe("First part\nSecond part\nThird part");
|
||||
});
|
||||
|
||||
it("ignores parts without text field", () => {
|
||||
const body = {
|
||||
params: {
|
||||
message: {
|
||||
parts: [{ text: "Hello" }, { other: "field" }, { text: "World" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(extractMessageText(body)).toBe("Hello\nWorld");
|
||||
});
|
||||
|
||||
it("returns empty string when params.message is absent", () => {
|
||||
const body = { params: {} };
|
||||
expect(extractMessageText(body)).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractMessageText — response result format", () => {
|
||||
it("extracts text from result.parts[].text", () => {
|
||||
const body = {
|
||||
result: {
|
||||
parts: [{ text: "Agent response" }],
|
||||
},
|
||||
};
|
||||
expect(extractMessageText(body)).toBe("Agent response");
|
||||
});
|
||||
|
||||
it("extracts text from result.parts[].root.text", () => {
|
||||
const body = {
|
||||
result: {
|
||||
parts: [{ root: { text: "Root response text" } }],
|
||||
},
|
||||
};
|
||||
expect(extractMessageText(body)).toBe("Root response text");
|
||||
});
|
||||
|
||||
it("prefers parts[].text over parts[].root.text", () => {
|
||||
const body = {
|
||||
result: {
|
||||
parts: [
|
||||
{ text: "Direct text" },
|
||||
{ root: { text: "Root text" } },
|
||||
],
|
||||
},
|
||||
};
|
||||
// Both are non-empty strings, so the first one wins (filter picks the first)
|
||||
// The implementation: rText from rParts[0].text = "Direct text"
|
||||
expect(extractMessageText(body)).toBe("Direct text");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractMessageText — plain string result", () => {
|
||||
it("returns body.result when it is a plain string", () => {
|
||||
const body = { result: "Simple string response" };
|
||||
expect(extractMessageText(body)).toBe("Simple string response");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractMessageText — priority order", () => {
|
||||
it("prefers task format over params format", () => {
|
||||
const body = {
|
||||
task: "Task text",
|
||||
params: { message: { parts: [{ text: "Params text" }] } },
|
||||
};
|
||||
// Implementation: checks task first, returns if non-empty
|
||||
expect(extractMessageText(body)).toBe("Task text");
|
||||
});
|
||||
|
||||
it("prefers params format over result format", () => {
|
||||
const body = {
|
||||
params: { message: { parts: [{ text: "Params text" }] } },
|
||||
result: { parts: [{ text: "Result text" }] },
|
||||
};
|
||||
// Implementation: checks params.message.parts first (after task)
|
||||
expect(extractMessageText(body)).toBe("Params text");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractMessageText — error resilience", () => {
|
||||
it("returns empty string on malformed input", () => {
|
||||
expect(extractMessageText({})).toBe("");
|
||||
expect(extractMessageText({ params: null })).toBe("");
|
||||
expect(extractMessageText({ result: null })).toBe("");
|
||||
});
|
||||
|
||||
it("returns empty string when all fields are absent", () => {
|
||||
expect(extractMessageText({ random: "field" })).toBe("");
|
||||
});
|
||||
|
||||
it("handles missing parts array gracefully", () => {
|
||||
const body = { params: { message: {} } };
|
||||
expect(extractMessageText(body)).toBe("");
|
||||
});
|
||||
|
||||
it("handles parts with undefined text gracefully", () => {
|
||||
const body = {
|
||||
result: {
|
||||
parts: [{ text: undefined }, { text: "valid" }],
|
||||
},
|
||||
};
|
||||
expect(extractMessageText(body)).toBe("valid");
|
||||
});
|
||||
});
|
||||
69
canvas/src/components/__tests__/MissingKeysModal.test.tsx
Normal file
69
canvas/src/components/__tests__/MissingKeysModal.test.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* Tests for MissingKeysModal's providerIdForModel helper.
|
||||
*
|
||||
* Covers: model match, no match, empty modelId, whitespace-only modelId,
|
||||
* model with no required_env, models undefined, single vs multiple env vars,
|
||||
* stable sort order for env var ordering.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { providerIdForModel } from "../MissingKeysModal";
|
||||
|
||||
describe("providerIdForModel — match behavior", () => {
|
||||
it("returns sorted-joined env vars when model is found", () => {
|
||||
const models = [
|
||||
{ id: "claude-3-5-sonnet", name: "Claude 3.5 Sonnet", required_env: ["ANTHROPIC_API_KEY"] },
|
||||
];
|
||||
expect(providerIdForModel("claude-3-5-sonnet", models)).toBe("ANTHROPIC_API_KEY");
|
||||
});
|
||||
|
||||
it("returns null when model is not found", () => {
|
||||
const models = [
|
||||
{ id: "claude-3-5-sonnet", name: "Claude 3.5 Sonnet", required_env: ["ANTHROPIC_API_KEY"] },
|
||||
];
|
||||
expect(providerIdForModel("unknown-model", models)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when models is undefined", () => {
|
||||
expect(providerIdForModel("claude-3-5-sonnet", undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when modelId is empty string", () => {
|
||||
const models = [{ id: "claude", name: "Claude", required_env: ["KEY"] }];
|
||||
expect(providerIdForModel("", models)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when modelId is whitespace-only", () => {
|
||||
const models = [{ id: "claude", name: "Claude", required_env: ["KEY"] }];
|
||||
expect(providerIdForModel(" ", models)).toBeNull();
|
||||
});
|
||||
|
||||
it("trims whitespace from modelId before matching", () => {
|
||||
const models = [{ id: "claude", name: "Claude", required_env: ["KEY"] }];
|
||||
expect(providerIdForModel(" claude ", models)).toBe("KEY");
|
||||
});
|
||||
});
|
||||
|
||||
describe("providerIdForModel — required_env variations", () => {
|
||||
it("returns null when model has no required_env", () => {
|
||||
const models = [{ id: "local-model", name: "Local Model", required_env: [] }];
|
||||
expect(providerIdForModel("local-model", models)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when model.required_env is undefined", () => {
|
||||
const models = [{ id: "local-model", name: "Local Model" }] as Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
required_env?: string[];
|
||||
}>;
|
||||
expect(providerIdForModel("local-model", models)).toBeNull();
|
||||
});
|
||||
|
||||
it("sorts and joins multiple required_env alphabetically", () => {
|
||||
const models = [
|
||||
{ id: "openrouter", name: "OpenRouter", required_env: ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"] },
|
||||
];
|
||||
// Expected: alphabetically sorted = ANTHROPIC_API_KEY|OPENAI_API_KEY
|
||||
expect(providerIdForModel("openrouter", models)).toBe("ANTHROPIC_API_KEY|OPENAI_API_KEY");
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user