0547488f13
- Chat: an Agent step shows its task, status and tool calls and opens its conversation in place; Windows paths (E:\…, C:/…) in replies open as files - Prompt: attach a file on this computer by path (native picker), clearer too-large messages - Artifacts: edit title/description and a skill's files (new snapshot), Reinstall, install history, Hub-only rows, skills installed into ~/.claude/skills and ~/.agents/skills - Files: batch upload of small files in one extract - AI Tools: npm install falls back to --prefix ~/.local when the global prefix is not writable
147 lines
5.2 KiB
TypeScript
147 lines
5.2 KiB
TypeScript
/**
|
|
* @file AgentDetails.test.tsx
|
|
* @description Verifies what the chat shows of a subagent: an Agent step's
|
|
* facts, task and report (from its result, or the task-notification that
|
|
* closed a background one), and that "View conversation" opens that
|
|
* agent's own conversation in place, rendered by the chat's owner.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
import { fireEvent, render, screen } from "@testing-library/react";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { MessageList } from "../MessageList";
|
|
import { agentNotices, agentRef, parseUsage, type AgentLinks } from "../AgentDetails";
|
|
import type { TranscriptContent, TranscriptMessage } from "../../../lib/types";
|
|
|
|
const at = new Date().toISOString();
|
|
const m = (p: Partial<TranscriptMessage>): TranscriptMessage =>
|
|
({ type: "user", timestamp: at, content: [], ...p }) as TranscriptMessage;
|
|
|
|
const call: TranscriptContent = {
|
|
type: "tool_use",
|
|
id: "t1",
|
|
name: "Agent",
|
|
input: { description: "Map the hooks", subagent_type: "Explore", prompt: "Read **server/**" },
|
|
};
|
|
const links = (p: Partial<AgentLinks> = {}): AgentLinks => ({
|
|
ids: new Map(),
|
|
notices: new Map(),
|
|
live: false,
|
|
...p,
|
|
});
|
|
|
|
describe("agentRef", () => {
|
|
it("reads a foreground agent's facts and report, without the orchestrator's footer", () => {
|
|
const a = agentRef(
|
|
call,
|
|
{
|
|
type: "tool_result",
|
|
id: "t1",
|
|
output: "The hooks live in server/routes/hooks.js.\n\nagentId: a1b2 (for resuming)",
|
|
agent: {
|
|
id: "a1b2",
|
|
model: "claude-opus-5-5",
|
|
status: "completed",
|
|
duration_ms: 65_000,
|
|
tokens: 12_000,
|
|
tool_uses: 7,
|
|
},
|
|
},
|
|
links()
|
|
);
|
|
expect(a).toMatchObject({
|
|
agentId: "a1b2",
|
|
type: "Explore",
|
|
status: "done",
|
|
background: false,
|
|
prompt: "Read **server/**",
|
|
report: "The hooks live in server/routes/hooks.js.",
|
|
durationMs: 65_000,
|
|
tokens: 12_000,
|
|
toolUses: 7,
|
|
});
|
|
});
|
|
|
|
it("closes a background agent with its notification's report and usage", () => {
|
|
const notice = m({
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: "<task-notification>\n<tool-use-id>t1</tool-use-id>\n<status>completed</status>\n<result>All done</result>\n<usage><subagent_tokens>88007</subagent_tokens><tool_uses>31</tool_uses><duration_ms>132350</duration_ms></usage>\n</task-notification>",
|
|
},
|
|
],
|
|
});
|
|
const launched: TranscriptContent = {
|
|
type: "tool_result",
|
|
id: "t1",
|
|
output: "Async agent launched successfully.\nagentId: a9b4",
|
|
};
|
|
const running = agentRef(call, launched, links({ live: true }));
|
|
expect(running).toMatchObject({ status: "running", background: true, report: null });
|
|
const done = agentRef(call, launched, links({ notices: agentNotices([notice]) }));
|
|
expect(done).toMatchObject({
|
|
agentId: "a9b4",
|
|
status: "done",
|
|
report: "All done",
|
|
tokens: 88007,
|
|
toolUses: 31,
|
|
durationMs: 132350,
|
|
});
|
|
});
|
|
|
|
it("finds the transcript from the session's list when the result names none", () => {
|
|
const a = agentRef(call, null, links({ ids: new Map([["t1", "c3d4"]]) }));
|
|
expect(a).toMatchObject({ agentId: "c3d4", status: "stopped" });
|
|
});
|
|
|
|
it("parses the line form of a usage block", () => {
|
|
expect(parseUsage("<usage>total_tokens: 500\ntool_uses: 3\nduration_ms: 900</usage>")).toEqual({
|
|
tokens: 500,
|
|
toolUses: 3,
|
|
durationMs: 900,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("an Agent step in the chat", () => {
|
|
it("shows the agent's facts and report, and opens its conversation in place", () => {
|
|
const render_ = vi.fn((a: { agentId: string | null }) => <p>chat of {a.agentId}</p>);
|
|
render(
|
|
<MessageList
|
|
loading={false}
|
|
agentLinks={links({ render: render_ })}
|
|
messages={[
|
|
m({ sender: "user", content: [{ type: "text", text: "go" }] }),
|
|
m({ type: "assistant", sender: "assistant", stop_reason: "tool_use", content: [call] }),
|
|
m({
|
|
content: [
|
|
{
|
|
type: "tool_result",
|
|
id: "t1",
|
|
output: "Found **3** hooks",
|
|
agent: {
|
|
id: "a1b2",
|
|
model: null,
|
|
status: "completed",
|
|
duration_ms: null,
|
|
tokens: null,
|
|
tool_uses: 4,
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
]}
|
|
/>
|
|
);
|
|
fireEvent.click(screen.getByRole("button", { name: /ran an agent/i }));
|
|
expect(screen.getByTestId("agent-facts").textContent).toContain("Explore");
|
|
expect(screen.getByTestId("agent-facts").textContent).toContain("done");
|
|
expect(screen.getByTestId("agent-facts").textContent).toContain("4 tool calls");
|
|
expect(screen.getByTestId("agent-step").textContent).toContain("Found 3 hooks");
|
|
expect(screen.queryByTestId("agent-chat")).toBeNull();
|
|
fireEvent.click(screen.getByTestId("agent-open"));
|
|
expect(screen.getByTestId("agent-chat").textContent).toBe("chat of a1b2");
|
|
// The thread takes the report's place.
|
|
expect(screen.getByTestId("agent-step").textContent).not.toContain("Found 3 hooks");
|
|
});
|
|
});
|