macro-agent 0.0.16 → 0.1.0
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/dist/acp/macro-agent.d.ts +2 -0
- package/dist/acp/macro-agent.d.ts.map +1 -1
- package/dist/acp/macro-agent.js +52 -20
- package/dist/acp/macro-agent.js.map +1 -1
- package/dist/agent/agent-manager.d.ts.map +1 -1
- package/dist/agent/agent-manager.js +23 -5
- package/dist/agent/agent-manager.js.map +1 -1
- package/dist/map/adapter/acp-over-map.d.ts +16 -0
- package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
- package/dist/map/adapter/acp-over-map.js +242 -24
- package/dist/map/adapter/acp-over-map.js.map +1 -1
- package/dist/map/adapter/map-adapter.d.ts +1 -0
- package/dist/map/adapter/map-adapter.d.ts.map +1 -1
- package/dist/map/adapter/map-adapter.js +57 -8
- package/dist/map/adapter/map-adapter.js.map +1 -1
- package/dist/store/event-store.d.ts +5 -0
- package/dist/store/event-store.d.ts.map +1 -1
- package/dist/store/event-store.js +126 -53
- package/dist/store/event-store.js.map +1 -1
- package/dist/store/instance.d.ts +0 -2
- package/dist/store/instance.d.ts.map +1 -1
- package/dist/store/instance.js +1 -24
- package/dist/store/instance.js.map +1 -1
- package/dist/store/types/agents.d.ts +5 -0
- package/dist/store/types/agents.d.ts.map +1 -1
- package/package.json +3 -3
- package/references/acp-factory-ref/package-lock.json +2 -2
- package/references/acp-factory-ref/package.json +2 -2
- package/references/claude-code-acp/package-lock.json +2 -2
- package/references/claude-code-acp/package.json +1 -1
- package/references/claude-code-acp/src/acp-agent.ts +3 -6
- package/src/acp/__tests__/history.test.ts +8 -4
- package/src/acp/macro-agent.ts +60 -26
- package/src/agent/__tests__/agent-manager.test.ts +4 -6
- package/src/agent/agent-manager.ts +24 -5
- package/src/map/adapter/__tests__/acp-over-map-cancel.test.ts +802 -0
- package/src/map/adapter/__tests__/acp-over-map-history.test.ts +1123 -0
- package/src/map/adapter/__tests__/acp-over-map-persistence.e2e.test.ts +440 -0
- package/src/map/adapter/acp-over-map.ts +282 -25
- package/src/map/adapter/map-adapter.ts +79 -9
- package/src/store/__tests__/event-store.test.ts +44 -12
- package/src/store/__tests__/instance.test.ts +5 -7
- package/src/store/event-store.ts +157 -57
- package/src/store/instance.ts +1 -29
- package/src/store/types/agents.ts +1 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP-over-MAP History Persistence E2E Test
|
|
3
|
+
*
|
|
4
|
+
* Tests the full lifecycle of history persistence across server restarts:
|
|
5
|
+
* 1. Start server → create session → send prompt (history recorded)
|
|
6
|
+
* 2. Shut down server
|
|
7
|
+
* 3. Restart server (same data directory) → reconnect → load history
|
|
8
|
+
*
|
|
9
|
+
* Uses a real file-backed EventStore to verify data survives restarts.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
13
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { tmpdir } from "os";
|
|
16
|
+
import { ACPOverMAPHandler } from "../acp-over-map.js";
|
|
17
|
+
import type { ACPEnvelope } from "../acp-over-map.js";
|
|
18
|
+
import { createEventStore, type EventStore } from "../../../store/event-store.js";
|
|
19
|
+
import type { AgentManager } from "../../../agent/agent-manager.js";
|
|
20
|
+
import type { TaskManager } from "../../../task/task-manager.js";
|
|
21
|
+
import type { Agent, Task } from "../../../store/types/index.js";
|
|
22
|
+
import type { AgentId } from "../../../store/types/index.js";
|
|
23
|
+
import { vi } from "vitest";
|
|
24
|
+
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────
|
|
26
|
+
// Helpers
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
function createMockAgent(overrides: Partial<Agent> = {}): Agent {
|
|
30
|
+
return {
|
|
31
|
+
id: "agent-1" as AgentId,
|
|
32
|
+
session_id: "session-1",
|
|
33
|
+
state: "running",
|
|
34
|
+
task: "Test task",
|
|
35
|
+
task_id: "task-1",
|
|
36
|
+
parent: null,
|
|
37
|
+
lineage: [],
|
|
38
|
+
config: {},
|
|
39
|
+
cwd: "/test/cwd",
|
|
40
|
+
created_at: Date.now(),
|
|
41
|
+
started_at: Date.now(),
|
|
42
|
+
...overrides,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createMockTask(overrides: Partial<Task> = {}): Task {
|
|
47
|
+
return {
|
|
48
|
+
id: "task-1",
|
|
49
|
+
description: "Test task",
|
|
50
|
+
status: "in_progress",
|
|
51
|
+
created_by: "agent-1",
|
|
52
|
+
created_at: Date.now(),
|
|
53
|
+
...overrides,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createMockAgentManager(promptUpdates: unknown[] = []): AgentManager {
|
|
58
|
+
const mockAgent = createMockAgent();
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
spawn: vi.fn().mockResolvedValue({
|
|
62
|
+
id: "agent-new",
|
|
63
|
+
session_id: "session-new",
|
|
64
|
+
agent: createMockAgent({ id: "agent-new" as AgentId, session_id: "session-new" }),
|
|
65
|
+
session: {},
|
|
66
|
+
}),
|
|
67
|
+
get: vi.fn().mockReturnValue(mockAgent),
|
|
68
|
+
list: vi.fn().mockReturnValue([mockAgent]),
|
|
69
|
+
listHeadManagers: vi.fn().mockReturnValue([mockAgent]),
|
|
70
|
+
getChildren: vi.fn().mockReturnValue([]),
|
|
71
|
+
getHierarchy: vi.fn().mockReturnValue({
|
|
72
|
+
root: { agent: mockAgent, children: [] },
|
|
73
|
+
depth: 1,
|
|
74
|
+
totalAgents: 1,
|
|
75
|
+
}),
|
|
76
|
+
getOrCreateHeadManager: vi.fn().mockResolvedValue({
|
|
77
|
+
id: "agent-1",
|
|
78
|
+
session_id: "session-1",
|
|
79
|
+
agent: mockAgent,
|
|
80
|
+
session: {},
|
|
81
|
+
}),
|
|
82
|
+
hasActiveSession: vi.fn().mockReturnValue(true),
|
|
83
|
+
resume: vi.fn().mockResolvedValue({
|
|
84
|
+
id: "agent-1",
|
|
85
|
+
session_id: "session-1",
|
|
86
|
+
agent: mockAgent,
|
|
87
|
+
session: {},
|
|
88
|
+
}),
|
|
89
|
+
terminate: vi.fn().mockResolvedValue(undefined),
|
|
90
|
+
prompt: vi.fn().mockReturnValue({
|
|
91
|
+
[Symbol.asyncIterator]: async function* () {
|
|
92
|
+
for (const update of promptUpdates) {
|
|
93
|
+
yield update;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
getSession: vi.fn().mockReturnValue(null),
|
|
98
|
+
onLifecycleEvent: vi.fn().mockReturnValue(() => {}),
|
|
99
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
100
|
+
respondToPermission: vi.fn().mockReturnValue(true),
|
|
101
|
+
cancelPermission: vi.fn().mockReturnValue(true),
|
|
102
|
+
} as unknown as AgentManager;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function createMockTaskManager(): TaskManager {
|
|
106
|
+
return {
|
|
107
|
+
get: vi.fn().mockReturnValue(createMockTask()),
|
|
108
|
+
list: vi.fn().mockReturnValue([createMockTask()]),
|
|
109
|
+
create: vi.fn().mockReturnValue(createMockTask()),
|
|
110
|
+
} as unknown as TaskManager;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function envelope(
|
|
114
|
+
streamId: string,
|
|
115
|
+
method: string,
|
|
116
|
+
params?: unknown,
|
|
117
|
+
sessionId?: string,
|
|
118
|
+
): ACPEnvelope {
|
|
119
|
+
return {
|
|
120
|
+
acp: {
|
|
121
|
+
jsonrpc: "2.0",
|
|
122
|
+
id: `${streamId}-${method}-${Date.now()}`,
|
|
123
|
+
method,
|
|
124
|
+
params,
|
|
125
|
+
},
|
|
126
|
+
acpContext: {
|
|
127
|
+
streamId,
|
|
128
|
+
sessionId,
|
|
129
|
+
direction: "client-to-agent",
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─────────────────────────────────────────────────────────────────
|
|
135
|
+
// E2E Tests
|
|
136
|
+
// ─────────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
describe("ACP-over-MAP history persistence (E2E with file-backed store)", () => {
|
|
139
|
+
let tmpDir: string;
|
|
140
|
+
const instanceId = "test-persist-instance";
|
|
141
|
+
const agentId = "agent-1" as AgentId;
|
|
142
|
+
const sessionId = "session-1";
|
|
143
|
+
|
|
144
|
+
afterEach(() => {
|
|
145
|
+
// Clean up temp directory
|
|
146
|
+
if (tmpDir) {
|
|
147
|
+
try {
|
|
148
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
149
|
+
} catch {
|
|
150
|
+
// Ignore cleanup errors
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Register an agent in the EventStore so loadSession can resolve it.
|
|
157
|
+
*/
|
|
158
|
+
function registerAgent(eventStore: EventStore): void {
|
|
159
|
+
eventStore.emit({
|
|
160
|
+
type: "spawn",
|
|
161
|
+
source: { agent_id: agentId },
|
|
162
|
+
payload: {
|
|
163
|
+
agent_id: agentId,
|
|
164
|
+
session_id: sessionId,
|
|
165
|
+
task: "Test task",
|
|
166
|
+
task_id: "task-1",
|
|
167
|
+
cwd: "/test/cwd",
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
eventStore.emit({
|
|
171
|
+
type: "lifecycle",
|
|
172
|
+
source: { agent_id: agentId },
|
|
173
|
+
payload: {
|
|
174
|
+
agent_id: agentId,
|
|
175
|
+
action: "started",
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
it("should persist history across server restarts", async () => {
|
|
181
|
+
tmpDir = mkdtempSync(join(tmpdir(), "acp-persist-"));
|
|
182
|
+
|
|
183
|
+
// ════════════════════════════════════════════════════════════════
|
|
184
|
+
// LIFECYCLE 1: Create session, send prompt, record history
|
|
185
|
+
// ════════════════════════════════════════════════════════════════
|
|
186
|
+
|
|
187
|
+
let eventStore1 = await createEventStore({
|
|
188
|
+
instanceId,
|
|
189
|
+
baseDir: tmpDir,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Register agent in the store
|
|
193
|
+
registerAgent(eventStore1);
|
|
194
|
+
|
|
195
|
+
const handler1 = new ACPOverMAPHandler({
|
|
196
|
+
agentManager: createMockAgentManager([
|
|
197
|
+
{
|
|
198
|
+
sessionUpdate: "agent_message_chunk",
|
|
199
|
+
content: { type: "text", text: "Hello! I'm your assistant. " },
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
sessionUpdate: "agent_message_chunk",
|
|
203
|
+
content: { type: "text", text: "How can I help you today?" },
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
sessionUpdate: "tool_call",
|
|
207
|
+
toolCallId: "tc-1",
|
|
208
|
+
title: "ListFiles",
|
|
209
|
+
status: "completed",
|
|
210
|
+
rawInput: { path: "/src" },
|
|
211
|
+
output: "index.ts\napp.ts",
|
|
212
|
+
},
|
|
213
|
+
]),
|
|
214
|
+
eventStore: eventStore1,
|
|
215
|
+
taskManager: createMockTaskManager(),
|
|
216
|
+
defaultCwd: "/test/cwd",
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Initialize stream
|
|
220
|
+
const streamId1 = "lifecycle-1-stream";
|
|
221
|
+
await handler1.processRequest(
|
|
222
|
+
agentId,
|
|
223
|
+
envelope(streamId1, "initialize", {
|
|
224
|
+
protocolVersion: 1,
|
|
225
|
+
capabilities: {},
|
|
226
|
+
clientInfo: { name: "test-tui", version: "1.0" },
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Create session
|
|
231
|
+
const newResult = await handler1.processRequest(
|
|
232
|
+
agentId,
|
|
233
|
+
envelope(streamId1, "session/new", {
|
|
234
|
+
cwd: "/test",
|
|
235
|
+
mcpServers: [],
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
const createdSessionId = (newResult.acp.result as { sessionId?: string })?.sessionId;
|
|
239
|
+
expect(createdSessionId).toBe(sessionId);
|
|
240
|
+
|
|
241
|
+
// Send a prompt — this should record turns in the EventStore
|
|
242
|
+
const promptResult = await handler1.processRequest(
|
|
243
|
+
agentId,
|
|
244
|
+
envelope(streamId1, "session/prompt", {
|
|
245
|
+
prompt: [{ type: "text", text: "List the files in /src" }],
|
|
246
|
+
}, createdSessionId),
|
|
247
|
+
);
|
|
248
|
+
expect((promptResult.acp.result as { stopReason: string }).stopReason).toBe("end_turn");
|
|
249
|
+
|
|
250
|
+
// Verify history exists in lifecycle 1
|
|
251
|
+
const historyCheck = await handler1.processRequest(
|
|
252
|
+
agentId,
|
|
253
|
+
envelope(streamId1, "_macro/getHistory", { sessionId: createdSessionId }),
|
|
254
|
+
);
|
|
255
|
+
const checkTurns = (historyCheck.acp.result as { turns: unknown[] }).turns;
|
|
256
|
+
expect(checkTurns).toHaveLength(2); // user + assistant
|
|
257
|
+
|
|
258
|
+
// ════════════════════════════════════════════════════════════════
|
|
259
|
+
// SHUTDOWN: Close the EventStore (simulates server shutdown)
|
|
260
|
+
// ════════════════════════════════════════════════════════════════
|
|
261
|
+
|
|
262
|
+
await eventStore1.close();
|
|
263
|
+
|
|
264
|
+
// ════════════════════════════════════════════════════════════════
|
|
265
|
+
// LIFECYCLE 2: Restart with same data dir, reconnect, load history
|
|
266
|
+
// ════════════════════════════════════════════════════════════════
|
|
267
|
+
|
|
268
|
+
const eventStore2 = await createEventStore({
|
|
269
|
+
instanceId,
|
|
270
|
+
baseDir: tmpDir,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Verify agent data survived restart
|
|
274
|
+
const restoredAgent = eventStore2.getAgent(agentId);
|
|
275
|
+
expect(restoredAgent).not.toBeNull();
|
|
276
|
+
expect(restoredAgent?.session_id).toBe(sessionId);
|
|
277
|
+
|
|
278
|
+
const handler2 = new ACPOverMAPHandler({
|
|
279
|
+
agentManager: createMockAgentManager(), // Fresh agent manager
|
|
280
|
+
eventStore: eventStore2,
|
|
281
|
+
taskManager: createMockTaskManager(),
|
|
282
|
+
defaultCwd: "/test/cwd",
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Initialize new stream (simulates TUI restart)
|
|
286
|
+
const streamId2 = "lifecycle-2-stream";
|
|
287
|
+
await handler2.processRequest(
|
|
288
|
+
agentId,
|
|
289
|
+
envelope(streamId2, "initialize", {
|
|
290
|
+
protocolVersion: 1,
|
|
291
|
+
capabilities: {},
|
|
292
|
+
clientInfo: { name: "test-tui-reconnect", version: "1.0" },
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// Load session with _resolve_ pattern (like the TUI does)
|
|
297
|
+
const loadResult = await handler2.processRequest(
|
|
298
|
+
agentId,
|
|
299
|
+
envelope(streamId2, "session/load", {
|
|
300
|
+
sessionId: "_resolve_",
|
|
301
|
+
cwd: "/test",
|
|
302
|
+
mcpServers: [],
|
|
303
|
+
_meta: { agentId },
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
// Verify loadSession returns the resolved session ID
|
|
308
|
+
const resolvedSessionId = (loadResult.acp.result as { sessionId?: string })?.sessionId;
|
|
309
|
+
expect(resolvedSessionId).toBeDefined();
|
|
310
|
+
expect(resolvedSessionId).toBe(sessionId);
|
|
311
|
+
expect(resolvedSessionId).not.toBe("_resolve_");
|
|
312
|
+
|
|
313
|
+
// Load history using the resolved session ID (exactly like the TUI does)
|
|
314
|
+
const historyResult = await handler2.processRequest(
|
|
315
|
+
agentId,
|
|
316
|
+
envelope(streamId2, "_macro/getHistory", { sessionId: resolvedSessionId }),
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// ════════════════════════════════════════════════════════════════
|
|
320
|
+
// VERIFY: History is restored correctly
|
|
321
|
+
// ════════════════════════════════════════════════════════════════
|
|
322
|
+
|
|
323
|
+
const turns = (historyResult.acp.result as {
|
|
324
|
+
turns: { role: string; timestamp: number; content: unknown }[];
|
|
325
|
+
}).turns;
|
|
326
|
+
|
|
327
|
+
expect(turns).toHaveLength(2);
|
|
328
|
+
|
|
329
|
+
// User turn
|
|
330
|
+
expect(turns[0].role).toBe("user");
|
|
331
|
+
expect(turns[0].content).toBe("List the files in /src");
|
|
332
|
+
|
|
333
|
+
// Assistant turn — accumulated text + tool call
|
|
334
|
+
expect(turns[1].role).toBe("assistant");
|
|
335
|
+
const assistantContent = turns[1].content as {
|
|
336
|
+
parts: { type: string; text?: string; toolCallId?: string; title?: string; output?: unknown }[];
|
|
337
|
+
};
|
|
338
|
+
expect(assistantContent.parts).toHaveLength(2);
|
|
339
|
+
expect(assistantContent.parts[0]).toEqual({
|
|
340
|
+
type: "text",
|
|
341
|
+
text: "Hello! I'm your assistant. How can I help you today?",
|
|
342
|
+
});
|
|
343
|
+
expect(assistantContent.parts[1]).toMatchObject({
|
|
344
|
+
type: "tool",
|
|
345
|
+
toolCallId: "tc-1",
|
|
346
|
+
title: "ListFiles",
|
|
347
|
+
status: "completed",
|
|
348
|
+
output: "index.ts\napp.ts",
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Timestamps should be in order (assistant is +1ms from user)
|
|
352
|
+
expect(turns[1].timestamp).toBeGreaterThanOrEqual(turns[0].timestamp);
|
|
353
|
+
|
|
354
|
+
await eventStore2.close();
|
|
355
|
+
}, 30000);
|
|
356
|
+
|
|
357
|
+
it("should persist multiple prompt rounds across restarts", async () => {
|
|
358
|
+
tmpDir = mkdtempSync(join(tmpdir(), "acp-persist-multi-"));
|
|
359
|
+
|
|
360
|
+
// ── Lifecycle 1: Two prompts ──
|
|
361
|
+
let eventStore1 = await createEventStore({ instanceId, baseDir: tmpDir });
|
|
362
|
+
registerAgent(eventStore1);
|
|
363
|
+
|
|
364
|
+
const agentManager1 = createMockAgentManager([
|
|
365
|
+
{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "First reply" } },
|
|
366
|
+
]);
|
|
367
|
+
|
|
368
|
+
const handler1 = new ACPOverMAPHandler({
|
|
369
|
+
agentManager: agentManager1,
|
|
370
|
+
eventStore: eventStore1,
|
|
371
|
+
taskManager: createMockTaskManager(),
|
|
372
|
+
defaultCwd: "/test/cwd",
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const stream1 = "multi-stream-1";
|
|
376
|
+
await handler1.processRequest(agentId, envelope(stream1, "initialize", {
|
|
377
|
+
protocolVersion: 1, capabilities: {}, clientInfo: { name: "test", version: "1.0" },
|
|
378
|
+
}));
|
|
379
|
+
await handler1.processRequest(agentId, envelope(stream1, "session/new", {
|
|
380
|
+
cwd: "/test", mcpServers: [],
|
|
381
|
+
}));
|
|
382
|
+
|
|
383
|
+
// First prompt
|
|
384
|
+
await handler1.processRequest(agentId, envelope(stream1, "session/prompt", {
|
|
385
|
+
prompt: [{ type: "text", text: "Question 1" }],
|
|
386
|
+
}, sessionId));
|
|
387
|
+
|
|
388
|
+
// Second prompt with different response
|
|
389
|
+
vi.mocked(agentManager1.prompt).mockReturnValue({
|
|
390
|
+
[Symbol.asyncIterator]: async function* () {
|
|
391
|
+
yield { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "Second reply" } };
|
|
392
|
+
},
|
|
393
|
+
} as any);
|
|
394
|
+
|
|
395
|
+
await handler1.processRequest(agentId, envelope(stream1, "session/prompt", {
|
|
396
|
+
prompt: [{ type: "text", text: "Question 2" }],
|
|
397
|
+
}, sessionId));
|
|
398
|
+
|
|
399
|
+
await eventStore1.close();
|
|
400
|
+
|
|
401
|
+
// ── Lifecycle 2: Verify all 4 turns survived ──
|
|
402
|
+
const eventStore2 = await createEventStore({ instanceId, baseDir: tmpDir });
|
|
403
|
+
|
|
404
|
+
const handler2 = new ACPOverMAPHandler({
|
|
405
|
+
agentManager: createMockAgentManager(),
|
|
406
|
+
eventStore: eventStore2,
|
|
407
|
+
taskManager: createMockTaskManager(),
|
|
408
|
+
defaultCwd: "/test/cwd",
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const stream2 = "multi-stream-2";
|
|
412
|
+
await handler2.processRequest(agentId, envelope(stream2, "initialize", {
|
|
413
|
+
protocolVersion: 1, capabilities: {}, clientInfo: { name: "test", version: "1.0" },
|
|
414
|
+
}));
|
|
415
|
+
|
|
416
|
+
const loadResult = await handler2.processRequest(agentId, envelope(stream2, "session/load", {
|
|
417
|
+
sessionId: "_resolve_", cwd: "/test", mcpServers: [], _meta: { agentId },
|
|
418
|
+
}));
|
|
419
|
+
const resolved = (loadResult.acp.result as { sessionId?: string })?.sessionId;
|
|
420
|
+
expect(resolved).toBe(sessionId);
|
|
421
|
+
|
|
422
|
+
const historyResult = await handler2.processRequest(agentId, envelope(stream2, "_macro/getHistory", {
|
|
423
|
+
sessionId: resolved,
|
|
424
|
+
}));
|
|
425
|
+
|
|
426
|
+
const turns = (historyResult.acp.result as { turns: { role: string; content: unknown }[] }).turns;
|
|
427
|
+
expect(turns).toHaveLength(4);
|
|
428
|
+
expect(turns[0].role).toBe("user");
|
|
429
|
+
expect(turns[0].content).toBe("Question 1");
|
|
430
|
+
expect(turns[1].role).toBe("assistant");
|
|
431
|
+
expect(turns[2].role).toBe("user");
|
|
432
|
+
expect(turns[2].content).toBe("Question 2");
|
|
433
|
+
expect(turns[3].role).toBe("assistant");
|
|
434
|
+
|
|
435
|
+
const reply2 = turns[3].content as { parts: { text: string }[] };
|
|
436
|
+
expect(reply2.parts[0].text).toBe("Second reply");
|
|
437
|
+
|
|
438
|
+
await eventStore2.close();
|
|
439
|
+
}, 30000);
|
|
440
|
+
});
|