@grackle-ai/common 0.132.0 → 0.132.1

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.
@@ -0,0 +1,624 @@
1
+ /**
2
+ * Tests for `mapAgentEvent` — AgentEvent → AHP SessionAction mapping.
3
+ *
4
+ * Each test covers one AgentEvent type and verifies:
5
+ * 1. Correct AHP action(s) are produced
6
+ * 2. Mapper context is updated correctly
7
+ * 3. Mapping notes have the right disposition
8
+ */
9
+ import { describe, it, expect } from "vitest";
10
+ import { ActionType } from "@grackle-ai/ahp";
11
+ import { mapAgentEvent } from "./ahp-mapper.js";
12
+ // ─── Helpers ────────────────────────────────────────────────────────
13
+ function makeContext(overrides) {
14
+ return {
15
+ turnId: undefined,
16
+ openToolCalls: [],
17
+ partCounter: 0,
18
+ eventIndex: 0,
19
+ metaAccumulator: {},
20
+ ...overrides,
21
+ };
22
+ }
23
+ function makeEvent(type, overrides) {
24
+ return {
25
+ type,
26
+ content: overrides?.content,
27
+ toolCallId: overrides?.toolCallId,
28
+ turnId: overrides?.turnId,
29
+ diagnostic: overrides?.diagnostic,
30
+ };
31
+ }
32
+ function assertActionType(actions, expectedType, index = 0) {
33
+ const action = actions[index];
34
+ expect(action.type).toBe(expectedType);
35
+ return action;
36
+ }
37
+ // ─── turn_started ──────────────────────────────────────────────────
38
+ describe("turn_started", () => {
39
+ it("maps to SessionTurnStarted with userMessage", () => {
40
+ const context = makeContext();
41
+ const event = makeEvent("turn_started", {
42
+ turnId: "turn-abc",
43
+ content: JSON.stringify({ user_message: "Hello world" }),
44
+ });
45
+ const result = mapAgentEvent(event, 0, context);
46
+ expect(result.actions.length).toBe(1);
47
+ const action = assertActionType(result.actions, ActionType.SessionTurnStarted);
48
+ expect(action.turnId).toBe("turn-abc");
49
+ expect(action.userMessage.text).toBe("Hello world");
50
+ expect(context.turnId).toBe("turn-abc");
51
+ expect(context.openToolCalls).toEqual([]);
52
+ expect(result.note?.disposition).toBe("mapped");
53
+ });
54
+ it("uses generated turnId when none provided", () => {
55
+ const context = makeContext();
56
+ const event = makeEvent("turn_started", {
57
+ content: JSON.stringify({ user_message: "Prompt" }),
58
+ });
59
+ const result = mapAgentEvent(event, 0, context);
60
+ expect(result.actions.length).toBe(1);
61
+ const action = assertActionType(result.actions, ActionType.SessionTurnStarted);
62
+ expect(action.turnId).toBe("turn-0");
63
+ expect(context.turnId).toBe("turn-0");
64
+ });
65
+ it("resets openToolCalls on new turn", () => {
66
+ const context = makeContext({ openToolCalls: ["old-tc-1", "old-tc-2"] });
67
+ const event = makeEvent("turn_started", {
68
+ turnId: "turn-new",
69
+ content: JSON.stringify({ user_message: "New turn" }),
70
+ });
71
+ mapAgentEvent(event, 0, context);
72
+ expect(context.openToolCalls).toEqual([]);
73
+ });
74
+ });
75
+ // ─── turn_complete ─────────────────────────────────────────────────
76
+ describe("turn_complete", () => {
77
+ it("maps to SessionTurnComplete when turn is active", () => {
78
+ const context = makeContext({ turnId: "turn-abc" });
79
+ const event = makeEvent("turn_complete", { turnId: "turn-abc" });
80
+ const result = mapAgentEvent(event, 1, context);
81
+ expect(result.actions.length).toBe(1);
82
+ assertActionType(result.actions, ActionType.SessionTurnComplete);
83
+ expect(context.turnId).toBeUndefined();
84
+ expect(context.openToolCalls).toEqual([]);
85
+ expect(result.note?.disposition).toBe("mapped");
86
+ });
87
+ it("drops when no active turn", () => {
88
+ const context = makeContext();
89
+ const event = makeEvent("turn_complete");
90
+ const result = mapAgentEvent(event, 1, context);
91
+ expect(result.actions.length).toBe(0);
92
+ expect(result.note?.disposition).toBe("dropped");
93
+ });
94
+ });
95
+ // ─── input_needed ──────────────────────────────────────────────────
96
+ describe("input_needed", () => {
97
+ it("drops as advisory only", () => {
98
+ const context = makeContext();
99
+ const event = makeEvent("input_needed");
100
+ const result = mapAgentEvent(event, 2, context);
101
+ expect(result.actions.length).toBe(0);
102
+ expect(result.note?.disposition).toBe("dropped");
103
+ expect(result.note?.detail).toContain("Advisory event");
104
+ });
105
+ });
106
+ // ─── text ──────────────────────────────────────────────────────────
107
+ describe("text", () => {
108
+ it("maps to SessionResponsePart(markdown) with turn", () => {
109
+ const context = makeContext({ turnId: "turn-abc", partCounter: 5 });
110
+ const event = makeEvent("text", { content: "Hello there" });
111
+ const result = mapAgentEvent(event, 3, context);
112
+ expect(result.actions.length).toBe(1);
113
+ const action = assertActionType(result.actions, ActionType.SessionResponsePart);
114
+ expect(action.part.kind).toBe("markdown");
115
+ expect(action.part.id).toBe("part-5");
116
+ expect(action.part.content).toBe("Hello there");
117
+ expect(result.note?.disposition).toBe("mapped");
118
+ });
119
+ it("drops when no active turn", () => {
120
+ const context = makeContext();
121
+ const event = makeEvent("text", { content: "Hello" });
122
+ const result = mapAgentEvent(event, 3, context);
123
+ expect(result.actions.length).toBe(0);
124
+ expect(result.note?.disposition).toBe("dropped");
125
+ });
126
+ });
127
+ // ─── tool_use ──────────────────────────────────────────────────────
128
+ describe("tool_use", () => {
129
+ it("maps to SessionToolCallStart + SessionToolCallReady", () => {
130
+ const context = makeContext({ turnId: "turn-abc", partCounter: 10 });
131
+ const event = makeEvent("tool_use", {
132
+ toolCallId: "tc-xyz",
133
+ content: JSON.stringify({
134
+ tool_name: "read_file",
135
+ display_name: "Read File",
136
+ }),
137
+ });
138
+ const result = mapAgentEvent(event, 4, context);
139
+ expect(result.actions.length).toBe(2);
140
+ const start = assertActionType(result.actions, ActionType.SessionToolCallStart);
141
+ expect(start.toolCallId).toBe("tc-xyz");
142
+ expect(start.toolName).toBe("read_file");
143
+ expect(start.displayName).toBe("Read File");
144
+ const ready = assertActionType(result.actions, ActionType.SessionToolCallReady, 1);
145
+ expect(ready.toolCallId).toBe("tc-xyz");
146
+ expect(ready.confirmed).toBe("not-needed");
147
+ expect(context.openToolCalls).toEqual(["tc-xyz"]);
148
+ expect(result.note ? 1 : 0).toBe(1);
149
+ expect(result.note?.disposition).toBe("mapped");
150
+ });
151
+ it("generates toolCallId when none provided", () => {
152
+ const context = makeContext({ turnId: "turn-abc", partCounter: 10 });
153
+ const event = makeEvent("tool_use", {
154
+ content: JSON.stringify({ tool_name: "shell" }),
155
+ });
156
+ mapAgentEvent(event, 4, context);
157
+ expect(context.openToolCalls).toEqual(["tc-10"]);
158
+ });
159
+ it("drops when no active turn", () => {
160
+ const context = makeContext();
161
+ const event = makeEvent("tool_use", { content: "{}" });
162
+ const result = mapAgentEvent(event, 4, context);
163
+ expect(result.actions.length).toBe(0);
164
+ expect(result.note?.disposition).toBe("dropped");
165
+ });
166
+ });
167
+ // ─── tool_result ───────────────────────────────────────────────────
168
+ describe("tool_result", () => {
169
+ it("pairs by toolCallId and maps to SessionToolCallComplete", () => {
170
+ const context = makeContext({
171
+ turnId: "turn-abc",
172
+ openToolCalls: ["tc-xyz"],
173
+ });
174
+ const event = makeEvent("tool_result", {
175
+ toolCallId: "tc-xyz",
176
+ content: JSON.stringify({ is_ok: true, content: "file contents here" }),
177
+ });
178
+ const result = mapAgentEvent(event, 5, context);
179
+ // Successful result produces: Complete + systemNotification
180
+ expect(result.actions.length).toBe(2);
181
+ const complete = assertActionType(result.actions, ActionType.SessionToolCallComplete);
182
+ expect(complete.toolCallId).toBe("tc-xyz");
183
+ expect(complete.result.success).toBe(true);
184
+ // First-class toolCallId (HR3) is used; matched id is removed from LIFO stack
185
+ expect(context.openToolCalls).toEqual([]);
186
+ expect(result.note?.disposition).toBe("mapped");
187
+ });
188
+ it("pairs by LIFO stack when no toolCallId", () => {
189
+ const context = makeContext({
190
+ turnId: "turn-abc",
191
+ openToolCalls: ["tc-first", "tc-last"],
192
+ });
193
+ const event = makeEvent("tool_result", {
194
+ content: JSON.stringify({ is_ok: true, content: "result" }),
195
+ });
196
+ mapAgentEvent(event, 5, context);
197
+ // Should pop "tc-last" (LIFO)
198
+ expect(context.openToolCalls).toEqual(["tc-first"]);
199
+ });
200
+ it("drops when no matching tool call", () => {
201
+ const context = makeContext({ turnId: "turn-abc", openToolCalls: [] });
202
+ const event = makeEvent("tool_result", { content: "{}" });
203
+ const result = mapAgentEvent(event, 5, context);
204
+ expect(result.actions.length).toBe(0);
205
+ expect(result.note?.disposition).toBe("dropped");
206
+ });
207
+ it("adds system notification for successful result", () => {
208
+ const context = makeContext({
209
+ turnId: "turn-abc",
210
+ openToolCalls: ["tc-xyz"],
211
+ });
212
+ const event = makeEvent("tool_result", {
213
+ toolCallId: "tc-xyz",
214
+ content: "Success: file written",
215
+ });
216
+ const result = mapAgentEvent(event, 5, context);
217
+ expect(result.actions.length).toBe(2);
218
+ expect(result.note).toBeDefined();
219
+ expect(result.note?.disposition).toBe("mapped");
220
+ });
221
+ it("drops when no active turn", () => {
222
+ const context = makeContext({ openToolCalls: ["tc-xyz"] });
223
+ const event = makeEvent("tool_result", { toolCallId: "tc-xyz", content: "{}" });
224
+ const result = mapAgentEvent(event, 5, context);
225
+ expect(result.actions.length).toBe(0);
226
+ expect(result.note?.disposition).toBe("dropped");
227
+ });
228
+ });
229
+ // ─── usage ─────────────────────────────────────────────────────────
230
+ describe("usage", () => {
231
+ it("carries cost_millicents into metaAccumulator", () => {
232
+ const context = makeContext({
233
+ metaAccumulator: { costMillicents: 100 },
234
+ });
235
+ const event = makeEvent("usage", {
236
+ content: JSON.stringify({ cost_millicents: 50 }),
237
+ });
238
+ const result = mapAgentEvent(event, 6, context);
239
+ expect(context.metaAccumulator.costMillicents).toBe(150);
240
+ expect(result.actions.length).toBe(0);
241
+ expect(result.note?.disposition).toBe("carried");
242
+ });
243
+ it("ignores non-finite cost_millicents", () => {
244
+ const context = makeContext();
245
+ const event = makeEvent("usage", {
246
+ content: JSON.stringify({ cost_millicents: NaN }),
247
+ });
248
+ mapAgentEvent(event, 6, context);
249
+ expect(context.metaAccumulator.costMillicents).toBeUndefined();
250
+ });
251
+ it("handles zero cost_millicents without breaking accumulator", () => {
252
+ const context = makeContext({ metaAccumulator: { costMillicents: 100 } });
253
+ const event = makeEvent("usage", {
254
+ content: JSON.stringify({ cost_millicents: 0 }),
255
+ });
256
+ mapAgentEvent(event, 6, context);
257
+ // 0 is a valid (non-null) cost — accumulator should be 100 + 0 = 100
258
+ expect(context.metaAccumulator.costMillicents).toBe(100);
259
+ });
260
+ });
261
+ // ─── error ─────────────────────────────────────────────────────────
262
+ describe("error", () => {
263
+ it("maps to SessionError when in-turn", () => {
264
+ const context = makeContext({ turnId: "turn-abc" });
265
+ const event = makeEvent("error", { content: "Something went wrong" });
266
+ const result = mapAgentEvent(event, 7, context);
267
+ expect(result.actions.length).toBe(1);
268
+ const action = assertActionType(result.actions, ActionType.SessionError);
269
+ expect(action.turnId).toBe("turn-abc");
270
+ expect(action.error.message).toBe("Something went wrong");
271
+ expect(result.note?.disposition).toBe("mapped");
272
+ // context.turnId must be cleared so subsequent events aren't mapped to the defunct turn
273
+ expect(context.turnId).toBeUndefined();
274
+ });
275
+ it("clears context.turnId so subsequent text events are dropped after in-turn error", () => {
276
+ const context = makeContext({ turnId: "turn-abc" });
277
+ mapAgentEvent(makeEvent("error", { content: "Oops" }), 7, context);
278
+ const textResult = mapAgentEvent(makeEvent("text", { content: "After error" }), 8, context);
279
+ expect(textResult.actions.length).toBe(0);
280
+ expect(textResult.note?.disposition).toBe("dropped");
281
+ });
282
+ it("maps to SessionCreationFailed when pre-turn", () => {
283
+ const context = makeContext();
284
+ const event = makeEvent("error", { content: "Init failed" });
285
+ const result = mapAgentEvent(event, 7, context);
286
+ expect(result.actions.length).toBe(1);
287
+ const action = assertActionType(result.actions, ActionType.SessionCreationFailed);
288
+ expect(action.error.message).toBe("Init failed");
289
+ expect(result.note?.disposition).toBe("mapped");
290
+ });
291
+ });
292
+ // ─── status ────────────────────────────────────────────────────────
293
+ describe("status", () => {
294
+ it("maps failed to SessionError when in-turn", () => {
295
+ const context = makeContext({ turnId: "turn-abc" });
296
+ const event = makeEvent("status", { content: "failed" });
297
+ const result = mapAgentEvent(event, 8, context);
298
+ expect(result.actions.length).toBe(1);
299
+ assertActionType(result.actions, ActionType.SessionError);
300
+ expect(context.turnId).toBeUndefined();
301
+ expect(result.note?.disposition).toBe("mapped");
302
+ });
303
+ it("maps failed to SessionCreationFailed when pre-turn", () => {
304
+ const context = makeContext();
305
+ const event = makeEvent("status", { content: "failed" });
306
+ const result = mapAgentEvent(event, 8, context);
307
+ expect(result.actions.length).toBe(1);
308
+ assertActionType(result.actions, ActionType.SessionCreationFailed);
309
+ expect(result.note?.disposition).toBe("mapped");
310
+ });
311
+ it("maps killed to SessionError when in-turn", () => {
312
+ const context = makeContext({ turnId: "turn-abc", openToolCalls: ["tc-1"] });
313
+ const event = makeEvent("status", { content: "killed" });
314
+ const result = mapAgentEvent(event, 9, context);
315
+ expect(result.actions.length).toBe(1);
316
+ assertActionType(result.actions, ActionType.SessionError);
317
+ expect(context.turnId).toBeUndefined();
318
+ expect(context.openToolCalls).toEqual([]);
319
+ });
320
+ it("drops killed when no active turn", () => {
321
+ const context = makeContext();
322
+ const event = makeEvent("status", { content: "killed" });
323
+ const result = mapAgentEvent(event, 9, context);
324
+ expect(result.actions.length).toBe(0);
325
+ expect(result.note?.disposition).toBe("dropped");
326
+ });
327
+ it("drops completed/waiting_input/running", () => {
328
+ for (const status of ["completed", "waiting_input", "running"]) {
329
+ const context = makeContext({ turnId: "turn-abc" });
330
+ const event = makeEvent("status", { content: status });
331
+ const result = mapAgentEvent(event, 10, context);
332
+ expect(result.actions.length).toBe(0);
333
+ expect(result.note?.disposition).toBe("dropped");
334
+ }
335
+ });
336
+ });
337
+ // ─── system ────────────────────────────────────────────────────────
338
+ describe("system", () => {
339
+ it("maps non-diagnostic system to SessionResponsePart(systemNotification)", () => {
340
+ const context = makeContext({ turnId: "turn-abc", partCounter: 20 });
341
+ const event = makeEvent("system", { content: "Subagent completed" });
342
+ const result = mapAgentEvent(event, 11, context);
343
+ expect(result.actions.length).toBe(1);
344
+ const action = assertActionType(result.actions, ActionType.SessionResponsePart);
345
+ expect(action.part.kind).toBe("systemNotification");
346
+ expect(action.part.content).toBe("Subagent completed");
347
+ expect(result.note?.disposition).toBe("mapped");
348
+ });
349
+ it("drops diagnostic system events", () => {
350
+ const context = makeContext();
351
+ const event = makeEvent("system", {
352
+ diagnostic: true,
353
+ content: JSON.stringify({ level: "info", msg: "diagnostic" }),
354
+ });
355
+ const result = mapAgentEvent(event, 12, context);
356
+ expect(result.actions.length).toBe(0);
357
+ expect(result.note?.disposition).toBe("carried");
358
+ expect(result.note?.detail).toContain("diagnostic");
359
+ });
360
+ it("drops non-diagnostic system when no active turn", () => {
361
+ const context = makeContext();
362
+ const event = makeEvent("system", { content: "Subagent completed" });
363
+ const result = mapAgentEvent(event, 11, context);
364
+ expect(result.actions.length).toBe(0);
365
+ expect(result.note?.disposition).toBe("dropped");
366
+ });
367
+ });
368
+ // ─── runtime_session_id ────────────────────────────────────────────
369
+ describe("runtime_session_id", () => {
370
+ it("carries content into metaAccumulator", () => {
371
+ const context = makeContext();
372
+ const event = makeEvent("runtime_session_id", { content: "runtime-abc-123" });
373
+ const result = mapAgentEvent(event, 13, context);
374
+ expect(context.metaAccumulator.runtimeSessionId).toBe("runtime-abc-123");
375
+ expect(result.actions.length).toBe(0);
376
+ expect(result.note?.disposition).toBe("carried");
377
+ });
378
+ it("drops when no content", () => {
379
+ const context = makeContext();
380
+ const event = makeEvent("runtime_session_id");
381
+ const result = mapAgentEvent(event, 13, context);
382
+ expect(result.actions.length).toBe(0);
383
+ expect(result.note?.disposition).toBe("dropped");
384
+ });
385
+ });
386
+ // ─── Unknown event types ──────────────────────────────────────────
387
+ describe("unknown event types", () => {
388
+ it("drops unrecognized event types", () => {
389
+ const context = makeContext();
390
+ const event = makeEvent("unknown_weird_type");
391
+ const result = mapAgentEvent(event, 100, context);
392
+ expect(result.actions.length).toBe(0);
393
+ expect(result.note?.disposition).toBe("dropped");
394
+ expect(result.note?.detail).toContain("Unrecognized event type");
395
+ });
396
+ });
397
+ // ─── Context mutation tests ────────────────────────────────────────
398
+ describe("context mutation", () => {
399
+ it("partCounter increments across multiple text events", () => {
400
+ const context = makeContext({ turnId: "turn-abc", partCounter: 0 });
401
+ const r1 = mapAgentEvent(makeEvent("text", { content: "A" }), 0, context);
402
+ const r2 = mapAgentEvent(makeEvent("text", { content: "B" }), 1, context);
403
+ expect(r1.actions[0]).toMatchObject({ part: { id: "part-0" } });
404
+ expect(r2.actions[0]).toMatchObject({ part: { id: "part-1" } });
405
+ });
406
+ it("metaAccumulator persists across events", () => {
407
+ const context = makeContext({ metaAccumulator: {} });
408
+ mapAgentEvent(makeEvent("usage", { content: JSON.stringify({ cost_millicents: 10 }) }), 0, context);
409
+ mapAgentEvent(makeEvent("usage", { content: JSON.stringify({ cost_millicents: 20 }) }), 1, context);
410
+ expect(context.metaAccumulator.costMillicents).toBe(30);
411
+ });
412
+ });
413
+ // ─── Branch-coverage supplement ────────────────────────────────────
414
+ //
415
+ // The tests above cover the headline mappings. This block fills in the
416
+ // remaining branches (parse fallbacks, ternary "else" arms, missing-field
417
+ // paths) so v8 coverage hits the per-package `branches` floor in
418
+ // rigs/heft-rig/coverage-thresholds.json.
419
+ describe("branch coverage", () => {
420
+ it("turn_started with non-JSON content uses raw content as userMessage", () => {
421
+ const context = makeContext();
422
+ const event = makeEvent("turn_started", { content: "just a plain string", turnId: "t1" });
423
+ const result = mapAgentEvent(event, 0, context);
424
+ const action = result.actions[0];
425
+ expect(action.userMessage.text).toBe("just a plain string");
426
+ });
427
+ it("turn_started with empty content yields empty userMessage text", () => {
428
+ const context = makeContext();
429
+ const event = makeEvent("turn_started", { turnId: "t1" });
430
+ const result = mapAgentEvent(event, 0, context);
431
+ const action = result.actions[0];
432
+ expect(action.userMessage.text).toBe("");
433
+ });
434
+ it("text with no content emits empty markdown part", () => {
435
+ const context = makeContext({ turnId: "t1", partCounter: 0 });
436
+ const event = makeEvent("text");
437
+ const result = mapAgentEvent(event, 0, context);
438
+ const action = result.actions[0];
439
+ expect(action.part.content).toBe("");
440
+ });
441
+ it("tool_use with non-JSON content falls back to unknown_tool / generated invocation", () => {
442
+ const context = makeContext({ turnId: "t1", partCounter: 0 });
443
+ const event = makeEvent("tool_use", { content: "not json" });
444
+ const result = mapAgentEvent(event, 0, context);
445
+ const start = result.actions[0];
446
+ const ready = result.actions[1];
447
+ expect(start.toolName).toBe("unknown_tool");
448
+ expect(start.displayName).toBe("unknown_tool");
449
+ expect(ready.invocationMessage).toBe("Running unknown_tool");
450
+ });
451
+ it("tool_use accepts `name` field when `tool_name` is absent", () => {
452
+ const context = makeContext({ turnId: "t1", partCounter: 0 });
453
+ const event = makeEvent("tool_use", { content: JSON.stringify({ name: "shell" }) });
454
+ const result = mapAgentEvent(event, 0, context);
455
+ const start = result.actions[0];
456
+ expect(start.toolName).toBe("shell");
457
+ });
458
+ it("tool_use falls back to toolName when display_name is absent", () => {
459
+ const context = makeContext({ turnId: "t1", partCounter: 0 });
460
+ const event = makeEvent("tool_use", { content: JSON.stringify({ tool_name: "edit" }) });
461
+ const result = mapAgentEvent(event, 0, context);
462
+ const start = result.actions[0];
463
+ expect(start.displayName).toBe("edit");
464
+ });
465
+ it("tool_use uses provided invocation_message when present", () => {
466
+ const context = makeContext({ turnId: "t1", partCounter: 0 });
467
+ const event = makeEvent("tool_use", {
468
+ content: JSON.stringify({ tool_name: "shell", invocation_message: "running shell `ls`" }),
469
+ });
470
+ const result = mapAgentEvent(event, 0, context);
471
+ const ready = result.actions[1];
472
+ expect(ready.invocationMessage).toBe("running shell `ls`");
473
+ });
474
+ it("tool_result with is_ok:false emits failure result with error", () => {
475
+ const context = makeContext({ turnId: "t1", openToolCalls: ["tc-1"] });
476
+ const event = makeEvent("tool_result", {
477
+ toolCallId: "tc-1",
478
+ content: JSON.stringify({ is_ok: false, past_tense_message: "failed to read" }),
479
+ });
480
+ const result = mapAgentEvent(event, 0, context);
481
+ const action = result.actions[0];
482
+ expect(action.result.success).toBe(false);
483
+ expect(action.result.content).toBeUndefined();
484
+ expect(action.result.error?.message).toBe("failed to read");
485
+ // No system notification on failure
486
+ expect(result.actions.length).toBe(1);
487
+ });
488
+ it("tool_result accepts `success` field when `is_ok` is absent", () => {
489
+ const context = makeContext({ turnId: "t1", openToolCalls: ["tc-1"] });
490
+ const event = makeEvent("tool_result", {
491
+ toolCallId: "tc-1",
492
+ content: JSON.stringify({ success: false, content: "nope" }),
493
+ });
494
+ const result = mapAgentEvent(event, 0, context);
495
+ const action = result.actions[0];
496
+ expect(action.result.success).toBe(false);
497
+ });
498
+ it("tool_result defaults to success=true when neither is_ok nor success present", () => {
499
+ const context = makeContext({ turnId: "t1", openToolCalls: ["tc-1"] });
500
+ const event = makeEvent("tool_result", {
501
+ toolCallId: "tc-1",
502
+ content: JSON.stringify({ content: "ok" }),
503
+ });
504
+ const result = mapAgentEvent(event, 0, context);
505
+ const action = result.actions[0];
506
+ expect(action.result.success).toBe(true);
507
+ });
508
+ it("tool_result with non-JSON content keeps default success=true", () => {
509
+ const context = makeContext({ turnId: "t1", openToolCalls: ["tc-1"] });
510
+ const event = makeEvent("tool_result", { toolCallId: "tc-1", content: "raw text result" });
511
+ const result = mapAgentEvent(event, 0, context);
512
+ const action = result.actions[0];
513
+ expect(action.result.success).toBe(true);
514
+ expect(action.result.pastTenseMessage).toBe("raw text result");
515
+ });
516
+ it("tool_result with no content emits no system notification", () => {
517
+ const context = makeContext({ turnId: "t1", openToolCalls: ["tc-1"] });
518
+ const event = makeEvent("tool_result", { toolCallId: "tc-1" });
519
+ const result = mapAgentEvent(event, 0, context);
520
+ expect(result.actions.length).toBe(1);
521
+ });
522
+ it("tool_result with content > 200 chars truncates the system notification", () => {
523
+ const context = makeContext({ turnId: "t1", openToolCalls: ["tc-1"] });
524
+ const longText = "x".repeat(250);
525
+ const event = makeEvent("tool_result", {
526
+ toolCallId: "tc-1",
527
+ content: JSON.stringify({ is_ok: true, content: longText }),
528
+ });
529
+ const result = mapAgentEvent(event, 0, context);
530
+ const notification = result.actions[1];
531
+ expect(notification.part.content.length).toBe(203); // 200 chars + "..."
532
+ expect(notification.part.content.endsWith("...")).toBe(true);
533
+ });
534
+ it("usage with null cost_millicents leaves accumulator untouched", () => {
535
+ const context = makeContext({ metaAccumulator: { costMillicents: 50 } });
536
+ const event = makeEvent("usage", { content: JSON.stringify({ cost_millicents: null }) });
537
+ mapAgentEvent(event, 0, context);
538
+ expect(context.metaAccumulator.costMillicents).toBe(50);
539
+ });
540
+ it("usage with no parsed content leaves accumulator untouched", () => {
541
+ const context = makeContext({ metaAccumulator: { costMillicents: 50 } });
542
+ const event = makeEvent("usage");
543
+ mapAgentEvent(event, 0, context);
544
+ expect(context.metaAccumulator.costMillicents).toBe(50);
545
+ });
546
+ it("error with no content falls back to 'Unknown error'", () => {
547
+ const context = makeContext({ turnId: "t1" });
548
+ const event = makeEvent("error");
549
+ const result = mapAgentEvent(event, 0, context);
550
+ const action = result.actions[0];
551
+ expect(action.error.message).toBe("Unknown error");
552
+ });
553
+ it("status with unknown content is dropped", () => {
554
+ const context = makeContext({ turnId: "t1" });
555
+ const event = makeEvent("status", { content: "made_up_status" });
556
+ const result = mapAgentEvent(event, 0, context);
557
+ expect(result.actions.length).toBe(0);
558
+ expect(result.note?.disposition).toBe("dropped");
559
+ expect(result.note?.detail).toContain("unrecognized");
560
+ });
561
+ it("status with no content is dropped (empty default branch)", () => {
562
+ const context = makeContext({ turnId: "t1" });
563
+ const event = makeEvent("status");
564
+ const result = mapAgentEvent(event, 0, context);
565
+ expect(result.actions.length).toBe(0);
566
+ expect(result.note?.disposition).toBe("dropped");
567
+ });
568
+ it("status terminated maps to SessionError when in-turn", () => {
569
+ const context = makeContext({ turnId: "t1", openToolCalls: ["tc-1"] });
570
+ const event = makeEvent("status", { content: "terminated" });
571
+ const result = mapAgentEvent(event, 0, context);
572
+ expect(result.actions.length).toBe(1);
573
+ const action = result.actions[0];
574
+ expect(action.type).toBe(ActionType.SessionError);
575
+ expect(context.turnId).toBeUndefined();
576
+ expect(context.openToolCalls).toEqual([]);
577
+ });
578
+ it("status terminated drops when no active turn", () => {
579
+ const context = makeContext();
580
+ const event = makeEvent("status", { content: "terminated" });
581
+ const result = mapAgentEvent(event, 0, context);
582
+ expect(result.actions.length).toBe(0);
583
+ expect(result.note?.disposition).toBe("dropped");
584
+ });
585
+ it("system diagnostic via parsed `span`/`trace`/`level` keys is carried (not mapped)", () => {
586
+ const context = makeContext({ turnId: "t1" });
587
+ const event = makeEvent("system", {
588
+ content: JSON.stringify({ span: "x", trace: "y" }),
589
+ });
590
+ const result = mapAgentEvent(event, 0, context);
591
+ expect(result.actions.length).toBe(0);
592
+ expect(result.note?.disposition).toBe("carried");
593
+ });
594
+ it("system with diagnostic-looking keys but also `text` is treated as user-visible", () => {
595
+ const context = makeContext({ turnId: "t1" });
596
+ const event = makeEvent("system", {
597
+ content: JSON.stringify({ span: "x", text: "user-visible content" }),
598
+ });
599
+ const result = mapAgentEvent(event, 0, context);
600
+ expect(result.actions.length).toBe(1);
601
+ expect(result.note?.disposition).toBe("mapped");
602
+ });
603
+ it("tool_result with explicit toolCallId splices from middle of LIFO stack", () => {
604
+ const context = makeContext({
605
+ turnId: "t1",
606
+ openToolCalls: ["tc-old", "tc-target", "tc-newer"],
607
+ });
608
+ const event = makeEvent("tool_result", {
609
+ toolCallId: "tc-target",
610
+ content: JSON.stringify({ is_ok: true, content: "done" }),
611
+ });
612
+ mapAgentEvent(event, 0, context);
613
+ // Splice removed "tc-target" from the middle, preserving order
614
+ expect(context.openToolCalls).toEqual(["tc-old", "tc-newer"]);
615
+ });
616
+ it("text uses event.turnId when context.turnId is unset", () => {
617
+ const context = makeContext();
618
+ const event = makeEvent("text", { content: "hi", turnId: "from-event" });
619
+ const result = mapAgentEvent(event, 0, context);
620
+ const action = result.actions[0];
621
+ expect(action.turnId).toBe("from-event");
622
+ });
623
+ });
624
+ //# sourceMappingURL=ahp-mapper.test.js.map