chatccc 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -14
- package/package.json +1 -1
- package/src/__tests__/adapter-interface.test.ts +152 -0
- package/src/__tests__/cards.test.ts +122 -5
- package/src/__tests__/claude-adapter.test.ts +529 -0
- package/src/__tests__/cursor-adapter.test.ts +250 -0
- package/src/__tests__/session.test.ts +147 -1
- package/src/adapters/adapter-interface.ts +127 -0
- package/src/adapters/claude-adapter.ts +258 -0
- package/src/adapters/cursor-adapter.ts +229 -0
- package/src/cards.ts +121 -22
- package/src/config.ts +63 -2
- package/src/feishu-api.ts +22 -8
- package/src/index.ts +107 -29
- package/src/session.ts +513 -455
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { normalizeSdkMessage, createClaudeAdapter } from "../adapters/claude-adapter.ts";
|
|
3
|
+
import type { UnifiedStreamMessage } from "../adapters/adapter-interface.ts";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Mock Claude SDK
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
const mockSend = vi.fn();
|
|
10
|
+
const mockClose = vi.fn();
|
|
11
|
+
const mockStreamNext = vi.fn();
|
|
12
|
+
const mockStreamIterable: AsyncGenerator<any, void> = {
|
|
13
|
+
next: mockStreamNext,
|
|
14
|
+
[Symbol.asyncIterator]() { return this; },
|
|
15
|
+
} as any;
|
|
16
|
+
|
|
17
|
+
function mockSession(overrides: Partial<{ send: any; close: any; streamNext: any }> = {}) {
|
|
18
|
+
return {
|
|
19
|
+
send: overrides.send ?? mockSend,
|
|
20
|
+
close: overrides.close ?? mockClose,
|
|
21
|
+
stream: () => ({
|
|
22
|
+
next: overrides.streamNext ?? mockStreamNext,
|
|
23
|
+
[Symbol.asyncIterator]() { return this; },
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
vi.mock("@anthropic-ai/claude-agent-sdk", () => {
|
|
29
|
+
// We only mock the full adapter integration tests; normalizeSdkMessage is pure
|
|
30
|
+
return {
|
|
31
|
+
unstable_v2_createSession: vi.fn(),
|
|
32
|
+
unstable_v2_resumeSession: vi.fn(),
|
|
33
|
+
getSessionInfo: vi.fn(),
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// normalizeSdkMessage — 核心映射逻辑测试(纯函数,不需要 mock SDK)
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
describe("normalizeSdkMessage", () => {
|
|
42
|
+
// --- assistant messages ---
|
|
43
|
+
|
|
44
|
+
it("normalizes assistant message with text block", () => {
|
|
45
|
+
const result = normalizeSdkMessage({
|
|
46
|
+
type: "assistant",
|
|
47
|
+
message: { content: [{ type: "text", text: "Hello world" }] },
|
|
48
|
+
});
|
|
49
|
+
expect(result).not.toBeNull();
|
|
50
|
+
expect(result!.type).toBe("assistant");
|
|
51
|
+
expect(result!.blocks).toEqual([{ type: "text", text: "Hello world" }]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("normalizes assistant message with thinking block", () => {
|
|
55
|
+
const result = normalizeSdkMessage({
|
|
56
|
+
type: "assistant",
|
|
57
|
+
message: { content: [{ type: "thinking", thinking: "Let me analyze..." }] },
|
|
58
|
+
});
|
|
59
|
+
expect(result).not.toBeNull();
|
|
60
|
+
expect(result!.blocks).toEqual([{ type: "thinking", thinking: "Let me analyze..." }]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("normalizes assistant message with tool_use block", () => {
|
|
64
|
+
const result = normalizeSdkMessage({
|
|
65
|
+
type: "assistant",
|
|
66
|
+
message: {
|
|
67
|
+
content: [{ type: "tool_use", name: "Read", input: { file_path: "/tmp/test.txt" } }],
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
expect(result).not.toBeNull();
|
|
71
|
+
expect(result!.blocks).toEqual([
|
|
72
|
+
{ type: "tool_use", name: "Read", input: { file_path: "/tmp/test.txt" } },
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('uses "unknown" for tool_use without name', () => {
|
|
77
|
+
const result = normalizeSdkMessage({
|
|
78
|
+
type: "assistant",
|
|
79
|
+
message: { content: [{ type: "tool_use", input: { x: 1 } }] },
|
|
80
|
+
});
|
|
81
|
+
expect(result!.blocks[0]).toMatchObject({ type: "tool_use", name: "unknown" });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("normalizes user message with tool_result block (success)", () => {
|
|
85
|
+
const result = normalizeSdkMessage({
|
|
86
|
+
type: "user",
|
|
87
|
+
message: {
|
|
88
|
+
content: [
|
|
89
|
+
{ type: "tool_result", tool_use_id: "abc123", content: "file content here", is_error: false },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
expect(result).not.toBeNull();
|
|
94
|
+
expect(result!.type).toBe("user");
|
|
95
|
+
const block = result!.blocks[0] as any;
|
|
96
|
+
expect(block.type).toBe("tool_result");
|
|
97
|
+
expect(block.tool_use_id).toBe("abc123");
|
|
98
|
+
expect(block.content).toBe("file content here");
|
|
99
|
+
expect(block.is_error).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("normalizes user message with tool_result block (error)", () => {
|
|
103
|
+
const result = normalizeSdkMessage({
|
|
104
|
+
type: "user",
|
|
105
|
+
message: {
|
|
106
|
+
content: [
|
|
107
|
+
{ type: "tool_result", tool_use_id: "err456", content: "permission denied", is_error: true },
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
expect(result!.blocks[0]).toMatchObject({
|
|
112
|
+
type: "tool_result",
|
|
113
|
+
tool_use_id: "err456",
|
|
114
|
+
is_error: true,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("normalizes tool_result with array content", () => {
|
|
119
|
+
const content = [{ type: "text", text: "line 1" }, { type: "text", text: "line 2" }];
|
|
120
|
+
const result = normalizeSdkMessage({
|
|
121
|
+
type: "user",
|
|
122
|
+
message: { content: [{ type: "tool_result", tool_use_id: "arr", content }] },
|
|
123
|
+
});
|
|
124
|
+
expect(result!.blocks[0]).toMatchObject({
|
|
125
|
+
type: "tool_result",
|
|
126
|
+
tool_use_id: "arr",
|
|
127
|
+
});
|
|
128
|
+
expect((result!.blocks[0] as any).content).toEqual(content);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("normalizes redacted_thinking block", () => {
|
|
132
|
+
const result = normalizeSdkMessage({
|
|
133
|
+
type: "assistant",
|
|
134
|
+
message: { content: [{ type: "redacted_thinking" }] },
|
|
135
|
+
});
|
|
136
|
+
expect(result!.blocks).toEqual([{ type: "redacted_thinking" }]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("normalizes search_result block", () => {
|
|
140
|
+
const result = normalizeSdkMessage({
|
|
141
|
+
type: "assistant",
|
|
142
|
+
message: {
|
|
143
|
+
content: [{ type: "search_result", query: "TypeScript best practices" }],
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
expect(result!.blocks).toEqual([
|
|
147
|
+
{ type: "search_result", query: "TypeScript best practices" },
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("normalizes search_result without query (empty string)", () => {
|
|
152
|
+
const result = normalizeSdkMessage({
|
|
153
|
+
type: "assistant",
|
|
154
|
+
message: { content: [{ type: "search_result" }] },
|
|
155
|
+
});
|
|
156
|
+
expect(result!.blocks).toEqual([{ type: "search_result", query: "" }]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// --- system messages ---
|
|
160
|
+
|
|
161
|
+
it("normalizes system compact_boundary message", () => {
|
|
162
|
+
const result = normalizeSdkMessage({
|
|
163
|
+
type: "system",
|
|
164
|
+
subtype: "compact_boundary",
|
|
165
|
+
compact_metadata: {
|
|
166
|
+
trigger: "auto",
|
|
167
|
+
pre_tokens: 15000,
|
|
168
|
+
post_tokens: 8000,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
expect(result).not.toBeNull();
|
|
172
|
+
expect(result!.type).toBe("system");
|
|
173
|
+
expect(result!.blocks).toEqual([
|
|
174
|
+
{ type: "compact_boundary", trigger: "auto", pre_tokens: 15000, post_tokens: 8000 },
|
|
175
|
+
]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("normalizes compact_boundary with manual trigger", () => {
|
|
179
|
+
const result = normalizeSdkMessage({
|
|
180
|
+
type: "system",
|
|
181
|
+
subtype: "compact_boundary",
|
|
182
|
+
compact_metadata: { trigger: "manual", pre_tokens: 20000 },
|
|
183
|
+
});
|
|
184
|
+
expect(result!.blocks).toEqual([
|
|
185
|
+
{ type: "compact_boundary", trigger: "manual", pre_tokens: 20000, post_tokens: undefined },
|
|
186
|
+
]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("normalizes compact_boundary without post_tokens", () => {
|
|
190
|
+
const result = normalizeSdkMessage({
|
|
191
|
+
type: "system",
|
|
192
|
+
subtype: "compact_boundary",
|
|
193
|
+
compact_metadata: { trigger: "auto", pre_tokens: 10000 },
|
|
194
|
+
});
|
|
195
|
+
expect(result!.blocks).toEqual([
|
|
196
|
+
{ type: "compact_boundary", trigger: "auto", pre_tokens: 10000, post_tokens: undefined },
|
|
197
|
+
]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// --- mixed blocks ---
|
|
201
|
+
|
|
202
|
+
it("normalizes message with multiple block types mixed", () => {
|
|
203
|
+
const result = normalizeSdkMessage({
|
|
204
|
+
type: "assistant",
|
|
205
|
+
message: {
|
|
206
|
+
content: [
|
|
207
|
+
{ type: "thinking", thinking: "Hmm..." },
|
|
208
|
+
{ type: "tool_use", name: "Grep", input: { pattern: "foo" } },
|
|
209
|
+
{ type: "text", text: "Found it." },
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
expect(result).not.toBeNull();
|
|
214
|
+
expect(result!.blocks).toHaveLength(3);
|
|
215
|
+
expect(result!.blocks[0]).toEqual({ type: "thinking", thinking: "Hmm..." });
|
|
216
|
+
expect(result!.blocks[1]).toEqual({ type: "tool_use", name: "Grep", input: { pattern: "foo" } });
|
|
217
|
+
expect(result!.blocks[2]).toEqual({ type: "text", text: "Found it." });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// --- edge cases ---
|
|
221
|
+
|
|
222
|
+
it("skips unknown block types", () => {
|
|
223
|
+
const result = normalizeSdkMessage({
|
|
224
|
+
type: "assistant",
|
|
225
|
+
message: {
|
|
226
|
+
content: [
|
|
227
|
+
{ type: "unknown_type", data: "whatever" },
|
|
228
|
+
{ type: "text", text: "still here" },
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
expect(result!.blocks).toHaveLength(1);
|
|
233
|
+
expect(result!.blocks[0]).toEqual({ type: "text", text: "still here" });
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("returns null for non-assistant/non-user/non-system messages", () => {
|
|
237
|
+
expect(
|
|
238
|
+
normalizeSdkMessage({ type: "result", subtype: "success" }),
|
|
239
|
+
).toBeNull();
|
|
240
|
+
expect(
|
|
241
|
+
normalizeSdkMessage({ type: "stream_event" }),
|
|
242
|
+
).toBeNull();
|
|
243
|
+
expect(
|
|
244
|
+
normalizeSdkMessage({ type: "status" }),
|
|
245
|
+
).toBeNull();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("returns null for system message without compact_boundary subtype", () => {
|
|
249
|
+
expect(
|
|
250
|
+
normalizeSdkMessage({ type: "system", subtype: "init" }),
|
|
251
|
+
).toBeNull();
|
|
252
|
+
expect(
|
|
253
|
+
normalizeSdkMessage({ type: "system", subtype: "notification" }),
|
|
254
|
+
).toBeNull();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("returns message with empty blocks for content=[ ]", () => {
|
|
258
|
+
const result = normalizeSdkMessage({
|
|
259
|
+
type: "assistant",
|
|
260
|
+
message: { content: [] },
|
|
261
|
+
});
|
|
262
|
+
expect(result).not.toBeNull();
|
|
263
|
+
expect(result!.blocks).toEqual([]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("returns null for message without content", () => {
|
|
267
|
+
const result = normalizeSdkMessage({
|
|
268
|
+
type: "assistant",
|
|
269
|
+
message: {},
|
|
270
|
+
});
|
|
271
|
+
expect(result).toBeNull();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("returns null for message with null content array", () => {
|
|
275
|
+
const result = normalizeSdkMessage({
|
|
276
|
+
type: "assistant",
|
|
277
|
+
message: { content: undefined as any },
|
|
278
|
+
});
|
|
279
|
+
expect(result).toBeNull();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("returns null for compact_boundary without metadata", () => {
|
|
283
|
+
const result = normalizeSdkMessage({
|
|
284
|
+
type: "system",
|
|
285
|
+
subtype: "compact_boundary",
|
|
286
|
+
});
|
|
287
|
+
expect(result).toBeNull();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("skips thinking block with empty thinking string", () => {
|
|
291
|
+
const result = normalizeSdkMessage({
|
|
292
|
+
type: "assistant",
|
|
293
|
+
message: { content: [{ type: "thinking", thinking: "" }] },
|
|
294
|
+
});
|
|
295
|
+
// empty thinking string → falsy check (block.thinking → "") → skipped
|
|
296
|
+
expect(result!.blocks).toEqual([]);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("skips text block with empty text string", () => {
|
|
300
|
+
const result = normalizeSdkMessage({
|
|
301
|
+
type: "assistant",
|
|
302
|
+
message: { content: [{ type: "text", text: "" }] },
|
|
303
|
+
});
|
|
304
|
+
// empty text string → falsy check → skipped
|
|
305
|
+
expect(result!.blocks).toEqual([]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("handles message with undefined type gracefully", () => {
|
|
309
|
+
const result = normalizeSdkMessage({
|
|
310
|
+
message: { content: [{ type: "text", text: "orphan" }] },
|
|
311
|
+
});
|
|
312
|
+
expect(result).toBeNull();
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// createClaudeAdapter — 集成测试(mock SDK)
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
describe("createClaudeAdapter", () => {
|
|
321
|
+
let sdk: any;
|
|
322
|
+
|
|
323
|
+
beforeEach(async () => {
|
|
324
|
+
vi.clearAllMocks();
|
|
325
|
+
sdk = await import("@anthropic-ai/claude-agent-sdk");
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("returns adapter with correct displayName and sessionDescPrefix", () => {
|
|
329
|
+
const adapter = createClaudeAdapter({
|
|
330
|
+
model: "claude-sonnet-4-6",
|
|
331
|
+
effort: "high",
|
|
332
|
+
isDefault: (v) => v.trim().toLowerCase() === "default",
|
|
333
|
+
});
|
|
334
|
+
expect(adapter.displayName).toBe("Claude Code");
|
|
335
|
+
expect(adapter.sessionDescPrefix).toBe("Claude Code Session:");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("createSession returns sessionId from init event", async () => {
|
|
339
|
+
const mockSessionObj = mockSession({
|
|
340
|
+
streamNext: vi
|
|
341
|
+
.fn()
|
|
342
|
+
.mockResolvedValueOnce({
|
|
343
|
+
done: false,
|
|
344
|
+
value: { session_id: "test-sid-001", type: "system", subtype: "init" },
|
|
345
|
+
})
|
|
346
|
+
.mockResolvedValueOnce({ done: true, value: undefined }),
|
|
347
|
+
});
|
|
348
|
+
sdk.unstable_v2_createSession.mockReturnValue(mockSessionObj);
|
|
349
|
+
|
|
350
|
+
const adapter = createClaudeAdapter({
|
|
351
|
+
model: "claude-sonnet-4-6",
|
|
352
|
+
effort: "high",
|
|
353
|
+
isDefault: () => false,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const result = await adapter.createSession("/tmp");
|
|
357
|
+
expect(result.sessionId).toBe("test-sid-001");
|
|
358
|
+
expect(sdk.unstable_v2_createSession).toHaveBeenCalledTimes(1);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("createSession throws when first event has no session_id", async () => {
|
|
362
|
+
const mockSessionObj = mockSession({
|
|
363
|
+
streamNext: vi
|
|
364
|
+
.fn()
|
|
365
|
+
.mockResolvedValueOnce({
|
|
366
|
+
done: false,
|
|
367
|
+
value: { type: "other", no_session_id: true },
|
|
368
|
+
}),
|
|
369
|
+
});
|
|
370
|
+
sdk.unstable_v2_createSession.mockReturnValue(mockSessionObj);
|
|
371
|
+
|
|
372
|
+
const adapter = createClaudeAdapter({
|
|
373
|
+
model: "claude-sonnet-4-6",
|
|
374
|
+
effort: "high",
|
|
375
|
+
isDefault: () => false,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
await expect(adapter.createSession("/tmp")).rejects.toThrow(
|
|
379
|
+
"No session ID",
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("createSession sends 'ok' to the session", async () => {
|
|
384
|
+
const sendSpy = vi.fn();
|
|
385
|
+
const mockSessionObj = mockSession({
|
|
386
|
+
send: sendSpy,
|
|
387
|
+
streamNext: vi
|
|
388
|
+
.fn()
|
|
389
|
+
.mockResolvedValueOnce({
|
|
390
|
+
done: false,
|
|
391
|
+
value: { session_id: "sid", type: "system", subtype: "init" },
|
|
392
|
+
})
|
|
393
|
+
.mockResolvedValueOnce({ done: true, value: undefined }),
|
|
394
|
+
});
|
|
395
|
+
sdk.unstable_v2_createSession.mockReturnValue(mockSessionObj);
|
|
396
|
+
|
|
397
|
+
const adapter = createClaudeAdapter({
|
|
398
|
+
model: "claude-sonnet-4-6",
|
|
399
|
+
effort: "high",
|
|
400
|
+
isDefault: () => false,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
await adapter.createSession("/tmp");
|
|
404
|
+
expect(sendSpy).toHaveBeenCalledWith("ok");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("prompt yields normalized messages", async () => {
|
|
408
|
+
const mockSessionObj = mockSession({
|
|
409
|
+
streamNext: (() => {
|
|
410
|
+
let callCount = 0;
|
|
411
|
+
return vi.fn(async () => {
|
|
412
|
+
callCount++;
|
|
413
|
+
if (callCount === 1) {
|
|
414
|
+
return {
|
|
415
|
+
done: false,
|
|
416
|
+
value: {
|
|
417
|
+
type: "assistant",
|
|
418
|
+
message: { content: [{ type: "text", text: "Hello!" }] },
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
return { done: true, value: undefined };
|
|
423
|
+
});
|
|
424
|
+
})(),
|
|
425
|
+
});
|
|
426
|
+
sdk.unstable_v2_resumeSession.mockReturnValue(mockSessionObj);
|
|
427
|
+
|
|
428
|
+
const adapter = createClaudeAdapter({
|
|
429
|
+
model: "claude-sonnet-4-6",
|
|
430
|
+
effort: "high",
|
|
431
|
+
isDefault: () => false,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const messages: UnifiedStreamMessage[] = [];
|
|
435
|
+
for await (const msg of adapter.prompt("sid", "hi", "/tmp")) {
|
|
436
|
+
messages.push(msg);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
expect(messages).toHaveLength(1);
|
|
440
|
+
expect(messages[0].type).toBe("assistant");
|
|
441
|
+
expect(messages[0].blocks).toEqual([{ type: "text", text: "Hello!" }]);
|
|
442
|
+
expect(sdk.unstable_v2_resumeSession).toHaveBeenCalledTimes(1);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("prompt handles AbortSignal", async () => {
|
|
446
|
+
const mockSessionObj = mockSession({
|
|
447
|
+
streamNext: vi
|
|
448
|
+
.fn()
|
|
449
|
+
.mockResolvedValueOnce({
|
|
450
|
+
done: false,
|
|
451
|
+
value: {
|
|
452
|
+
type: "assistant",
|
|
453
|
+
message: { content: [{ type: "text", text: "msg1" }] },
|
|
454
|
+
},
|
|
455
|
+
})
|
|
456
|
+
.mockResolvedValueOnce({
|
|
457
|
+
done: false,
|
|
458
|
+
value: {
|
|
459
|
+
type: "assistant",
|
|
460
|
+
message: { content: [{ type: "text", text: "msg2" }] },
|
|
461
|
+
},
|
|
462
|
+
})
|
|
463
|
+
.mockResolvedValue({ done: true, value: undefined }),
|
|
464
|
+
});
|
|
465
|
+
sdk.unstable_v2_resumeSession.mockReturnValue(mockSessionObj);
|
|
466
|
+
|
|
467
|
+
const controller = new AbortController();
|
|
468
|
+
const adapter = createClaudeAdapter({
|
|
469
|
+
model: "claude-sonnet-4-6",
|
|
470
|
+
effort: "high",
|
|
471
|
+
isDefault: () => false,
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const messages: UnifiedStreamMessage[] = [];
|
|
475
|
+
for await (const msg of adapter.prompt("sid", "hi", "/tmp", controller.signal)) {
|
|
476
|
+
messages.push(msg);
|
|
477
|
+
if (messages.length === 1) controller.abort();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Should only get the first message before abort stops iteration
|
|
481
|
+
expect(messages).toHaveLength(1);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("getSessionInfo returns mapped SessionInfo", async () => {
|
|
485
|
+
sdk.getSessionInfo.mockResolvedValue({
|
|
486
|
+
sessionId: "sid",
|
|
487
|
+
cwd: "/home/user",
|
|
488
|
+
summary: "A test session",
|
|
489
|
+
lastModified: 1234567890,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const adapter = createClaudeAdapter({
|
|
493
|
+
model: "claude-sonnet-4-6",
|
|
494
|
+
effort: "high",
|
|
495
|
+
isDefault: () => false,
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const info = await adapter.getSessionInfo("sid");
|
|
499
|
+
expect(info).toEqual({
|
|
500
|
+
sessionId: "sid",
|
|
501
|
+
cwd: "/home/user",
|
|
502
|
+
summary: "A test session",
|
|
503
|
+
lastModified: 1234567890,
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("getSessionInfo returns undefined when SDK returns undefined", async () => {
|
|
508
|
+
sdk.getSessionInfo.mockResolvedValue(undefined);
|
|
509
|
+
|
|
510
|
+
const adapter = createClaudeAdapter({
|
|
511
|
+
model: "claude-sonnet-4-6",
|
|
512
|
+
effort: "high",
|
|
513
|
+
isDefault: () => false,
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const info = await adapter.getSessionInfo("nonexistent");
|
|
517
|
+
expect(info).toBeUndefined();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("closeSession is no-op (does not throw)", async () => {
|
|
521
|
+
const adapter = createClaudeAdapter({
|
|
522
|
+
model: "claude-sonnet-4-6",
|
|
523
|
+
effort: "high",
|
|
524
|
+
isDefault: () => false,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
await expect(adapter.closeSession("any-sid")).resolves.toBeUndefined();
|
|
528
|
+
});
|
|
529
|
+
});
|