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:
Molecule AI · core-fe 2026-05-10 02:54:54 +00:00
parent 2bc3bea914
commit d35403d402
3 changed files with 228 additions and 1 deletions

View File

@ -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";

View 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");
});
});

View 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");
});
});