@vellumai/assistant 0.5.5 → 0.5.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.
Files changed (102) hide show
  1. package/Dockerfile +3 -4
  2. package/package.json +1 -1
  3. package/src/__tests__/actor-token-service.test.ts +113 -0
  4. package/src/__tests__/config-schema.test.ts +2 -2
  5. package/src/__tests__/context-window-manager.test.ts +78 -0
  6. package/src/__tests__/conversation-title-service.test.ts +30 -1
  7. package/src/__tests__/docker-signing-key-bootstrap.test.ts +207 -0
  8. package/src/__tests__/memory-regressions.test.ts +8 -30
  9. package/src/__tests__/require-fresh-approval.test.ts +4 -0
  10. package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
  11. package/src/__tests__/tool-executor.test.ts +4 -0
  12. package/src/cli/commands/conversations.ts +0 -18
  13. package/src/config/env.ts +8 -2
  14. package/src/config/feature-flag-registry.json +0 -8
  15. package/src/config/schema.ts +0 -12
  16. package/src/config/schemas/memory.ts +0 -4
  17. package/src/config/schemas/platform.ts +1 -1
  18. package/src/config/schemas/security.ts +4 -0
  19. package/src/context/window-manager.ts +53 -2
  20. package/src/daemon/config-watcher.ts +1 -4
  21. package/src/daemon/conversation-agent-loop.ts +0 -60
  22. package/src/daemon/conversation-memory.ts +0 -117
  23. package/src/daemon/conversation-runtime-assembly.ts +0 -2
  24. package/src/daemon/handlers/conversations.ts +0 -11
  25. package/src/daemon/lifecycle.ts +3 -46
  26. package/src/followups/followup-store.ts +5 -2
  27. package/src/memory/conversation-crud.ts +0 -236
  28. package/src/memory/conversation-title-service.ts +26 -10
  29. package/src/memory/db-init.ts +5 -13
  30. package/src/memory/indexer.ts +15 -106
  31. package/src/memory/job-handlers/embedding.ts +0 -79
  32. package/src/memory/job-utils.ts +1 -1
  33. package/src/memory/jobs-store.ts +0 -8
  34. package/src/memory/jobs-worker.ts +0 -20
  35. package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
  36. package/src/memory/migrations/index.ts +1 -3
  37. package/src/memory/qdrant-client.ts +4 -6
  38. package/src/memory/schema/conversations.ts +0 -3
  39. package/src/memory/schema/index.ts +0 -2
  40. package/src/messaging/draft-store.ts +2 -2
  41. package/src/permissions/defaults.ts +3 -3
  42. package/src/permissions/trust-client.ts +2 -13
  43. package/src/permissions/trust-store.ts +8 -3
  44. package/src/runtime/auth/route-policy.ts +14 -0
  45. package/src/runtime/auth/token-service.ts +133 -0
  46. package/src/runtime/http-server.ts +2 -0
  47. package/src/runtime/routes/conversation-management-routes.ts +0 -36
  48. package/src/runtime/routes/conversation-query-routes.ts +44 -2
  49. package/src/runtime/routes/conversation-routes.ts +2 -1
  50. package/src/runtime/routes/memory-item-routes.test.ts +221 -3
  51. package/src/runtime/routes/memory-item-routes.ts +124 -2
  52. package/src/runtime/routes/upgrade-broadcast-routes.ts +151 -0
  53. package/src/schedule/schedule-store.ts +0 -21
  54. package/src/skills/inline-command-render.ts +5 -1
  55. package/src/skills/inline-command-runner.ts +30 -2
  56. package/src/tools/memory/handlers.ts +1 -129
  57. package/src/tools/permission-checker.ts +18 -0
  58. package/src/tools/skills/load.ts +9 -2
  59. package/src/util/platform.ts +5 -5
  60. package/src/util/xml.ts +8 -0
  61. package/src/workspace/heartbeat-service.ts +5 -24
  62. package/src/__tests__/archive-recall.test.ts +0 -560
  63. package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
  64. package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
  65. package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
  66. package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
  67. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
  68. package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
  69. package/src/__tests__/memory-brief-time.test.ts +0 -285
  70. package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
  71. package/src/__tests__/memory-chunk-archive.test.ts +0 -400
  72. package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
  73. package/src/__tests__/memory-episode-archive.test.ts +0 -370
  74. package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
  75. package/src/__tests__/memory-observation-archive.test.ts +0 -375
  76. package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
  77. package/src/__tests__/memory-reducer-job.test.ts +0 -538
  78. package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
  79. package/src/__tests__/memory-reducer-store.test.ts +0 -728
  80. package/src/__tests__/memory-reducer-types.test.ts +0 -707
  81. package/src/__tests__/memory-reducer.test.ts +0 -704
  82. package/src/__tests__/memory-simplified-config.test.ts +0 -281
  83. package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
  84. package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
  85. package/src/config/schemas/memory-simplified.ts +0 -101
  86. package/src/memory/archive-recall.ts +0 -516
  87. package/src/memory/archive-store.ts +0 -400
  88. package/src/memory/brief-formatting.ts +0 -33
  89. package/src/memory/brief-open-loops.ts +0 -266
  90. package/src/memory/brief-time.ts +0 -162
  91. package/src/memory/brief.ts +0 -75
  92. package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
  93. package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
  94. package/src/memory/migrations/185-memory-brief-state.ts +0 -52
  95. package/src/memory/migrations/186-memory-archive.ts +0 -109
  96. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
  97. package/src/memory/reducer-scheduler.ts +0 -242
  98. package/src/memory/reducer-store.ts +0 -271
  99. package/src/memory/reducer-types.ts +0 -106
  100. package/src/memory/reducer.ts +0 -467
  101. package/src/memory/schema/memory-archive.ts +0 -121
  102. package/src/memory/schema/memory-brief.ts +0 -55
