Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions packages/web/src/__tests__/SubagentCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { SubagentCard } from "../components/StepCards";
import type { TrajectoryStep } from "../types";

function nativeStep(
status = "CORTEX_STEP_STATUS_DONE",
): TrajectoryStep {
return {
type: "CORTEX_STEP_TYPE_INVOKE_SUBAGENT",
status,
invokeSubagent: {
subagents: [
{
role: "Integration Reviewer",
typeName: "general-purpose",
initialPrompt: "Review the integration",
},
{
role: "Security Reviewer",
typeName: "research",
initialPrompt: "Review security boundaries",
},
],
},
};
}

describe("SubagentCard", () => {
it("renders every native subagent and expands every prompt", async () => {
render(<SubagentCard step={nativeStep()} />);

expect(screen.getByText("Integration Reviewer")).toBeInTheDocument();
expect(screen.getByText("Security Reviewer")).toBeInTheDocument();
expect(screen.queryByText("Review the integration")).not.toBeInTheDocument();

await userEvent.click(
screen.getByRole("button", { name: /2 Subagents Invoked/i }),
);

expect(screen.getByText("Review the integration")).toBeInTheDocument();
expect(screen.getByText("Review security boundaries")).toBeInTheDocument();
});

it.each([
["CORTEX_STEP_STATUS_PENDING", "Pending", "cmd-wait"],
["CORTEX_STEP_STATUS_ERROR", "Failed", "cmd-fail"],
["CORTEX_STEP_STATUS_CANCELED", "Canceled", "cmd-fail"],
["CORTEX_STEP_STATUS_INTERRUPTED", "Interrupted", "cmd-fail"],
["CORTEX_STEP_STATUS_DONE", "Done", "cmd-ok"],
])("renders %s with the correct state", (status, label, className) => {
const { container } = render(<SubagentCard step={nativeStep(status)} />);

expect(screen.getByText(label)).toBeInTheDocument();
expect(container.querySelector(".subagent-card")).toHaveClass(className);
});

it("renders tool-specific send_message content", async () => {
const step: TrajectoryStep = {
type: "CORTEX_STEP_TYPE_TOOL_CALL",
metadata: {
toolCall: {
name: "send_message",
argumentsJson: JSON.stringify({
Recipient: "conversation-123",
Message: "Please inspect the auth flow",
}),
},
},
};
render(<SubagentCard step={step} />);

expect(screen.getByText("Message to conversation-123")).toBeInTheDocument();
expect(screen.getByText("conversation-123")).toBeInTheDocument();
await userEvent.click(
screen.getByRole("button", { name: /Message to conversation-123/i }),
);
expect(screen.getByText("Please inspect the auth flow")).toBeInTheDocument();
});

it("renders untrusted labels as text rather than HTML", () => {
const step: TrajectoryStep = {
type: "CORTEX_STEP_TYPE_INVOKE_SUBAGENT",
invokeSubagent: {
subagents: [
{
role: '<img src=x onerror="alert(1)">',
initialPrompt: "safe text",
},
],
},
};
const { container } = render(<SubagentCard step={step} />);

expect(
screen.getByText('<img src=x onerror="alert(1)">'),
).toBeInTheDocument();
expect(container.querySelector("img")).toBeNull();
});
});
183 changes: 183 additions & 0 deletions packages/web/src/__tests__/stepsToMessages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,5 +455,188 @@ describe("stepsToMessages", () => {
expect(msgs).toHaveLength(1);
expect(msgs[0].type).toBe("CORTEX_STEP_TYPE_SUBAGENT");
expect(msgs[0].step).toBe(step);
expect(msgs[0].subagent).toMatchObject({
kind: "invoke",
title: "Subagent Invoked",
action: "Invoking research subagent",
items: [
{
role: "Config Auditor",
typeName: "research",
details: [
{ label: "Instructions", text: "Audit all config files" },
],
},
],
});
});

