@vellumai/assistant 0.5.4 → 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 (151) hide show
  1. package/Dockerfile +17 -27
  2. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -0
  3. package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +42 -0
  4. package/package.json +1 -1
  5. package/src/__tests__/actor-token-service.test.ts +113 -0
  6. package/src/__tests__/config-schema.test.ts +2 -2
  7. package/src/__tests__/context-window-manager.test.ts +78 -0
  8. package/src/__tests__/conversation-title-service.test.ts +30 -1
  9. package/src/__tests__/credential-security-invariants.test.ts +2 -0
  10. package/src/__tests__/docker-signing-key-bootstrap.test.ts +207 -0
  11. package/src/__tests__/memory-regressions.test.ts +8 -30
  12. package/src/__tests__/openai-whisper.test.ts +93 -0
  13. package/src/__tests__/require-fresh-approval.test.ts +4 -0
  14. package/src/__tests__/slack-messaging-token-resolution.test.ts +319 -0
  15. package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
  16. package/src/__tests__/tool-executor.test.ts +4 -0
  17. package/src/__tests__/volume-security-guard.test.ts +155 -0
  18. package/src/cli/commands/conversations.ts +0 -18
  19. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -0
  20. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +16 -37
  21. package/src/config/env-registry.ts +9 -0
  22. package/src/config/env.ts +8 -2
  23. package/src/config/feature-flag-registry.json +8 -8
  24. package/src/config/schema.ts +0 -12
  25. package/src/config/schemas/memory.ts +0 -4
  26. package/src/config/schemas/platform.ts +1 -1
  27. package/src/config/schemas/security.ts +4 -0
  28. package/src/context/window-manager.ts +53 -2
  29. package/src/credential-execution/managed-catalog.ts +5 -15
  30. package/src/daemon/conversation-agent-loop.ts +0 -60
  31. package/src/daemon/conversation-memory.ts +0 -117
  32. package/src/daemon/conversation-runtime-assembly.ts +0 -2
  33. package/src/daemon/daemon-control.ts +7 -0
  34. package/src/daemon/handlers/conversations.ts +0 -11
  35. package/src/daemon/lifecycle.ts +10 -47
  36. package/src/daemon/providers-setup.ts +2 -1
  37. package/src/followups/followup-store.ts +5 -2
  38. package/src/hooks/manager.ts +7 -0
  39. package/src/instrument.ts +33 -1
  40. package/src/memory/conversation-crud.ts +0 -236
  41. package/src/memory/conversation-title-service.ts +26 -10
  42. package/src/memory/db-init.ts +5 -13
  43. package/src/memory/embedding-local.ts +11 -5
  44. package/src/memory/indexer.ts +15 -106
  45. package/src/memory/job-handlers/conversation-starters.ts +24 -36
  46. package/src/memory/job-handlers/embedding.ts +0 -79
  47. package/src/memory/job-utils.ts +1 -1
  48. package/src/memory/jobs-store.ts +0 -8
  49. package/src/memory/jobs-worker.ts +0 -20
  50. package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
  51. package/src/memory/migrations/index.ts +1 -3
  52. package/src/memory/qdrant-client.ts +4 -6
  53. package/src/memory/schema/conversations.ts +0 -3
  54. package/src/memory/schema/index.ts +0 -2
  55. package/src/messaging/draft-store.ts +2 -2
  56. package/src/messaging/provider.ts +9 -0
  57. package/src/messaging/providers/slack/adapter.ts +29 -2
  58. package/src/oauth/connection-resolver.test.ts +22 -18
  59. package/src/oauth/connection-resolver.ts +92 -7
  60. package/src/oauth/platform-connection.test.ts +78 -69
  61. package/src/oauth/platform-connection.ts +12 -19
  62. package/src/permissions/defaults.ts +3 -3
  63. package/src/permissions/trust-client.ts +332 -0
  64. package/src/permissions/trust-store-interface.ts +105 -0
  65. package/src/permissions/trust-store.ts +531 -39
  66. package/src/platform/client.test.ts +148 -0
  67. package/src/platform/client.ts +71 -0
  68. package/src/providers/speech-to-text/openai-whisper.test.ts +190 -0
  69. package/src/providers/speech-to-text/openai-whisper.ts +68 -0
  70. package/src/providers/speech-to-text/resolve.ts +9 -0
  71. package/src/providers/speech-to-text/types.ts +17 -0
  72. package/src/runtime/auth/route-policy.ts +14 -0
  73. package/src/runtime/auth/token-service.ts +133 -0
  74. package/src/runtime/http-server.ts +4 -2
  75. package/src/runtime/routes/conversation-management-routes.ts +0 -36
  76. package/src/runtime/routes/conversation-query-routes.ts +44 -2
  77. package/src/runtime/routes/conversation-routes.ts +2 -1
  78. package/src/runtime/routes/inbound-message-handler.ts +27 -3
  79. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -1
  80. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +287 -0
  81. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +122 -0
  82. package/src/runtime/routes/log-export-routes.ts +1 -0
  83. package/src/runtime/routes/memory-item-routes.test.ts +221 -3
  84. package/src/runtime/routes/memory-item-routes.ts +124 -2
  85. package/src/runtime/routes/secret-routes.ts +4 -1
  86. package/src/runtime/routes/upgrade-broadcast-routes.ts +151 -0
  87. package/src/schedule/schedule-store.ts +0 -21
  88. package/src/security/ces-credential-client.ts +173 -0
  89. package/src/security/secure-keys.ts +65 -22
  90. package/src/signals/bash.ts +3 -0
  91. package/src/signals/cancel.ts +3 -0
  92. package/src/signals/confirm.ts +3 -0
  93. package/src/signals/conversation-undo.ts +3 -0
  94. package/src/signals/event-stream.ts +7 -0
  95. package/src/signals/shotgun.ts +3 -0
  96. package/src/signals/trust-rule.ts +3 -0
  97. package/src/skills/inline-command-render.ts +5 -1
  98. package/src/skills/inline-command-runner.ts +30 -2
  99. package/src/telemetry/usage-telemetry-reporter.test.ts +23 -36
  100. package/src/telemetry/usage-telemetry-reporter.ts +21 -19
  101. package/src/tools/memory/handlers.ts +1 -129
  102. package/src/tools/permission-checker.ts +18 -0
  103. package/src/tools/skills/load.ts +9 -2
  104. package/src/util/device-id.ts +70 -7
  105. package/src/util/logger.ts +35 -9
  106. package/src/util/platform.ts +29 -5
  107. package/src/util/xml.ts +8 -0
  108. package/src/workspace/heartbeat-service.ts +5 -24
  109. package/src/workspace/migrations/migrate-to-workspace-volume.ts +113 -0
  110. package/src/workspace/migrations/registry.ts +2 -0
  111. package/src/__tests__/archive-recall.test.ts +0 -560
  112. package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
  113. package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
  114. package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
  115. package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
  116. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
  117. package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
  118. package/src/__tests__/memory-brief-time.test.ts +0 -285
  119. package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
  120. package/src/__tests__/memory-chunk-archive.test.ts +0 -400
  121. package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
  122. package/src/__tests__/memory-episode-archive.test.ts +0 -370
  123. package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
  124. package/src/__tests__/memory-observation-archive.test.ts +0 -375
  125. package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
  126. package/src/__tests__/memory-reducer-job.test.ts +0 -538
  127. package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
  128. package/src/__tests__/memory-reducer-store.test.ts +0 -728
  129. package/src/__tests__/memory-reducer-types.test.ts +0 -707
  130. package/src/__tests__/memory-reducer.test.ts +0 -704
  131. package/src/__tests__/memory-simplified-config.test.ts +0 -281
  132. package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
  133. package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
  134. package/src/config/schemas/memory-simplified.ts +0 -101
  135. package/src/memory/archive-recall.ts +0 -516
  136. package/src/memory/archive-store.ts +0 -400
  137. package/src/memory/brief-formatting.ts +0 -33
  138. package/src/memory/brief-open-loops.ts +0 -266
  139. package/src/memory/brief-time.ts +0 -162
  140. package/src/memory/brief.ts +0 -75
  141. package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
  142. package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
  143. package/src/memory/migrations/185-memory-brief-state.ts +0 -52
  144. package/src/memory/migrations/186-memory-archive.ts +0 -109
  145. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
  146. package/src/memory/reducer-scheduler.ts +0 -242
  147. package/src/memory/reducer-store.ts +0 -271
  148. package/src/memory/reducer-types.ts +0 -106
  149. package/src/memory/reducer.ts +0 -467
  150. package/src/memory/schema/memory-archive.ts +0 -121
  151. 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
- });