Skip to content

Commit df43254

Browse files
authored
test(agent): extract shared ClaudeAcpAgent test harness
The idle-drain, streamed-text, and slash-command tests each carried their own copies of installFakeSession and the message-builder helpers. Extract them into a shared harness (packages/agent/src/test/helpers/claude-agent.ts) and import from all three, unifying the drifted installFakeSession into one definition (returns {query, input}, optional knownSlashCommands, superset of inert fields). makeAgent and the vi.mock blocks stay per-file — they depend on the dynamically-imported (post-vi.mock) agent class, so moving them risks mock hoisting surprises. File-specific helpers (streamed-text's messageStart/ textDelta/enableSignedCommitServer, slash-command's findUnsupportedChunkText) stay put. No behavior change. Generated-By: PostHog Code Task-Id: 39548658-f313-445b-bd1c-d07eaf9281cf
1 parent 070116a commit df43254

4 files changed

Lines changed: 179 additions & 301 deletions

File tree

packages/agent/src/adapters/claude/claude-agent.idle-drain.test.ts

Lines changed: 15 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import type { AgentSideConnection } from "@agentclientprotocol/sdk";
2-
import type {
3-
SDKMessage,
4-
SDKUserMessage,
5-
} from "@anthropic-ai/claude-agent-sdk";
2+
import type { SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
63
import { beforeEach, describe, expect, it, vi } from "vitest";
7-
import { createMockQuery, type MockQuery } from "../../test/mocks/claude-sdk";
8-
import { Pushable } from "../../utils/streams";
4+
import {
5+
assistantMessage,
6+
type ClientMocks,
7+
echoUserMessage,
8+
installFakeSession,
9+
makeClientMocks,
10+
messageChunkTexts,
11+
resultSuccess,
12+
send,
13+
tick,
14+
} from "../../test/helpers/claude-agent";
15+
import type { MockQuery } from "../../test/mocks/claude-sdk";
16+
import type { Pushable } from "../../utils/streams";
917

1018
vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
1119
query: vi.fn(),
@@ -25,126 +33,12 @@ vi.mock("./mcp/tool-metadata", () => ({
2533
const { ClaudeAcpAgent } = await import("./claude-agent");
2634
type Agent = InstanceType<typeof ClaudeAcpAgent>;
2735

28-
interface ClientMocks {
29-
sessionUpdate: ReturnType<typeof vi.fn>;
30-
extNotification: ReturnType<typeof vi.fn>;
31-
}
32-
3336
function makeAgent(): { agent: Agent; client: ClientMocks } {
34-
const client: ClientMocks = {
35-
sessionUpdate: vi.fn().mockResolvedValue(undefined),
36-
extNotification: vi.fn().mockResolvedValue(undefined),
37-
};
37+
const client = makeClientMocks();
3838
const agent = new ClaudeAcpAgent(client as unknown as AgentSideConnection);
3939
return { agent, client };
4040
}
4141

42-
function installFakeSession(
43-
agent: Agent,
44-
sessionId: string,
45-
): { query: MockQuery; input: Pushable<SDKUserMessage> } {
46-
const query = createMockQuery();
47-
const input = new Pushable<SDKUserMessage>();
48-
const abortController = new AbortController();
49-
50-
const session = {
51-
query,
52-
queryOptions: { sessionId, cwd: "/tmp/repo", abortController },
53-
buildInProcessMcpServers: () => ({}),
54-
localToolsServerNames: [] as string[],
55-
input,
56-
cancelled: false,
57-
interruptReason: undefined,
58-
settingsManager: { dispose: vi.fn(), getRepoRoot: () => "/tmp/repo" },
59-
permissionMode: "default" as const,
60-
abortController,
61-
accumulatedUsage: {
62-
inputTokens: 0,
63-
outputTokens: 0,
64-
cachedReadTokens: 0,
65-
cachedWriteTokens: 0,
66-
},
67-
sessionResources: new Set(),
68-
configOptions: [],
69-
promptRunning: false,
70-
pendingMessages: new Map(),
71-
nextPendingOrder: 0,
72-
cwd: "/tmp/repo",
73-
notificationHistory: [] as unknown[],
74-
taskRunId: "run-1",
75-
lastContextWindowSize: 200_000,
76-
modelId: "claude-sonnet-4-6",
77-
taskState: new Map(),
78-
};
79-
80-
(agent as unknown as { session: typeof session }).session = session;
81-
(agent as unknown as { sessionId: string }).sessionId = sessionId;
82-
83-
return { query, input };
84-
}
85-
86-
function tick(): Promise<void> {
87-
return new Promise((resolve) => setImmediate(resolve));
88-
}
89-
90-
async function send(query: MockQuery, message: unknown): Promise<void> {
91-
query._mockHelpers.sendMessage(message as SDKMessage);
92-
await tick();
93-
}
94-
95-
// Replays the prompt's own user message back through the query so
96-
// `promptReplayed` flips and the terminal `result` is not skipped.
97-
async function echoUserMessage(
98-
query: MockQuery,
99-
input: Pushable<SDKUserMessage>,
100-
): Promise<void> {
101-
const { value: pushed } = await input[Symbol.asyncIterator]().next();
102-
await send(query, pushed);
103-
}
104-
105-
function assistantMessage(sessionId: string, apiId: string, text: string) {
106-
return {
107-
type: "assistant",
108-
parent_tool_use_id: null,
109-
session_id: sessionId,
110-
uuid: `assistant-${apiId}`,
111-
message: {
112-
id: apiId,
113-
role: "assistant",
114-
content: [{ type: "text", text }],
115-
},
116-
};
117-
}
118-
119-
function resultSuccess(sessionId: string, uuid = "result-1") {
120-
return {
121-
type: "result",
122-
subtype: "success",
123-
session_id: sessionId,
124-
uuid,
125-
result: "",
126-
is_error: false,
127-
usage: {},
128-
modelUsage: {},
129-
};
130-
}
131-
132-
function messageChunkTexts(
133-
calls: ClientMocks["sessionUpdate"]["mock"]["calls"],
134-
): string[] {
135-
return calls
136-
.map(
137-
([call]) =>
138-
(
139-
call as {
140-
update?: { sessionUpdate?: string; content?: { text?: string } };
141-
}
142-
).update,
143-
)
144-
.filter((update) => update?.sessionUpdate === "agent_message_chunk")
145-
.map((update) => update?.content?.text ?? "");
146-
}
147-
14842
// Runs one complete turn: prompt -> echo -> assistant text -> result.
14943
async function runTurn(
15044
agent: Agent,

packages/agent/src/adapters/claude/claude-agent.slash-command.test.ts

Lines changed: 8 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import type { AgentSideConnection } from "@agentclientprotocol/sdk";
22
import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
33
import { beforeEach, describe, expect, it, vi } from "vitest";
4-
import { createMockQuery, type MockQuery } from "../../test/mocks/claude-sdk";
5-
import { Pushable } from "../../utils/streams";
4+
import {
5+
type ClientMocks,
6+
installFakeSession,
7+
makeClientMocks,
8+
} from "../../test/helpers/claude-agent";
69

710
vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
811
query: vi.fn(),
@@ -20,65 +23,12 @@ vi.mock("./mcp/tool-metadata", () => ({
2023
const { ClaudeAcpAgent } = await import("./claude-agent");
2124
type Agent = InstanceType<typeof ClaudeAcpAgent>;
2225

23-
interface ClientMocks {
24-
sessionUpdate: ReturnType<typeof vi.fn>;
25-
extNotification: ReturnType<typeof vi.fn>;
26-
}
27-
2826
function makeAgent(): { agent: Agent; client: ClientMocks } {
29-
const client: ClientMocks = {
30-
sessionUpdate: vi.fn().mockResolvedValue(undefined),
31-
extNotification: vi.fn().mockResolvedValue(undefined),
32-
};
27+
const client = makeClientMocks();
3328
const agent = new ClaudeAcpAgent(client as unknown as AgentSideConnection);
3429
return { agent, client };
3530
}
3631

37-
function installFakeSession(
38-
agent: Agent,
39-
sessionId: string,
40-
knownSlashCommands?: Set<string>,
41-
): MockQuery {
42-
const query = createMockQuery();
43-
const input = new Pushable();
44-
const abortController = new AbortController();
45-
46-
const session = {
47-
query,
48-
queryOptions: { sessionId, cwd: "/tmp/repo", abortController },
49-
buildInProcessMcpServers: () => ({}),
50-
localToolsServerNames: [] as string[],
51-
input,
52-
cancelled: false,
53-
interruptReason: undefined,
54-
settingsManager: { dispose: vi.fn(), getRepoRoot: () => "/tmp/repo" },
55-
permissionMode: "default" as const,
56-
abortController,
57-
accumulatedUsage: {
58-
inputTokens: 0,
59-
outputTokens: 0,
60-
cachedReadTokens: 0,
61-
cachedWriteTokens: 0,
62-
},
63-
sessionResources: new Set(),
64-
configOptions: [],
65-
promptRunning: false,
66-
pendingMessages: new Map(),
67-
nextPendingOrder: 0,
68-
cwd: "/tmp/repo",
69-
notificationHistory: [] as unknown[],
70-
taskRunId: "run-1",
71-
lastContextWindowSize: 200_000,
72-
modelId: "claude-sonnet-4-6",
73-
knownSlashCommands,
74-
};
75-
76-
(agent as unknown as { session: typeof session }).session = session;
77-
(agent as unknown as { sessionId: string }).sessionId = sessionId;
78-
79-
return query;
80-
}
81-
8232
function findUnsupportedChunkText(
8333
calls: ClientMocks["sessionUpdate"]["mock"]["calls"],
8434
): string | undefined {
@@ -148,7 +98,7 @@ describe("ClaudeAcpAgent.prompt — early idle handling", () => {
14898

14999
it.each(cases)("$label", async (tc) => {
150100
const { agent, client } = makeAgent();
151-
const query = installFakeSession(
101+
const { query } = installFakeSession(
152102
agent,
153103
tc.sessionId,
154104
tc.knownCommands as Set<string> | undefined,
@@ -208,7 +158,7 @@ describe("ClaudeAcpAgent.prompt — force-cancel backstop", () => {
208158
it("returns 'cancelled' when the SDK never yields after interrupt (issue #680)", async () => {
209159
const { agent } = makeAgent();
210160
const sessionId = "s-wedged";
211-
const query = installFakeSession(agent, sessionId);
161+
const { query } = installFakeSession(agent, sessionId);
212162
query.interrupt.mockImplementation(async () => {});
213163
(agent as unknown as { forceCancelGraceMs: number }).forceCancelGraceMs = 5;
214164

0 commit comments

Comments
 (0)