@@ -1,707 +0,0 @@
1
- import { describe, expect, mock, test } from "bun:test";
2
-
3
- // ---------------------------------------------------------------------------
4
- // Mocks — declared before imports that depend on logger
5
- // ---------------------------------------------------------------------------
6
-
7
- function makeLoggerStub(): Record<string, unknown> {
8
- const stub: Record<string, unknown> = {};
9
- for (const m of [
10
- "info",
11
- "warn",
12
- "error",
13
- "debug",
14
- "trace",
15
- "fatal",
16
- "silent",
17
- "child",
18
- ]) {
19
- stub[m] = m === "child" ? () => makeLoggerStub() : () => {};
20
- }
21
- return stub;
22
- }
23
-
24
- mock.module("../util/logger.js", () => ({
25
- getLogger: () => makeLoggerStub(),
26
- }));
27
-
28
- import { parseReducerOutput } from "../memory/reducer.js";
29
- import { EMPTY_REDUCER_RESULT } from "../memory/reducer-types.js";
30
-
31
- // ---------------------------------------------------------------------------
32
- // Helpers
33
- // ---------------------------------------------------------------------------
34
-
35
- /** Wrap a JS object through JSON.stringify so parseReducerOutput can consume it. */
36
- function toRaw(obj: unknown): string {
37
- return JSON.stringify(obj);
38
- }
39
-
40
- // ---------------------------------------------------------------------------
41
- // Tests: EMPTY_REDUCER_RESULT
42
- // ---------------------------------------------------------------------------
43
-
44
- describe("EMPTY_REDUCER_RESULT", () => {
45
- test("has all four arrays empty", () => {
46
- expect(EMPTY_REDUCER_RESULT.timeContexts).toEqual([]);
47
- expect(EMPTY_REDUCER_RESULT.openLoops).toEqual([]);
48
- expect(EMPTY_REDUCER_RESULT.archiveObservations).toEqual([]);
49
- expect(EMPTY_REDUCER_RESULT.archiveEpisodes).toEqual([]);
50
- });
51
-
52
- test("is frozen (immutable)", () => {
53
- expect(Object.isFrozen(EMPTY_REDUCER_RESULT)).toBe(true);
54
- });
55
- });
56
-
57
- // ---------------------------------------------------------------------------
58
- // Tests: parseReducerOutput — invalid inputs
59
- // ---------------------------------------------------------------------------
60
-
61
- describe("parseReducerOutput — invalid inputs", () => {
62
- test("returns empty result for non-JSON string", () => {
63
- expect(parseReducerOutput("not json at all")).toBe(EMPTY_REDUCER_RESULT);
64
- });
65
-
66
- test("returns empty result for empty string", () => {
67
- expect(parseReducerOutput("")).toBe(EMPTY_REDUCER_RESULT);
68
- });
69
-
70
- test("returns empty result for JSON array", () => {
71
- expect(parseReducerOutput("[]")).toBe(EMPTY_REDUCER_RESULT);
72
- });
73
-
74
- test("returns empty result for JSON null", () => {
75
- expect(parseReducerOutput("null")).toBe(EMPTY_REDUCER_RESULT);
76
- });
77
-
78
- test("returns empty result for JSON number", () => {
79
- expect(parseReducerOutput("42")).toBe(EMPTY_REDUCER_RESULT);
80
- });
81
-
82
- test("returns empty result for JSON string", () => {
83
- expect(parseReducerOutput('"hello"')).toBe(EMPTY_REDUCER_RESULT);
84
- });
85
-
86
- test("returns empty result for object with no recognized arrays", () => {
87
- expect(parseReducerOutput(toRaw({ foo: "bar" }))).toEqual({
88
- timeContexts: [],
89
- openLoops: [],
90
- archiveObservations: [],
91
- archiveEpisodes: [],
92
- });
93
- });
94
-
95
- test("returns empty result when all recognized keys are not arrays", () => {
96
- expect(
97
- parseReducerOutput(
98
- toRaw({
99
- timeContexts: "not an array",
100
- openLoops: 42,
101
- archiveObservations: null,
102
- archiveEpisodes: true,
103
- }),
104
- ),
105
- ).toEqual({
106
- timeContexts: [],
107
- openLoops: [],
108
- archiveObservations: [],
109
- archiveEpisodes: [],
110
- });
111
- });
112
- });
113
-
114
- // ---------------------------------------------------------------------------
115
- // Tests: parseReducerOutput — valid full output
116
- // ---------------------------------------------------------------------------
117
-
118
- describe("parseReducerOutput — valid full output", () => {
119
- test("parses a complete valid reducer output", () => {
120
- const raw: Record<string, unknown> = {
121
- timeContexts: [
122
- {
123
- action: "create",
124
- summary: "User traveling next week",
125
- source: "conversation",
126
- activeFrom: 1700000000000,
127
- activeUntil: 1700604800000,
128
- },
129
- {
130
- action: "update",
131
- id: "tc-1",
132
- summary: "Updated travel dates",
133
- },
134
- {
135
- action: "resolve",
136
- id: "tc-2",
137
- },
138
- ],
139
- openLoops: [
140
- {
141
- action: "create",
142
- summary: "Follow up with Bob",
143
- source: "conversation",
144
- dueAt: 1700172800000,
145
- },
146
- {
147
- action: "update",
148
- id: "ol-1",
149
- summary: "Bob replied — need to review",
150
- },
151
- {
152
- action: "resolve",
153
- id: "ol-2",
154
- status: "resolved",
155
- },
156
- ],
157
- archiveObservations: [
158
- {
159
- content: "User prefers dark mode",
160
- role: "user",
161
- modality: "text",
162
- source: "vellum",
163
- },
164
- ],
165
- archiveEpisodes: [
166
- {
167
- title: "Setup discussion",
168
- summary: "User configured their workspace preferences",
169
- source: "vellum",
170
- },
171
- ],
172
- };
173
-
174
- const result = parseReducerOutput(toRaw(raw));
175
-
176
- expect(result.timeContexts).toHaveLength(3);
177
- expect(result.timeContexts[0]).toEqual({
178
- action: "create",
179
- summary: "User traveling next week",
180
- source: "conversation",
181
- activeFrom: 1700000000000,
182
- activeUntil: 1700604800000,
183
- });
184
- expect(result.timeContexts[1]).toEqual({
185
- action: "update",
186
- id: "tc-1",
187
- summary: "Updated travel dates",
188
- });
189
- expect(result.timeContexts[2]).toEqual({
190
- action: "resolve",
191
- id: "tc-2",
192
- });
193
-
194
- expect(result.openLoops).toHaveLength(3);
195
- expect(result.openLoops[0]).toEqual({
196
- action: "create",
197
- summary: "Follow up with Bob",
198
- source: "conversation",
199
- dueAt: 1700172800000,
200
- });
201
- expect(result.openLoops[1]).toEqual({
202
- action: "update",
203
- id: "ol-1",
204
- summary: "Bob replied — need to review",
205
- });
206
- expect(result.openLoops[2]).toEqual({
207
- action: "resolve",
208
- id: "ol-2",
209
- status: "resolved",
210
- });
211
-
212
- expect(result.archiveObservations).toHaveLength(1);
213
- expect(result.archiveObservations[0]).toEqual({
214
- content: "User prefers dark mode",
215
- role: "user",
216
- modality: "text",
217
- source: "vellum",
218
- });
219
-
220
- expect(result.archiveEpisodes).toHaveLength(1);
221
- expect(result.archiveEpisodes[0]).toEqual({
222
- title: "Setup discussion",
223
- summary: "User configured their workspace preferences",
224
- source: "vellum",
225
- });
226
- });
227
- });
228
-
229
- // ---------------------------------------------------------------------------
230
- // Tests: parseReducerOutput — partial outputs
231
- // ---------------------------------------------------------------------------
232
-
233
- describe("parseReducerOutput — partial outputs", () => {
234
- test("accepts output with only timeContexts", () => {
235
- const result = parseReducerOutput(
236
- toRaw({
237
- timeContexts: [
238
- {
239
- action: "create",
240
- summary: "Deadline Friday",
241
- source: "conversation",
242
- activeFrom: 1700000000000,
243
- activeUntil: 1700604800000,
244
- },
245
- ],
246
- }),
247
- );
248
- expect(result.timeContexts).toHaveLength(1);
249
- expect(result.openLoops).toHaveLength(0);
250
- expect(result.archiveObservations).toHaveLength(0);
251
- expect(result.archiveEpisodes).toHaveLength(0);
252
- });
253
-
254
- test("accepts output with only openLoops", () => {
255
- const result = parseReducerOutput(
256
- toRaw({
257
- openLoops: [
258
- {
259
- action: "create",
260
- summary: "Need to reply to Alice",
261
- source: "conversation",
262
- },
263
- ],
264
- }),
265
- );
266
- expect(result.openLoops).toHaveLength(1);
267
- expect(result.timeContexts).toHaveLength(0);
268
- });
269
-
270
- test("accepts output with only archiveObservations", () => {
271
- const result = parseReducerOutput(
272
- toRaw({
273
- archiveObservations: [{ content: "User likes coffee", role: "user" }],
274
- }),
275
- );
276
- expect(result.archiveObservations).toHaveLength(1);
277
- expect(result.archiveObservations[0]).toEqual({
278
- content: "User likes coffee",
279
- role: "user",
280
- });
281
- });
282
-
283
- test("accepts output with only archiveEpisodes", () => {
284
- const result = parseReducerOutput(
285
- toRaw({
286
- archiveEpisodes: [{ title: "Onboarding", summary: "First session" }],
287
- }),
288
- );
289
- expect(result.archiveEpisodes).toHaveLength(1);
290
- expect(result.archiveEpisodes[0]).toEqual({
291
- title: "Onboarding",
292
- summary: "First session",
293
- });
294
- });
295
-
296
- test("accepts empty arrays for all keys", () => {
297
- const result = parseReducerOutput(
298
- toRaw({
299
- timeContexts: [],
300
- openLoops: [],
301
- archiveObservations: [],
302
- archiveEpisodes: [],
303
- }),
304
- );
305
- expect(result.timeContexts).toHaveLength(0);
306
- expect(result.openLoops).toHaveLength(0);
307
- expect(result.archiveObservations).toHaveLength(0);
308
- expect(result.archiveEpisodes).toHaveLength(0);
309
- // Should be a fresh object, not EMPTY_REDUCER_RESULT reference
310
- expect(result).not.toBe(EMPTY_REDUCER_RESULT);
311
- });
312
-
313
- test("drops invalid individual operations while keeping valid ones", () => {
314
- const result = parseReducerOutput(
315
- toRaw({
316
- timeContexts: [
317
- // valid create
318
- {
319
- action: "create",
320
- summary: "Valid",
321
- source: "conversation",
322
- activeFrom: 1000,
323
- activeUntil: 2000,
324
- },
325
- // invalid — missing summary
326
- {
327
- action: "create",
328
- source: "conversation",
329
- activeFrom: 1000,
330
- activeUntil: 2000,
331
- },
332
- // invalid — unknown action
333
- { action: "delete", id: "tc-1" },
334
- // valid resolve
335
- { action: "resolve", id: "tc-3" },
336
- ],
337
- openLoops: [
338
- // valid create
339
- {
340
- action: "create",
341
- summary: "Valid loop",
342
- source: "conversation",
343
- },
344
- // invalid resolve — missing status
345
- { action: "resolve", id: "ol-1" },
346
- // invalid resolve — invalid status
347
- { action: "resolve", id: "ol-2", status: "cancelled" },
348
- // null entry
349
- null,
350
- ],
351
- }),
352
- );
353
-
354
- expect(result.timeContexts).toHaveLength(2);
355
- expect(result.timeContexts[0].action).toBe("create");
356
- expect(result.timeContexts[1].action).toBe("resolve");
357
-
358
- expect(result.openLoops).toHaveLength(1);
359
- expect(result.openLoops[0].action).toBe("create");
360
- });
361
- });
362
-
363
- // ---------------------------------------------------------------------------
364
- // Tests: parseReducerOutput — time-context validation edge cases
365
- // ---------------------------------------------------------------------------
366
-
367
- describe("parseReducerOutput — time-context validation", () => {
368
- test("rejects create with empty summary", () => {
369
- const result = parseReducerOutput(
370
- toRaw({
371
- timeContexts: [
372
- {
373
- action: "create",
374
- summary: "",
375
- source: "conversation",
376
- activeFrom: 1000,
377
- activeUntil: 2000,
378
- },
379
- ],
380
- }),
381
- );
382
- expect(result.timeContexts).toHaveLength(0);
383
- });
384
-
385
- test("rejects create with non-string summary", () => {
386
- const result = parseReducerOutput(
387
- toRaw({
388
- timeContexts: [
389
- {
390
- action: "create",
391
- summary: 123,
392
- source: "conversation",
393
- activeFrom: 1000,
394
- activeUntil: 2000,
395
- },
396
- ],
397
- }),
398
- );
399
- expect(result.timeContexts).toHaveLength(0);
400
- });
401
-
402
- test("rejects create with negative activeUntil", () => {
403
- const result = parseReducerOutput(
404
- toRaw({
405
- timeContexts: [
406
- {
407
- action: "create",
408
- summary: "Test",
409
- source: "conversation",
410
- activeFrom: 1000,
411
- activeUntil: -1,
412
- },
413
- ],
414
- }),
415
- );
416
- expect(result.timeContexts).toHaveLength(0);
417
- });
418
-
419
- test("accepts create with activeFrom of 0 (epoch)", () => {
420
- const result = parseReducerOutput(
421
- toRaw({
422
- timeContexts: [
423
- {
424
- action: "create",
425
- summary: "From epoch",
426
- source: "conversation",
427
- activeFrom: 0,
428
- activeUntil: 1000,
429
- },
430
- ],
431
- }),
432
- );
433
- expect(result.timeContexts).toHaveLength(1);
434
- });
435
-
436
- test("rejects update with no updatable fields", () => {
437
- const result = parseReducerOutput(
438
- toRaw({
439
- timeContexts: [{ action: "update", id: "tc-1" }],
440
- }),
441
- );
442
- expect(result.timeContexts).toHaveLength(0);
443
- });
444
-
445
- test("accepts update with only summary", () => {
446
- const result = parseReducerOutput(
447
- toRaw({
448
- timeContexts: [
449
- { action: "update", id: "tc-1", summary: "New summary" },
450
- ],
451
- }),
452
- );
453
- expect(result.timeContexts).toHaveLength(1);
454
- const op = result.timeContexts[0];
455
- expect(op.action).toBe("update");
456
- if (op.action === "update") {
457
- expect(op.summary).toBe("New summary");
458
- expect(op.activeFrom).toBeUndefined();
459
- expect(op.activeUntil).toBeUndefined();
460
- }
461
- });
462
-
463
- test("rejects resolve with missing id", () => {
464
- const result = parseReducerOutput(
465
- toRaw({
466
- timeContexts: [{ action: "resolve" }],
467
- }),
468
- );
469
- expect(result.timeContexts).toHaveLength(0);
470
- });
471
- });
472
-
473
- // ---------------------------------------------------------------------------
474
- // Tests: parseReducerOutput — open-loop validation edge cases
475
- // ---------------------------------------------------------------------------
476
-
477
- describe("parseReducerOutput — open-loop validation", () => {
478
- test("accepts create without optional dueAt", () => {
479
- const result = parseReducerOutput(
480
- toRaw({
481
- openLoops: [
482
- {
483
- action: "create",
484
- summary: "No deadline",
485
- source: "conversation",
486
- },
487
- ],
488
- }),
489
- );
490
- expect(result.openLoops).toHaveLength(1);
491
- if (result.openLoops[0].action === "create") {
492
- expect(result.openLoops[0].dueAt).toBeUndefined();
493
- }
494
- });
495
-
496
- test("accepts create with dueAt", () => {
497
- const result = parseReducerOutput(
498
- toRaw({
499
- openLoops: [
500
- {
501
- action: "create",
502
- summary: "With deadline",
503
- source: "conversation",
504
- dueAt: 1700000000000,
505
- },
506
- ],
507
- }),
508
- );
509
- expect(result.openLoops).toHaveLength(1);
510
- if (result.openLoops[0].action === "create") {
511
- expect(result.openLoops[0].dueAt).toBe(1700000000000);
512
- }
513
- });
514
-
515
- test("rejects create with missing source", () => {
516
- const result = parseReducerOutput(
517
- toRaw({
518
- openLoops: [{ action: "create", summary: "Missing source" }],
519
- }),
520
- );
521
- expect(result.openLoops).toHaveLength(0);
522
- });
523
-
524
- test("rejects update with no updatable fields", () => {
525
- const result = parseReducerOutput(
526
- toRaw({
527
- openLoops: [{ action: "update", id: "ol-1" }],
528
- }),
529
- );
530
- expect(result.openLoops).toHaveLength(0);
531
- });
532
-
533
- test("accepts update with only dueAt", () => {
534
- const result = parseReducerOutput(
535
- toRaw({
536
- openLoops: [{ action: "update", id: "ol-1", dueAt: 1700000000000 }],
537
- }),
538
- );
539
- expect(result.openLoops).toHaveLength(1);
540
- });
541
-
542
- test("accepts resolve with status 'expired'", () => {
543
- const result = parseReducerOutput(
544
- toRaw({
545
- openLoops: [{ action: "resolve", id: "ol-1", status: "expired" }],
546
- }),
547
- );
548
- expect(result.openLoops).toHaveLength(1);
549
- if (result.openLoops[0].action === "resolve") {
550
- expect(result.openLoops[0].status).toBe("expired");
551
- }
552
- });
553
-
554
- test("rejects resolve with invalid status value", () => {
555
- const result = parseReducerOutput(
556
- toRaw({
557
- openLoops: [{ action: "resolve", id: "ol-1", status: "deleted" }],
558
- }),
559
- );
560
- expect(result.openLoops).toHaveLength(0);
561
- });
562
- });
563
-
564
- // ---------------------------------------------------------------------------
565
- // Tests: parseReducerOutput — archive observation validation
566
- // ---------------------------------------------------------------------------
567
-
568
- describe("parseReducerOutput — archive observation validation", () => {
569
- test("rejects observation with missing content", () => {
570
- const result = parseReducerOutput(
571
- toRaw({
572
- archiveObservations: [{ role: "user" }],
573
- }),
574
- );
575
- expect(result.archiveObservations).toHaveLength(0);
576
- });
577
-
578
- test("rejects observation with missing role", () => {
579
- const result = parseReducerOutput(
580
- toRaw({
581
- archiveObservations: [{ content: "Something" }],
582
- }),
583
- );
584
- expect(result.archiveObservations).toHaveLength(0);
585
- });
586
-
587
- test("includes optional modality and source when present", () => {
588
- const result = parseReducerOutput(
589
- toRaw({
590
- archiveObservations: [
591
- {
592
- content: "User likes tea",
593
- role: "user",
594
- modality: "voice",
595
- source: "phone",
596
- },
597
- ],
598
- }),
599
- );
600
- expect(result.archiveObservations).toHaveLength(1);
601
- expect(result.archiveObservations[0].modality).toBe("voice");
602
- expect(result.archiveObservations[0].source).toBe("phone");
603
- });
604
-
605
- test("omits modality and source when not valid strings", () => {
606
- const result = parseReducerOutput(
607
- toRaw({
608
- archiveObservations: [
609
- {
610
- content: "Fact",
611
- role: "user",
612
- modality: 123,
613
- source: false,
614
- },
615
- ],
616
- }),
617
- );
618
- expect(result.archiveObservations).toHaveLength(1);
619
- expect(result.archiveObservations[0].modality).toBeUndefined();
620
- expect(result.archiveObservations[0].source).toBeUndefined();
621
- });
622
- });
623
-
624
- // ---------------------------------------------------------------------------
625
- // Tests: parseReducerOutput — archive episode validation
626
- // ---------------------------------------------------------------------------
627
-
628
- describe("parseReducerOutput — archive episode validation", () => {
629
- test("rejects episode with missing title", () => {
630
- const result = parseReducerOutput(
631
- toRaw({
632
- archiveEpisodes: [{ summary: "Some summary" }],
633
- }),
634
- );
635
- expect(result.archiveEpisodes).toHaveLength(0);
636
- });
637
-
638
- test("rejects episode with missing summary", () => {
639
- const result = parseReducerOutput(
640
- toRaw({
641
- archiveEpisodes: [{ title: "Some title" }],
642
- }),
643
- );
644
- expect(result.archiveEpisodes).toHaveLength(0);
645
- });
646
-
647
- test("includes optional source when present", () => {
648
- const result = parseReducerOutput(
649
- toRaw({
650
- archiveEpisodes: [
651
- { title: "Chat", summary: "A chat happened", source: "telegram" },
652
- ],
653
- }),
654
- );
655
- expect(result.archiveEpisodes).toHaveLength(1);
656
- expect(result.archiveEpisodes[0].source).toBe("telegram");
657
- });
658
-
659
- test("omits source when not a valid string", () => {
660
- const result = parseReducerOutput(
661
- toRaw({
662
- archiveEpisodes: [
663
- { title: "Chat", summary: "A chat happened", source: 42 },
664
- ],
665
- }),
666
- );
667
- expect(result.archiveEpisodes).toHaveLength(1);
668
- expect(result.archiveEpisodes[0].source).toBeUndefined();
669
- });
670
- });
671
-
672
- // ---------------------------------------------------------------------------
673
- // Tests: parseReducerOutput — extra/unknown keys are tolerated
674
- // ---------------------------------------------------------------------------
675
-
676
- describe("parseReducerOutput — tolerates extra keys", () => {
677
- test("ignores unknown top-level keys", () => {
678
- const result = parseReducerOutput(
679
- toRaw({
680
- timeContexts: [],
681
- unknownKey: "whatever",
682
- anotherOne: [1, 2, 3],
683
- }),
684
- );
685
- expect(result).not.toBe(EMPTY_REDUCER_RESULT);
686
- expect(result.timeContexts).toHaveLength(0);
687
- });
688
-
689
- test("ignores extra fields on individual operations", () => {
690
- const result = parseReducerOutput(
691
- toRaw({
692
- openLoops: [
693
- {
694
- action: "create",
695
- summary: "Valid",
696
- source: "conversation",
697
- extraField: true,
698
- nested: { deep: 1 },
699
- },
700
- ],
701
- }),
702
- );
703
- expect(result.openLoops).toHaveLength(1);
704
- // Extra fields should NOT be present on the validated result
705
- expect((result.openLoops[0] as any).extraField).toBeUndefined();
706
- });
707
- });