Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from "vitest";
import { getInvokeSkillItems } from "#/components/conversation-events/chat/event-content-helpers/get-invoke-skill-items";
import { ObservationEvent } from "#/types/agent-server/core";
import { InvokeSkillObservation } from "#/types/agent-server/core/base/observation";

const makeEvent = (
observation: Partial<InvokeSkillObservation>,
): ObservationEvent<InvokeSkillObservation> =>
({
id: "obs-skill",
timestamp: "2026-06-08T00:00:00.000Z",
source: "environment",
tool_name: "invoke_skill",
tool_call_id: "tool-skill",
action_id: "action-skill",
observation: {
kind: "InvokeSkillObservation",
skill_name: "worktree-switch",
content: [],
...observation,
},
}) as ObservationEvent<InvokeSkillObservation>;

describe("getInvokeSkillItems", () => {
it("returns a single item with the skill name and joined text content", () => {
const items = getInvokeSkillItems(
makeEvent({
skill_name: "worktree-switch",
content: [{ type: "text", text: "# Skill content" }],
}),
);

expect(items).toEqual([
{ name: "worktree-switch", content: "# Skill content" },
]);
});

it("joins multiple text blocks with newlines and trims", () => {
const items = getInvokeSkillItems(
makeEvent({
skill_name: "docker",
content: [
{ type: "text", text: " line one" },
{ type: "text", text: "line two " },
],
}),
);

expect(items).toEqual([{ name: "docker", content: "line one\nline two" }]);
});

it("ignores non-text content blocks", () => {
const items = getInvokeSkillItems(
makeEvent({
skill_name: "docker",
content: [
{ type: "image", image_urls: ["data:image/png;base64,abc"] },
{ type: "text", text: "only text survives" },
] as InvokeSkillObservation["content"],
}),
);

expect(items).toEqual([{ name: "docker", content: "only text survives" }]);
});

it("returns an empty array when there is no skill name and no content", () => {
const items = getInvokeSkillItems(
makeEvent({ skill_name: "", content: [] }),
);

expect(items).toEqual([]);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { describe, expect, it } from "vitest";
import { getACPToolCallResult } from "#/components/conversation-events/chat/event-content-helpers/get-observation-result";
import {
getACPToolCallResult,
getObservationResult,
} from "#/components/conversation-events/chat/event-content-helpers/get-observation-result";
import { ACPToolCallEvent } from "#/types/agent-server/core/events/acp-tool-call-event";
import { ObservationEvent } from "#/types/agent-server/core";

const makeACPEvent = (
overrides: Partial<ACPToolCallEvent> = {},
Expand Down Expand Up @@ -50,3 +54,69 @@ describe("getACPToolCallResult", () => {
expect(getACPToolCallResult(makeACPEvent({ status: null }))).toBeUndefined();
});
});

const makeObs = (
observation: ObservationEvent["observation"],
): ObservationEvent => ({
id: "obs-1",
timestamp: "2024-01-01T00:00:00Z",
source: "environment",
tool_name: "tool",
tool_call_id: "tc-1",
action_id: "act-1",
observation,
});

describe("getObservationResult", () => {
it("maps InvokeSkillObservation is_error → error, otherwise success", () => {
expect(
getObservationResult(
makeObs({
kind: "InvokeSkillObservation",
skill_name: "s",
content: [],
is_error: true,
}),
),
).toBe("error");
expect(
getObservationResult(
makeObs({
kind: "InvokeSkillObservation",
skill_name: "s",
content: [],
is_error: false,
}),
),
).toBe("success");
});

it("maps TaskObservation is_error or failed status → error, otherwise success", () => {
const task = (extra: { status: string; is_error?: boolean }) =>
makeObs({
kind: "TaskObservation",
content: [],
task_id: "t1",
subagent: "code-explorer",
...extra,
});
expect(getObservationResult(task({ status: "completed" }))).toBe("success");
expect(getObservationResult(task({ status: "failed" }))).toBe("error");
expect(
getObservationResult(task({ status: "completed", is_error: true })),
).toBe("error");
});

it("maps CanvasUIObservation is_error → error, otherwise success", () => {
expect(
getObservationResult(
makeObs({ kind: "CanvasUIObservation", content: [], is_error: true }),
),
).toBe("error");
expect(
getObservationResult(
makeObs({ kind: "CanvasUIObservation", content: [], is_error: false }),
),
).toBe("success");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect } from "vitest";
import { renderWithProviders } from "test-utils";
import { I18nKey } from "#/i18n/declaration";
import { SkillReadyContentList } from "#/components/conversation-events/chat/event-message-components/skill-ready-content-list";
import { SkillReadyItem } from "#/components/conversation-events/chat/event-content-helpers/create-skill-ready-event";

Expand Down Expand Up @@ -30,6 +31,24 @@ describe("SkillReadyContentList", () => {
).toBeInTheDocument();
});

it("renders a custom header label when titleKey is provided", () => {
const items = makeItems(["docker", "content"]);

renderWithProviders(
<SkillReadyContentList
items={items}
titleKey={I18nKey.SKILLS$INVOKED_SKILL_KNOWLEDGE}
/>,
);

expect(
screen.getByText("SKILLS$INVOKED_SKILL_KNOWLEDGE"),
).toBeInTheDocument();
expect(
screen.queryByText("SKILLS$TRIGGERED_SKILL_KNOWLEDGE"),
).not.toBeInTheDocument();
});

it("does not show content before clicking", () => {
const items = makeItems(["docker", "Docker usage guide"]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ describe("getEventContent", () => {
).not.toBeInTheDocument();
});

it("returns empty details for file view action instead of 'Unknown event'", () => {
it("renders a file view action through the file-editor visualizer", () => {
const fileViewAction: ActionEvent = {
id: "action-2",
timestamp: new Date().toISOString(),
Expand Down Expand Up @@ -151,7 +151,11 @@ describe("getEventContent", () => {

render(<span>{title}</span>);
expect(screen.getByText("ACTION_MESSAGE$READ")).toBeInTheDocument();
expect(details).toBe("");
// FileEditor is now migrated to a React visualizer: details is a node that
// renders the file-path chip rather than the old empty markdown string.
expect(typeof details).not.toBe("string");
render(<div>{details}</div>);
expect(screen.getByText("/workspace/README.md")).toBeInTheDocument();
});

it("shows action kind for action-like events missing tool_name/tool_call_id", () => {
Expand Down Expand Up @@ -241,4 +245,85 @@ describe("getEventContent", () => {
expect(details).toContain("worktree-switch");
expect(details).toContain("# Skill content");
});

it("titles a TaskAction with the subagent and shows the query", () => {
const taskAction: ActionEvent = {
id: "act-task",
timestamp: new Date().toISOString(),
source: "agent",
thought: [],
thinking_blocks: [],
tool_name: "task",
tool_call_id: "tool-task",
action: {
kind: "TaskAction",
prompt: "Summarize the README",
subagent_type: "code-explorer",
},
} as unknown as ActionEvent;

const { title } = getEventContent(taskAction);

render(<span>{title}</span>);
expect(screen.getByText("ACTION_MESSAGE$TASK")).toBeInTheDocument();
expect(screen.queryByText("TASK")).not.toBeInTheDocument();
});

it("titles a TaskObservation with the subagent", () => {
const taskObservation: ObservationEvent = {
id: "obs-task",
timestamp: new Date().toISOString(),
source: "environment",
tool_name: "task",
tool_call_id: "tool-task",
action_id: "act-task",
observation: {
kind: "TaskObservation",
content: [{ type: "text", text: "All done." }],
is_error: false,
task_id: "task_00000001",
subagent: "code-explorer",
status: "completed",
},
};

const { title } = getEventContent(taskObservation);

render(<span>{title}</span>);
expect(screen.getByText("OBSERVATION_MESSAGE$TASK")).toBeInTheDocument();
// The body is rendered by the task visualizer (covered in task.test.tsx),
// so only the title is asserted here.
});

it("renders CanvasUIObservation as just its acknowledgement text", () => {
const canvasUIObservation: ObservationEvent = {
id: "obs-canvas",
timestamp: new Date().toISOString(),
source: "environment",
tool_name: "canvas_ui",
tool_call_id: "tool-canvas",
action_id: "action-canvas",
observation: {
kind: "CanvasUIObservation",
content: [
{
type: "text",
text: "UI command 'open_tab' dispatched to the Agent Canvas frontend.",
},
],
is_error: false,
},
};

const { title, details } = getEventContent(canvasUIObservation);

render(<span>{title}</span>);
expect(
screen.getByText("OBSERVATION_MESSAGE$CANVAS_UI"),
).toBeInTheDocument();
// The body is exactly the acknowledgement text, not a JSON dump.
expect(details).toBe(
"UI command 'open_tab' dispatched to the Agent Canvas frontend.",
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`bashVisualizer > matches snapshot 1`] = `
<div
class="flex flex-col gap-2"
>
<div
class="relative"
>
<div
class="absolute top-2 right-2 z-10"
>
<button
aria-label="BUTTON$COPY"
class="button-base p-1 cursor-pointer"
data-testid="copy-to-clipboard"
hidden=""
type="button"
>
<svg
fill="none"
height="15"
viewBox="0 0 15 15"
width="15"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.25 2.5C1.25 1.80964 1.80964 1.25 2.5 1.25H8.75C9.44036 1.25 10 1.80964 10 2.5V5H12.5C13.1904 5 13.75 5.55964 13.75 6.25V12.5C13.75 13.1904 13.1904 13.75 12.5 13.75H6.25C5.55964 13.75 5 13.1904 5 12.5V10H2.5C1.80964 10 1.25 9.44036 1.25 8.75V2.5ZM6.25 10V12.5H12.5V6.25H10V8.75C10 9.44036 9.44036 10 8.75 10H6.25ZM8.75 8.75V2.5L2.5 2.5V8.75H8.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
<div
class="rounded-lg text-xs"
style="color: rgb(212, 212, 212); font-size: 13px; text-shadow: none; font-family: Menlo, Monaco, Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace; direction: ltr; text-align: left; white-space: pre; word-spacing: normal; word-break: normal; line-height: 1.5; tab-size: 4; hyphens: none; padding: 1em; margin: 0.5em 0px; overflow: auto; background: rgb(30, 30, 30);"
>
<code
class="language-bash"
style="white-space: pre; color: rgb(212, 212, 212); font-size: 13px; text-shadow: none; font-family: Menlo, Monaco, Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace; direction: ltr; text-align: left; word-spacing: normal; word-break: normal; line-height: 1.5; tab-size: 4; hyphens: none;"
>
<span
class="token"
style="color: rgb(220, 220, 170);"
>
ls
</span>
</code>
</div>
</div>
<div
class="flex flex-col gap-1"
>
<div
class="relative"
>
<div
class="absolute top-2 right-2 z-10"
>
<button
aria-label="BUTTON$COPY"
class="button-base p-1 cursor-pointer"
data-testid="copy-to-clipboard"
hidden=""
type="button"
>
<svg
fill="none"
height="15"
viewBox="0 0 15 15"
width="15"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.25 2.5C1.25 1.80964 1.80964 1.25 2.5 1.25H8.75C9.44036 1.25 10 1.80964 10 2.5V5H12.5C13.1904 5 13.75 5.55964 13.75 6.25V12.5C13.75 13.1904 13.1904 13.75 12.5 13.75H6.25C5.55964 13.75 5 13.1904 5 12.5V10H2.5C1.80964 10 1.25 9.44036 1.25 8.75V2.5ZM6.25 10V12.5H12.5V6.25H10V8.75C10 9.44036 9.44036 10 8.75 10H6.25ZM8.75 8.75V2.5L2.5 2.5V8.75H8.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
<pre
class="overflow-auto whitespace-pre-wrap rounded border border-surface-raised bg-surface-raised p-2 text-xs text-foreground"
>
ok
</pre>
</div>
</div>
</div>
`;
Loading
Loading