it("correlates a payloadless native marker with captured planner arguments", () => {
const marker: TrajectoryStep = {
type: "CORTEX_STEP_TYPE_INVOKE_SUBAGENT",
status: "CORTEX_STEP_STATUS_DONE",
};
const steps: TrajectoryStep[] = [
{
type: "CORTEX_STEP_TYPE_PLANNER_RESPONSE",
plannerResponse: {
toolCalls: [
{
name: "invoke_subagent",
argumentsJson: JSON.stringify({
Subagents: [
{
Role: "Integration Reviewer",
TypeName: "general-purpose",
Prompt: "Review API integration",
},
{
Role: "Security Reviewer",
TypeName: "research",
Prompt: "Review trust boundaries",
},
],
}),
},
],
},
},
marker,
];

const msgs = stepsToMessages(steps);

expect(msgs).toHaveLength(1);
expect(msgs[0].step).toBe(marker);
expect(msgs[0].subagent?.title).toBe("2 Subagents Invoked");
expect(msgs[0].subagent?.items).toHaveLength(2);
expect(msgs[0].subagent?.items.map((item) => item.role)).toEqual([
"Integration Reviewer",
"Security Reviewer",
]);
});

it("does not correlate a stale tool call across planner turns", () => {
const steps: TrajectoryStep[] = [
{
type: "CORTEX_STEP_TYPE_PLANNER_RESPONSE",
plannerResponse: {
toolCalls: [
{
name: "invoke_subagent",
argumentsJson: JSON.stringify({
Subagents: [{ Role: "Stale Reviewer" }],
}),
},
],
},
},
{
type: "CORTEX_STEP_TYPE_PLANNER_RESPONSE",
plannerResponse: {},
},
{
type: "CORTEX_STEP_TYPE_INVOKE_SUBAGENT",
},
];

const [msg] = stepsToMessages(steps);

expect(msg.subagent?.items[0].role).toBe("Subagent");
});

it("uses the current native invokeSubagent payload and metadata", () => {
const step: TrajectoryStep = {
type: "CORTEX_STEP_TYPE_INVOKE_SUBAGENT",
status: "CORTEX_STEP_STATUS_RUNNING",
metadata: {
toolSummary: "Review team",
toolAction: "Running two reviews",
},
invokeSubagent: {
subagents: [
{
role: "Bug Hunter",
typeName: "general-purpose",
initialPrompt: "Find bugs",
modelTier: "MODEL_TIER_PRO",
},
{
role: "Security Auditor",
typeName: "research",
initialPrompt: "Find vulnerabilities",
},
],
},
};

const [msg] = stepsToMessages([step]);

expect(msg.subagent).toMatchObject({
kind: "invoke",
title: "Review team",
action: "Running two reviews",
});
expect(msg.subagent?.items).toHaveLength(2);
expect(msg.subagent?.items[0]).toMatchObject({
role: "Bug Hunter",
typeName: "general-purpose",
model: "MODEL_TIER_PRO",
});
});

it.each([
{
name: "define_subagent",
args: {
name: "security-reviewer",
description: "Reviews trust boundaries",
system_prompt: "Inspect untrusted input",
},
kind: "define",
title: "Define security-reviewer",
role: "security-reviewer",
},
{
name: "send_message",
args: { Recipient: "conversation-123", Message: "Check the parser" },
kind: "message",
title: "Message to conversation-123",
role: "conversation-123",
},
{
name: "manage_subagents",
args: { Action: "kill", ConversationIds: ["one", "two"] },
kind: "manage",
title: "Stop Subagents",
role: "kill",
},
])("normalizes $name instead of labeling it as an invocation", (fixture) => {
const step: TrajectoryStep = {
type: "CORTEX_STEP_TYPE_TOOL_CALL",
metadata: {
toolCall: {
name: fixture.name,
argumentsJson: JSON.stringify(fixture.args),
},
},
};

const [msg] = stepsToMessages([step]);

expect(msg.subagent).toMatchObject({
kind: fixture.kind,
title: fixture.title,
items: [{ role: fixture.role }],
});
});

it("does not treat inherited object properties as subagent tool names", () => {
const step: TrajectoryStep = {
type: "CORTEX_STEP_TYPE_TOOL_CALL",
metadata: { toolCall: { name: "toString" } },
};

expect(stepsToMessages([step])).toEqual([]);
});
});
2 changes: 1 addition & 1 deletion packages/web/src/components/ChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ function SystemMessage({
if (msg.type === "CORTEX_STEP_TYPE_SUBAGENT") {
return (
<div className="message system">
<SubagentCard step={msg.step} />
<SubagentCard step={msg.step} data={msg.subagent} />
</div>
);
}
Expand Down
Loading
Loading