talon-agent 1.0.0 → 1.2.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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/package.json +15 -11
  4. package/prompts/dream.md +7 -3
  5. package/prompts/heartbeat.md +30 -0
  6. package/prompts/identity.md +1 -0
  7. package/prompts/teams.md +3 -0
  8. package/prompts/telegram.md +1 -0
  9. package/src/__tests__/chat-settings.test.ts +108 -2
  10. package/src/__tests__/cleanup-registry.test.ts +58 -0
  11. package/src/__tests__/config.test.ts +118 -52
  12. package/src/__tests__/cron-store-extended.test.ts +661 -0
  13. package/src/__tests__/cron-store.test.ts +145 -11
  14. package/src/__tests__/daily-log.test.ts +224 -13
  15. package/src/__tests__/dispatcher.test.ts +424 -23
  16. package/src/__tests__/dream.test.ts +1028 -0
  17. package/src/__tests__/errors-extended.test.ts +428 -0
  18. package/src/__tests__/errors.test.ts +95 -3
  19. package/src/__tests__/fuzz.test.ts +87 -15
  20. package/src/__tests__/gateway-actions.test.ts +1174 -433
  21. package/src/__tests__/gateway-http.test.ts +210 -19
  22. package/src/__tests__/gateway-retry.test.ts +359 -0
  23. package/src/__tests__/gateway-withRetry-extended.test.ts +343 -0
  24. package/src/__tests__/graph.test.ts +830 -0
  25. package/src/__tests__/handlers-stream.test.ts +208 -0
  26. package/src/__tests__/handlers.test.ts +2539 -70
  27. package/src/__tests__/heartbeat.test.ts +364 -0
  28. package/src/__tests__/history-extended.test.ts +775 -0
  29. package/src/__tests__/history-persistence.test.ts +74 -19
  30. package/src/__tests__/history.test.ts +113 -79
  31. package/src/__tests__/integration.test.ts +43 -8
  32. package/src/__tests__/log-init.test.ts +129 -0
  33. package/src/__tests__/log.test.ts +23 -5
  34. package/src/__tests__/media-index.test.ts +317 -35
  35. package/src/__tests__/plugin.test.ts +314 -0
  36. package/src/__tests__/prompt-builder-extended.test.ts +296 -0
  37. package/src/__tests__/prompt-builder.test.ts +44 -9
  38. package/src/__tests__/sessions.test.ts +258 -4
  39. package/src/__tests__/storage-save-errors.test.ts +342 -0
  40. package/src/__tests__/teams-frontend.test.ts +526 -31
  41. package/src/__tests__/telegram-formatting.test.ts +82 -0
  42. package/src/__tests__/terminal-commands.test.ts +208 -1
  43. package/src/__tests__/terminal-renderer.test.ts +223 -0
  44. package/src/__tests__/time.test.ts +107 -0
  45. package/src/__tests__/workspace-migrate.test.ts +256 -0
  46. package/src/__tests__/workspace.test.ts +63 -1
  47. package/src/backend/claude-sdk/tools.ts +64 -18
  48. package/src/bootstrap.ts +14 -14
  49. package/src/cli.ts +440 -125
  50. package/src/core/cron.ts +20 -5
  51. package/src/core/dispatcher.ts +27 -9
  52. package/src/core/dream.ts +79 -24
  53. package/src/core/errors.ts +12 -2
  54. package/src/core/gateway-actions.ts +182 -46
  55. package/src/core/gateway.ts +93 -41
  56. package/src/core/heartbeat.ts +515 -0
  57. package/src/core/plugin.ts +1 -1
  58. package/src/core/prompt-builder.ts +1 -4
  59. package/src/core/pulse.ts +4 -3
  60. package/src/frontend/teams/actions.ts +3 -1
  61. package/src/frontend/teams/formatting.ts +47 -8
  62. package/src/frontend/teams/graph.ts +35 -11
  63. package/src/frontend/teams/index.ts +155 -57
  64. package/src/frontend/teams/tools.ts +4 -6
  65. package/src/frontend/telegram/actions.ts +358 -82
  66. package/src/frontend/telegram/admin.ts +162 -72
  67. package/src/frontend/telegram/callbacks.ts +16 -10
  68. package/src/frontend/telegram/commands.ts +37 -21
  69. package/src/frontend/telegram/formatting.ts +2 -4
  70. package/src/frontend/telegram/handlers.ts +262 -66
  71. package/src/frontend/telegram/index.ts +39 -14
  72. package/src/frontend/telegram/middleware.ts +14 -4
  73. package/src/frontend/telegram/userbot.ts +16 -4
  74. package/src/frontend/terminal/renderer.ts +1 -4
  75. package/src/index.ts +28 -4
  76. package/src/storage/chat-settings.ts +32 -9
  77. package/src/storage/cron-store.ts +53 -11
  78. package/src/storage/daily-log.ts +72 -19
  79. package/src/storage/history.ts +39 -21
  80. package/src/storage/media-index.ts +37 -12
  81. package/src/storage/sessions.ts +3 -2
  82. package/src/util/cleanup-registry.ts +34 -0
  83. package/src/util/config.ts +85 -23
  84. package/src/util/log.ts +47 -17
  85. package/src/util/paths.ts +10 -0
  86. package/src/util/time.ts +29 -6
  87. package/src/util/watchdog.ts +5 -1
  88. package/src/util/workspace.ts +51 -10
@@ -0,0 +1,1028 @@
1
+ /**
2
+ * Tests for src/core/dream.ts
3
+ *
4
+ * Covers: initDream, maybeStartDream (guard paths), forceDream (already running),
5
+ * and the readDreamState / writeDreamState helpers via observable side effects.
6
+ * The actual SDK agent call (executeDream → runDreamAgent) is mocked to avoid
7
+ * spawning a real Claude agent in CI.
8
+ */
9
+
10
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
11
+
12
+ // ── Mocks ─────────────────────────────────────────────────────────────────
13
+
14
+ vi.mock("../util/log.js", () => ({
15
+ log: vi.fn(),
16
+ logError: vi.fn(),
17
+ logWarn: vi.fn(),
18
+ }));
19
+
20
+ const existsSyncMock = vi.fn(() => false);
21
+ const readFileSyncMock = vi.fn(() => "null");
22
+ const mkdirSyncMock = vi.fn();
23
+ const appendFileSyncMock = vi.fn();
24
+
25
+ vi.mock("node:fs", () => ({
26
+ existsSync: existsSyncMock,
27
+ readFileSync: readFileSyncMock,
28
+ mkdirSync: mkdirSyncMock,
29
+ appendFileSync: appendFileSyncMock,
30
+ }));
31
+
32
+ const writeAtomicSyncMock = vi.fn();
33
+ vi.mock("write-file-atomic", () => ({
34
+ default: { sync: writeAtomicSyncMock },
35
+ }));
36
+
37
+ // Mock the agent SDK so runDreamAgent doesn't actually spawn Claude
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ const queryMock = vi.fn<() => AsyncGenerator<any>>(async function* () {
40
+ // Yield nothing — simulates a clean run
41
+ });
42
+ vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
43
+ query: queryMock,
44
+ }));
45
+
46
+ vi.mock("../util/paths.js", () => ({
47
+ files: {
48
+ dreamState: "/fake/.talon/workspace/memory/dream_state.json",
49
+ memory: "/fake/.talon/workspace/memory/memory.md",
50
+ log: "/fake/.talon/talon.log",
51
+ },
52
+ dirs: {
53
+ root: "/fake/.talon",
54
+ logs: "/fake/.talon/workspace/logs",
55
+ workspace: "/fake/.talon/workspace",
56
+ data: "/fake/.talon/data",
57
+ memory: "/fake/.talon/workspace/memory",
58
+ dailyMemory: "/fake/.talon/workspace/memory/daily",
59
+ },
60
+ }));
61
+
62
+ // ── Tests ─────────────────────────────────────────────────────────────────
63
+
64
+ const { initDream, maybeStartDream, forceDream } =
65
+ await import("../core/dream.js");
66
+
67
+ describe("initDream", () => {
68
+ it("accepts a config object without throwing", () => {
69
+ expect(() => initDream({ model: "claude-sonnet-4-6" })).not.toThrow();
70
+ });
71
+
72
+ it("accepts all optional config fields", () => {
73
+ expect(() =>
74
+ initDream({
75
+ model: "claude-sonnet-4-6",
76
+ dreamModel: "claude-haiku-4-5",
77
+ claudeBinary: "/usr/local/bin/claude",
78
+ workspace: "/tmp/test-workspace",
79
+ }),
80
+ ).not.toThrow();
81
+ });
82
+ });
83
+
84
+ describe("maybeStartDream", () => {
85
+ beforeEach(() => {
86
+ initDream({ model: "claude-sonnet-4-6" });
87
+ });
88
+
89
+ it("does nothing when no dream state exists (elapsed = 0 vs never)", () => {
90
+ // existsSync returns false → readDreamState returns null → last_run = 0
91
+ // elapsed = now - 0 = now (> 12h) → dream would run, but agent is mocked
92
+ existsSyncMock.mockReturnValue(false);
93
+ // Should not throw
94
+ expect(() => maybeStartDream()).not.toThrow();
95
+ });
96
+
97
+ it("does nothing when dream was recently run (within 12 hours)", () => {
98
+ const recentRun = Date.now() - 1_000; // 1 second ago
99
+ existsSyncMock.mockReturnValue(true);
100
+ readFileSyncMock.mockReturnValue(
101
+ JSON.stringify({ last_run: recentRun, status: "idle" }),
102
+ );
103
+
104
+ expect(() => maybeStartDream()).not.toThrow();
105
+ });
106
+
107
+ it("does not start if already dreaming (concurrent guard)", async () => {
108
+ // Start a dream to set dreaming = true
109
+ existsSyncMock.mockReturnValue(false);
110
+
111
+ // forceDream will try to run, but since the SDK is mocked to return immediately
112
+ // the dreaming flag will be released after the promise resolves.
113
+ // We test that a second call to forceDream while one is already awaited throws.
114
+
115
+ // Reset the module to get a fresh dreaming=false state
116
+ // (we can't easily reset module state here, so just verify the throw path)
117
+ // Instead, use forceDream twice concurrently:
118
+ const firstDream = forceDream().catch(() => {});
119
+
120
+ // The "dreaming" flag should now be true — forceDream should throw
121
+ await expect(forceDream()).rejects.toThrow("Dream already running");
122
+ await firstDream;
123
+ });
124
+ });
125
+
126
+ describe("forceDream", () => {
127
+ beforeEach(() => {
128
+ initDream({ model: "claude-sonnet-4-6" });
129
+ existsSyncMock.mockReturnValue(false);
130
+ writeAtomicSyncMock.mockClear();
131
+ });
132
+
133
+ it("writes dream state twice (running then idle)", async () => {
134
+ await forceDream();
135
+ // writeDreamState is called twice: once with status:running, once with status:idle
136
+ expect(writeAtomicSyncMock).toHaveBeenCalledTimes(2);
137
+
138
+ const firstCall = JSON.parse(
139
+ writeAtomicSyncMock.mock.calls[0][1] as string,
140
+ );
141
+ expect(firstCall.status).toBe("running");
142
+
143
+ const secondCall = JSON.parse(
144
+ writeAtomicSyncMock.mock.calls[1][1] as string,
145
+ );
146
+ expect(secondCall.status).toBe("idle");
147
+ });
148
+
149
+ it("resolves successfully when agent mock yields no messages", async () => {
150
+ await expect(forceDream()).resolves.toBeUndefined();
151
+ });
152
+
153
+ it("rejects and re-throws when not initialized (no configRef)", async () => {
154
+ // Re-import with a fresh module to get unconfigured state
155
+ vi.resetModules();
156
+ // Re-apply mocks
157
+ vi.doMock("../util/log.js", () => ({
158
+ log: vi.fn(),
159
+ logError: vi.fn(),
160
+ logWarn: vi.fn(),
161
+ }));
162
+ vi.doMock("node:fs", () => ({
163
+ existsSync: vi.fn(() => false),
164
+ readFileSync: vi.fn(() => "null"),
165
+ mkdirSync: vi.fn(),
166
+ appendFileSync: vi.fn(),
167
+ }));
168
+ vi.doMock("write-file-atomic", () => ({ default: { sync: vi.fn() } }));
169
+ vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({
170
+ query: vi.fn(async function* () {}),
171
+ }));
172
+ vi.doMock("../util/paths.js", () => ({
173
+ files: {
174
+ dreamState: "/fake/.talon/workspace/memory/dream_state.json",
175
+ memory: "/fake/.talon/workspace/memory/memory.md",
176
+ log: "/fake/.talon/talon.log",
177
+ },
178
+ dirs: {
179
+ root: "/fake/.talon",
180
+ logs: "/fake/.talon/workspace/logs",
181
+ workspace: "/fake/.talon/workspace",
182
+ data: "/fake/.talon/data",
183
+ memory: "/fake/.talon/workspace/memory",
184
+ dailyMemory: "/fake/.talon/workspace/memory/daily",
185
+ },
186
+ }));
187
+
188
+ const { forceDream: forceDreamFresh } = await import("../core/dream.js");
189
+ // Not initialized → runDreamAgent warns and returns "" → no throw on "forced"
190
+ // Actually looking at the code: if !configRef → logWarn and return ""
191
+ // Which means executeDream resolves and writes state → no throw
192
+ await expect(forceDreamFresh()).resolves.toBeUndefined();
193
+ });
194
+ });
195
+
196
+ describe("readDreamState — edge cases", () => {
197
+ beforeEach(() => {
198
+ vi.resetModules();
199
+ vi.doMock("../util/log.js", () => ({
200
+ log: vi.fn(),
201
+ logError: vi.fn(),
202
+ logWarn: vi.fn(),
203
+ }));
204
+ vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({
205
+ query: vi.fn(async function* () {}),
206
+ }));
207
+ vi.doMock("write-file-atomic", () => ({ default: { sync: vi.fn() } }));
208
+ vi.doMock("../util/paths.js", () => ({
209
+ files: {
210
+ dreamState: "/fake/.talon/workspace/memory/dream_state.json",
211
+ memory: "/fake/.talon/workspace/memory/memory.md",
212
+ log: "/fake/.talon/talon.log",
213
+ },
214
+ dirs: {
215
+ root: "/fake/.talon",
216
+ logs: "/fake/.talon/workspace/logs",
217
+ workspace: "/fake/.talon/workspace",
218
+ data: "/fake/.talon/data",
219
+ memory: "/fake/.talon/workspace/memory",
220
+ dailyMemory: "/fake/.talon/workspace/memory/daily",
221
+ },
222
+ }));
223
+ });
224
+
225
+ it("maybeStartDream is a no-op when dream file is corrupt (falls through to elapsed check)", async () => {
226
+ vi.doMock("node:fs", () => ({
227
+ existsSync: vi.fn(() => true),
228
+ readFileSync: vi.fn(() => "{ invalid json "),
229
+ mkdirSync: vi.fn(),
230
+ appendFileSync: vi.fn(),
231
+ }));
232
+
233
+ const { initDream: initFresh, maybeStartDream: maybeFresh } =
234
+ await import("../core/dream.js");
235
+ initFresh({ model: "claude-sonnet-4-6" });
236
+ // Corrupt state → readDreamState returns null → last_run=0 → long elapsed → dream fires
237
+ // It won't throw because errors are swallowed in executeDream when trigger="auto"
238
+ expect(() => maybeFresh()).not.toThrow();
239
+ });
240
+
241
+ it("maybeStartDream treats non-numeric last_run as stale (readDreamState returns null)", async () => {
242
+ // Covers `if (typeof parsed.last_run !== "number") return null`
243
+ const invalidState = { last_run: "not-a-number", status: "idle" };
244
+ vi.doMock("node:fs", () => ({
245
+ existsSync: vi.fn(() => true),
246
+ readFileSync: vi.fn(() => JSON.stringify(invalidState)),
247
+ mkdirSync: vi.fn(),
248
+ appendFileSync: vi.fn(),
249
+ }));
250
+
251
+ const { initDream: initFresh, maybeStartDream: maybeFresh } =
252
+ await import("../core/dream.js");
253
+ initFresh({ model: "claude-sonnet-4-6" });
254
+ // non-numeric last_run → readDreamState returns null → treated as very old → dream fires
255
+ expect(() => maybeFresh()).not.toThrow();
256
+ });
257
+
258
+ it("maybeStartDream skips when state has last_run within interval", async () => {
259
+ const recentState = { last_run: Date.now() - 60_000, status: "idle" };
260
+ vi.doMock("node:fs", () => ({
261
+ existsSync: vi.fn(() => true),
262
+ readFileSync: vi.fn(() => JSON.stringify(recentState)),
263
+ mkdirSync: vi.fn(),
264
+ appendFileSync: vi.fn(),
265
+ }));
266
+
267
+ const { initDream: initFresh, maybeStartDream: maybeFresh } =
268
+ await import("../core/dream.js");
269
+ initFresh({ model: "claude-sonnet-4-6" });
270
+ const queryMock = vi.fn(async function* () {});
271
+ vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({ query: queryMock }));
272
+ maybeFresh();
273
+ // Query should NOT have been called (interval not yet elapsed)
274
+ expect(queryMock).not.toHaveBeenCalled();
275
+ });
276
+ });
277
+
278
+ // ── logDreamMessage coverage — all switch cases ───────────────────────────────
279
+
280
+ describe("logDreamMessage — processes all message types", () => {
281
+ // Use the top-level hoisted vi.mock mocks (queryMock, appendFileSyncMock, etc.)
282
+ // instead of vi.doMock + vi.resetModules to avoid flaky module cache issues in CI.
283
+
284
+ beforeEach(() => {
285
+ appendFileSyncMock.mockClear();
286
+ existsSyncMock.mockReturnValue(false);
287
+ readFileSyncMock.mockReturnValue("dream prompt template");
288
+ writeAtomicSyncMock.mockClear();
289
+ initDream({ model: "claude-sonnet-4-6", workspace: "/fake/ws" });
290
+ });
291
+
292
+ afterEach(() => {
293
+ // Reset query mock to default (yield nothing) so other tests aren't affected
294
+ queryMock.mockImplementation(async function* () {});
295
+ });
296
+
297
+ function setupQuery(messages: unknown[]) {
298
+ queryMock.mockImplementation(async function* () {
299
+ for (const msg of messages) yield msg;
300
+ });
301
+ }
302
+
303
+ function getLogOutput(): string {
304
+ return appendFileSyncMock.mock.calls
305
+ .map((c: unknown[]) => c[1] as string)
306
+ .join("");
307
+ }
308
+
309
+ it("processes assistant message with text content blocks", async () => {
310
+ setupQuery([
311
+ {
312
+ type: "assistant",
313
+ message: { content: [{ type: "text", text: "I am analyzing..." }] },
314
+ },
315
+ ]);
316
+ await forceDream();
317
+ expect(getLogOutput()).toContain("I am analyzing...");
318
+ });
319
+
320
+ it("processes assistant message with tool_use content blocks", async () => {
321
+ setupQuery([
322
+ {
323
+ type: "assistant",
324
+ message: {
325
+ content: [
326
+ {
327
+ type: "tool_use",
328
+ name: "Read",
329
+ input: { file_path: "/tmp/test.md" },
330
+ },
331
+ ],
332
+ },
333
+ },
334
+ ]);
335
+ await forceDream();
336
+ const output = getLogOutput();
337
+ expect(output).toContain("Read");
338
+ expect(output).toContain("Tool call:");
339
+ });
340
+
341
+ it("processes result message", async () => {
342
+ setupQuery([
343
+ {
344
+ type: "result",
345
+ subtype: "success",
346
+ result: "Memory consolidated successfully.",
347
+ },
348
+ ]);
349
+ await forceDream();
350
+ const output = getLogOutput();
351
+ expect(output).toContain("Memory consolidated successfully.");
352
+ expect(output).toContain("Result");
353
+ });
354
+
355
+ it("processes result message with truncation for long results", async () => {
356
+ setupQuery([
357
+ { type: "result", subtype: "success", result: "X".repeat(3000) },
358
+ ]);
359
+ await forceDream();
360
+ expect(getLogOutput()).toContain("... (truncated)");
361
+ });
362
+
363
+ it("processes system message", async () => {
364
+ setupQuery([{ type: "system", subtype: "init" }]);
365
+ await forceDream();
366
+ expect(getLogOutput()).toContain("System");
367
+ });
368
+
369
+ it("processes user message with string tool_use_result", async () => {
370
+ setupQuery([{ type: "user", tool_use_result: "tool ran successfully" }]);
371
+ await forceDream();
372
+ expect(getLogOutput()).toContain("tool ran successfully");
373
+ });
374
+
375
+ it("processes user message with object tool_use_result", async () => {
376
+ setupQuery([
377
+ {
378
+ type: "user",
379
+ tool_use_result: { output: "file contents", truncated: false },
380
+ },
381
+ ]);
382
+ await forceDream();
383
+ expect(getLogOutput()).toContain("file contents");
384
+ });
385
+
386
+ it("processes user message with long tool_use_result (truncation)", async () => {
387
+ setupQuery([{ type: "user", tool_use_result: "Y".repeat(3000) }]);
388
+ await forceDream();
389
+ expect(getLogOutput()).toContain("... (truncated)");
390
+ });
391
+
392
+ it("skips stream_event messages (default case)", async () => {
393
+ setupQuery([
394
+ { type: "stream_event", event: { type: "content_block_delta" } },
395
+ ]);
396
+ await expect(forceDream()).resolves.toBeUndefined();
397
+ });
398
+
399
+ it("result message without 'result' field falls back to JSON.stringify", async () => {
400
+ setupQuery([
401
+ // Intentionally omit 'result' field — covers JSON.stringify(msg) branch
402
+ { type: "result", subtype: "success" },
403
+ ]);
404
+ await forceDream();
405
+ expect(getLogOutput()).toContain("result");
406
+ });
407
+
408
+ it("user message without tool_use_result is silently skipped", async () => {
409
+ setupQuery([
410
+ { type: "user" }, // no tool_use_result field
411
+ ]);
412
+ await expect(forceDream()).resolves.toBeUndefined();
413
+ });
414
+
415
+ it("processes multiple message types in sequence", async () => {
416
+ setupQuery([
417
+ { type: "system", subtype: "init" },
418
+ {
419
+ type: "assistant",
420
+ message: { content: [{ type: "text", text: "Analyzing logs." }] },
421
+ },
422
+ { type: "result", subtype: "success", result: "Done." },
423
+ ]);
424
+ await forceDream();
425
+ const output = getLogOutput();
426
+ expect(output).toContain("Analyzing logs.");
427
+ expect(output).toContain("Done.");
428
+ });
429
+ });
430
+
431
+ describe("dream error paths", () => {
432
+ beforeEach(() => {
433
+ vi.resetModules();
434
+ });
435
+
436
+ it("logError when writeDreamState throws (writeFileAtomic.sync fails)", async () => {
437
+ const logErrorMock = vi.fn();
438
+ vi.doMock("../util/log.js", () => ({
439
+ log: vi.fn(),
440
+ logError: logErrorMock,
441
+ logWarn: vi.fn(),
442
+ }));
443
+ vi.doMock("node:fs", () => ({
444
+ existsSync: vi.fn(() => false),
445
+ readFileSync: vi.fn(() => "dream prompt"),
446
+ mkdirSync: vi.fn(),
447
+ appendFileSync: vi.fn(),
448
+ }));
449
+ vi.doMock("write-file-atomic", () => ({
450
+ default: {
451
+ sync: vi.fn(() => {
452
+ throw new Error("disk full");
453
+ }),
454
+ },
455
+ }));
456
+ vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({
457
+ query: vi.fn(async function* () {}),
458
+ }));
459
+ vi.doMock("../util/paths.js", () => ({
460
+ files: {
461
+ dreamState: "/fake/.talon/data/dream_state.json",
462
+ memory: "/fake/.talon/workspace/memory/memory.md",
463
+ log: "/fake/.talon/talon.log",
464
+ },
465
+ dirs: {
466
+ root: "/fake/.talon",
467
+ logs: "/fake/.talon/workspace/logs",
468
+ workspace: "/fake/.talon/workspace",
469
+ data: "/fake/.talon/data",
470
+ memory: "/fake/.talon/workspace/memory",
471
+ dailyMemory: "/fake/.talon/workspace/memory/daily",
472
+ },
473
+ }));
474
+
475
+ const mod = await import("../core/dream.js");
476
+ mod.initDream({ model: "claude-sonnet-4-6" });
477
+ // writeDreamState throwing should not crash forceDream (error is swallowed)
478
+ await expect(mod.forceDream()).resolves.toBeUndefined();
479
+ expect(logErrorMock).toHaveBeenCalledWith(
480
+ "dream",
481
+ "Failed to write dream state",
482
+ expect.any(Error),
483
+ );
484
+ });
485
+
486
+ it("logError when appendFileSync throws in appendDreamLog", async () => {
487
+ const logErrorMock = vi.fn();
488
+ vi.doMock("../util/log.js", () => ({
489
+ log: vi.fn(),
490
+ logError: logErrorMock,
491
+ logWarn: vi.fn(),
492
+ }));
493
+ vi.doMock("node:fs", () => ({
494
+ existsSync: vi.fn(() => false),
495
+ readFileSync: vi.fn(() => "dream prompt"),
496
+ mkdirSync: vi.fn(),
497
+ appendFileSync: vi.fn(() => {
498
+ throw new Error("append failed");
499
+ }),
500
+ }));
501
+ vi.doMock("write-file-atomic", () => ({ default: { sync: vi.fn() } }));
502
+ vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({
503
+ query: vi.fn(async function* () {}),
504
+ }));
505
+ vi.doMock("../util/paths.js", () => ({
506
+ files: {
507
+ dreamState: "/fake/.talon/data/dream_state.json",
508
+ memory: "/fake/.talon/workspace/memory/memory.md",
509
+ log: "/fake/.talon/talon.log",
510
+ },
511
+ dirs: {
512
+ root: "/fake/.talon",
513
+ logs: "/fake/.talon/workspace/logs",
514
+ workspace: "/fake/.talon/workspace",
515
+ data: "/fake/.talon/data",
516
+ memory: "/fake/.talon/workspace/memory",
517
+ dailyMemory: "/fake/.talon/workspace/memory/daily",
518
+ },
519
+ }));
520
+
521
+ const mod = await import("../core/dream.js");
522
+ mod.initDream({ model: "claude-sonnet-4-6" });
523
+ // appendFileSync throws, but appendDreamLog catches it
524
+ await expect(mod.forceDream()).resolves.toBeUndefined();
525
+ expect(logErrorMock).toHaveBeenCalledWith(
526
+ "dream",
527
+ "Failed to write dream log",
528
+ expect.any(Error),
529
+ );
530
+ });
531
+
532
+ it("forceDream rethrows when runDreamAgent fails (prompt file unreadable)", async () => {
533
+ const logErrorMock = vi.fn();
534
+ vi.doMock("../util/log.js", () => ({
535
+ log: vi.fn(),
536
+ logError: logErrorMock,
537
+ logWarn: vi.fn(),
538
+ }));
539
+ vi.doMock("node:fs", () => ({
540
+ existsSync: vi.fn(() => false), // readDreamState returns null (no read needed)
541
+ readFileSync: vi.fn(() => {
542
+ throw new Error("ENOENT: prompt file missing");
543
+ }),
544
+ mkdirSync: vi.fn(),
545
+ appendFileSync: vi.fn(),
546
+ }));
547
+ vi.doMock("write-file-atomic", () => ({ default: { sync: vi.fn() } }));
548
+ vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({
549
+ query: vi.fn(async function* () {}),
550
+ }));
551
+ vi.doMock("../util/paths.js", () => ({
552
+ files: {
553
+ dreamState: "/fake/.talon/data/dream_state.json",
554
+ memory: "/fake/.talon/workspace/memory/memory.md",
555
+ log: "/fake/.talon/talon.log",
556
+ },
557
+ dirs: {
558
+ root: "/fake/.talon",
559
+ logs: "/fake/.talon/workspace/logs",
560
+ workspace: "/fake/.talon/workspace",
561
+ data: "/fake/.talon/data",
562
+ memory: "/fake/.talon/workspace/memory",
563
+ dailyMemory: "/fake/.talon/workspace/memory/daily",
564
+ },
565
+ }));
566
+
567
+ const mod = await import("../core/dream.js");
568
+ mod.initDream({ model: "claude-sonnet-4-6" });
569
+ // forceDream rethrows when runDreamAgent throws (trigger === "forced")
570
+ await expect(mod.forceDream()).rejects.toThrow(
571
+ "Failed to read dream prompt",
572
+ );
573
+ expect(logErrorMock).toHaveBeenCalledWith(
574
+ "dream",
575
+ expect.stringContaining("Memory consolidation failed"),
576
+ expect.any(Error),
577
+ );
578
+ });
579
+
580
+ it("runDreamAgent uses non-zero lastRunTimestamp when state has last_run > 0 (line 112 TRUE branch)", async () => {
581
+ const appendFileSyncMock = vi.fn();
582
+ const lastRun = Date.now() - 3_600_000; // 1 hour ago
583
+ const stateJson = JSON.stringify({ last_run: lastRun, status: "idle" });
584
+
585
+ vi.doMock("node:fs", () => {
586
+ const readFileSyncFn = vi
587
+ .fn()
588
+ .mockReturnValueOnce(stateJson) // readDreamState reads dream_state.json
589
+ .mockReturnValue("dream prompt"); // subsequent calls: prompt file
590
+ return {
591
+ existsSync: vi.fn(() => true),
592
+ readFileSync: readFileSyncFn,
593
+ mkdirSync: vi.fn(),
594
+ appendFileSync: appendFileSyncMock,
595
+ };
596
+ });
597
+ vi.doMock("write-file-atomic", () => ({ default: { sync: vi.fn() } }));
598
+ vi.doMock("../util/log.js", () => ({
599
+ log: vi.fn(),
600
+ logError: vi.fn(),
601
+ logWarn: vi.fn(),
602
+ }));
603
+ vi.doMock("../util/paths.js", () => ({
604
+ files: {
605
+ dreamState: "/fake/.talon/data/dream_state.json",
606
+ memory: "/fake/.talon/workspace/memory/memory.md",
607
+ log: "/fake/.talon/talon.log",
608
+ },
609
+ dirs: {
610
+ root: "/fake/.talon",
611
+ logs: "/fake/.talon/workspace/logs",
612
+ workspace: "/fake/.talon/workspace",
613
+ data: "/fake/.talon/data",
614
+ memory: "/fake/.talon/workspace/memory",
615
+ dailyMemory: "/fake/.talon/workspace/memory/daily",
616
+ },
617
+ }));
618
+ vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({
619
+ query: vi.fn(async function* () {}),
620
+ }));
621
+
622
+ const mod = await import("../core/dream.js");
623
+ mod.initDream({ model: "claude-sonnet-4-6", workspace: "/fake/ws" });
624
+ await mod.forceDream();
625
+
626
+ const logOutput = appendFileSyncMock.mock.calls
627
+ .map((c: unknown[]) => c[1] as string)
628
+ .join("");
629
+ // TRUE branch: lastRunTimestamp > 0 → ISO string used instead of "beginning of time"
630
+ expect(logOutput).toContain(new Date(lastRun).toISOString());
631
+ expect(logOutput).not.toContain("beginning of time");
632
+ });
633
+
634
+ it("runDreamAgent includes pathToClaudeCodeExecutable when claudeBinary set, and uses dreamModel (lines 135+150 TRUE branches)", async () => {
635
+ vi.doMock("node:fs", () => ({
636
+ existsSync: vi.fn(() => false),
637
+ readFileSync: vi.fn(() => "dream prompt"),
638
+ mkdirSync: vi.fn(),
639
+ appendFileSync: vi.fn(),
640
+ }));
641
+ vi.doMock("write-file-atomic", () => ({ default: { sync: vi.fn() } }));
642
+ vi.doMock("../util/log.js", () => ({
643
+ log: vi.fn(),
644
+ logError: vi.fn(),
645
+ logWarn: vi.fn(),
646
+ }));
647
+ vi.doMock("../util/paths.js", () => ({
648
+ files: {
649
+ dreamState: "/fake/.talon/data/dream_state.json",
650
+ memory: "/fake/.talon/workspace/memory/memory.md",
651
+ log: "/fake/.talon/talon.log",
652
+ },
653
+ dirs: {
654
+ root: "/fake/.talon",
655
+ logs: "/fake/.talon/workspace/logs",
656
+ workspace: "/fake/.talon/workspace",
657
+ data: "/fake/.talon/data",
658
+ memory: "/fake/.talon/workspace/memory",
659
+ dailyMemory: "/fake/.talon/workspace/memory/daily",
660
+ },
661
+ }));
662
+ const queryMock = vi.fn(async function* () {});
663
+ vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({ query: queryMock }));
664
+
665
+ const mod = await import("../core/dream.js");
666
+ mod.initDream({
667
+ model: "claude-sonnet-4-6",
668
+ dreamModel: "claude-haiku-4-5",
669
+ claudeBinary: "/usr/local/bin/claude",
670
+ workspace: "/fake/ws",
671
+ });
672
+ await mod.forceDream();
673
+
674
+ expect(queryMock).toHaveBeenCalled();
675
+ const callArgs = (queryMock.mock.calls[0] as unknown[])[0] as {
676
+ options: Record<string, unknown>;
677
+ };
678
+ // dreamModel TRUE branch (line 135): haiku model used instead of sonnet
679
+ expect(callArgs.options).toHaveProperty("model", "claude-haiku-4-5");
680
+ // claudeBinary TRUE branch (line 150): pathToClaudeCodeExecutable spread in
681
+ expect(callArgs.options).toHaveProperty(
682
+ "pathToClaudeCodeExecutable",
683
+ "/usr/local/bin/claude",
684
+ );
685
+ });
686
+
687
+ it("assistant block with type='text' but no 'text' property maps to empty string (line 226 FALSE branch)", async () => {
688
+ const appendFileSyncMock = vi.fn();
689
+ vi.doMock("node:fs", () => ({
690
+ existsSync: vi.fn(() => false),
691
+ readFileSync: vi.fn(() => "dream prompt"),
692
+ mkdirSync: vi.fn(),
693
+ appendFileSync: appendFileSyncMock,
694
+ }));
695
+ vi.doMock("write-file-atomic", () => ({ default: { sync: vi.fn() } }));
696
+ vi.doMock("../util/log.js", () => ({
697
+ log: vi.fn(),
698
+ logError: vi.fn(),
699
+ logWarn: vi.fn(),
700
+ }));
701
+ vi.doMock("../util/paths.js", () => ({
702
+ files: {
703
+ dreamState: "/fake/.talon/data/dream_state.json",
704
+ memory: "/fake/.talon/workspace/memory/memory.md",
705
+ log: "/fake/.talon/talon.log",
706
+ },
707
+ dirs: {
708
+ root: "/fake/.talon",
709
+ logs: "/fake/.talon/workspace/logs",
710
+ workspace: "/fake/.talon/workspace",
711
+ data: "/fake/.talon/data",
712
+ memory: "/fake/.talon/workspace/memory",
713
+ dailyMemory: "/fake/.talon/workspace/memory/daily",
714
+ },
715
+ }));
716
+ vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({
717
+ query: vi.fn(async function* () {
718
+ // type="text" but no "text" property — covers `"text" in b` FALSE branch
719
+ yield { type: "assistant", message: { content: [{ type: "text" }] } };
720
+ }),
721
+ }));
722
+
723
+ const mod = await import("../core/dream.js");
724
+ mod.initDream({ model: "claude-sonnet-4-6", workspace: "/fake/ws" });
725
+ await mod.forceDream();
726
+
727
+ // textBlocks = [""] (empty string from false branch), length > 0 → Assistant block logged
728
+ const logOutput = appendFileSyncMock.mock.calls
729
+ .map((c: unknown[]) => c[1] as string)
730
+ .join("");
731
+ expect(logOutput).toContain("Assistant");
732
+ });
733
+
734
+ it("agent crash causes forceDream to reject with failure log entry", async () => {
735
+ const appendFileSyncMock = vi.fn();
736
+ vi.doMock("../util/log.js", () => ({
737
+ log: vi.fn(),
738
+ logError: vi.fn(),
739
+ logWarn: vi.fn(),
740
+ }));
741
+ vi.doMock("node:fs", () => ({
742
+ existsSync: vi.fn(() => false),
743
+ readFileSync: vi.fn(() => "dream prompt"),
744
+ mkdirSync: vi.fn(),
745
+ appendFileSync: appendFileSyncMock,
746
+ }));
747
+ vi.doMock("write-file-atomic", () => ({ default: { sync: vi.fn() } }));
748
+ vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({
749
+ query: vi.fn(async function* () {
750
+ throw new Error("agent crashed unexpectedly");
751
+ }),
752
+ }));
753
+ vi.doMock("../util/paths.js", () => ({
754
+ files: {
755
+ dreamState: "/fake/.talon/data/dream_state.json",
756
+ memory: "/fake/.talon/workspace/memory/memory.md",
757
+ log: "/fake/.talon/talon.log",
758
+ },
759
+ dirs: {
760
+ root: "/fake/.talon",
761
+ logs: "/fake/.talon/workspace/logs",
762
+ workspace: "/fake/.talon/workspace",
763
+ data: "/fake/.talon/data",
764
+ memory: "/fake/.talon/workspace/memory",
765
+ dailyMemory: "/fake/.talon/workspace/memory/daily",
766
+ },
767
+ }));
768
+
769
+ const mod = await import("../core/dream.js");
770
+ mod.initDream({ model: "claude-sonnet-4-6" });
771
+ await expect(mod.forceDream()).rejects.toThrow(
772
+ "agent crashed unexpectedly",
773
+ );
774
+ const logOutput = appendFileSyncMock.mock.calls
775
+ .map((c: unknown[]) => c[1] as string)
776
+ .join("");
777
+ expect(logOutput).toContain("Dream FAILED");
778
+ });
779
+
780
+ it("maybeStartDream returns early when dreaming=true (line 61 TRUE branch)", async () => {
781
+ let resolveQuery!: () => void;
782
+ const queryPromise = new Promise<void>((resolve) => {
783
+ resolveQuery = resolve;
784
+ });
785
+ vi.doMock("node:fs", () => ({
786
+ existsSync: vi.fn(() => false),
787
+ readFileSync: vi.fn(() => "dream prompt"),
788
+ mkdirSync: vi.fn(),
789
+ appendFileSync: vi.fn(),
790
+ }));
791
+ vi.doMock("write-file-atomic", () => ({ default: { sync: vi.fn() } }));
792
+ vi.doMock("../util/log.js", () => ({
793
+ log: vi.fn(),
794
+ logError: vi.fn(),
795
+ logWarn: vi.fn(),
796
+ }));
797
+ vi.doMock("../util/paths.js", () => ({
798
+ files: {
799
+ dreamState: "/fake/.talon/data/dream_state.json",
800
+ memory: "/fake/.talon/workspace/memory/memory.md",
801
+ log: "/fake/.talon/talon.log",
802
+ },
803
+ dirs: {
804
+ root: "/fake/.talon",
805
+ logs: "/fake/.talon/workspace/logs",
806
+ workspace: "/fake/.talon/workspace",
807
+ data: "/fake/.talon/data",
808
+ memory: "/fake/.talon/workspace/memory",
809
+ dailyMemory: "/fake/.talon/workspace/memory/daily",
810
+ },
811
+ }));
812
+ vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({
813
+ query: vi.fn(async function* () {
814
+ await queryPromise; // suspend until we release
815
+ }),
816
+ }));
817
+
818
+ const mod = await import("../core/dream.js");
819
+ mod.initDream({ model: "claude-sonnet-4-6", workspace: "/fake/ws" });
820
+
821
+ // forceDream sets dreaming=true synchronously before its first await
822
+ const dreamPromise = mod.forceDream();
823
+ // dreaming is now true — maybeStartDream should hit the `if (dreaming) return` guard
824
+ mod.maybeStartDream();
825
+ // Release the suspended query so forceDream can finish
826
+ resolveQuery();
827
+ await dreamPromise;
828
+ });
829
+
830
+ it("auto-triggered dream failure swallows error (line 98 FALSE branch: trigger !== 'forced')", async () => {
831
+ vi.doMock("node:fs", () => ({
832
+ existsSync: vi.fn(() => false),
833
+ readFileSync: vi.fn(() => {
834
+ throw new Error("prompt missing for auto dream");
835
+ }),
836
+ mkdirSync: vi.fn(),
837
+ appendFileSync: vi.fn(),
838
+ }));
839
+ vi.doMock("write-file-atomic", () => ({ default: { sync: vi.fn() } }));
840
+ const logErrorMock = vi.fn();
841
+ vi.doMock("../util/log.js", () => ({
842
+ log: vi.fn(),
843
+ logError: logErrorMock,
844
+ logWarn: vi.fn(),
845
+ }));
846
+ vi.doMock("../util/paths.js", () => ({
847
+ files: {
848
+ dreamState: "/fake/.talon/data/dream_state.json",
849
+ memory: "/fake/.talon/workspace/memory/memory.md",
850
+ log: "/fake/.talon/talon.log",
851
+ },
852
+ dirs: {
853
+ root: "/fake/.talon",
854
+ logs: "/fake/.talon/workspace/logs",
855
+ workspace: "/fake/.talon/workspace",
856
+ data: "/fake/.talon/data",
857
+ memory: "/fake/.talon/workspace/memory",
858
+ dailyMemory: "/fake/.talon/workspace/memory/daily",
859
+ },
860
+ }));
861
+ vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({
862
+ query: vi.fn(async function* () {}),
863
+ }));
864
+
865
+ const mod = await import("../core/dream.js");
866
+ mod.initDream({ model: "claude-sonnet-4-6", workspace: "/fake/ws" });
867
+
868
+ // maybeStartDream fires auto dream (elapsed > 12h since state is null)
869
+ mod.maybeStartDream();
870
+ // Give the async fire-and-forget dream time to run and fail
871
+ await new Promise((r) => setTimeout(r, 50));
872
+
873
+ // trigger === "auto" → error is NOT re-thrown (FALSE branch of if trigger==="forced")
874
+ // logError should have been called with the failure
875
+ expect(logErrorMock).toHaveBeenCalledWith(
876
+ "dream",
877
+ expect.stringContaining("Memory consolidation failed"),
878
+ expect.any(Error),
879
+ );
880
+ });
881
+
882
+ it("model defaults to 'claude-sonnet-4-6' when neither dreamModel nor model set (line 135 FALSE??FALSE branch)", async () => {
883
+ vi.doMock("node:fs", () => ({
884
+ existsSync: vi.fn(() => false),
885
+ readFileSync: vi.fn(() => "dream prompt"),
886
+ mkdirSync: vi.fn(),
887
+ appendFileSync: vi.fn(),
888
+ }));
889
+ vi.doMock("write-file-atomic", () => ({ default: { sync: vi.fn() } }));
890
+ vi.doMock("../util/log.js", () => ({
891
+ log: vi.fn(),
892
+ logError: vi.fn(),
893
+ logWarn: vi.fn(),
894
+ }));
895
+ vi.doMock("../util/paths.js", () => ({
896
+ files: {
897
+ dreamState: "/fake/.talon/data/dream_state.json",
898
+ memory: "/fake/.talon/workspace/memory/memory.md",
899
+ log: "/fake/.talon/talon.log",
900
+ },
901
+ dirs: {
902
+ root: "/fake/.talon",
903
+ logs: "/fake/.talon/workspace/logs",
904
+ workspace: "/fake/.talon/workspace",
905
+ data: "/fake/.talon/data",
906
+ memory: "/fake/.talon/workspace/memory",
907
+ dailyMemory: "/fake/.talon/workspace/memory/daily",
908
+ },
909
+ }));
910
+ const queryMock = vi.fn(async function* () {});
911
+ vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({ query: queryMock }));
912
+
913
+ const mod = await import("../core/dream.js");
914
+ // No model or dreamModel → falls through to "claude-sonnet-4-6" literal default
915
+ mod.initDream({ workspace: "/fake/ws" });
916
+ await mod.forceDream();
917
+
918
+ const callArgs = (queryMock.mock.calls[0] as unknown[])[0] as {
919
+ options: Record<string, unknown>;
920
+ };
921
+ expect(callArgs.options).toHaveProperty("model", "claude-sonnet-4-6");
922
+ });
923
+ });
924
+
925
+ describe("runDreamAgent — timeout arrow fn fires after DREAM_TIMEOUT_MS", () => {
926
+ it("covers the setTimeout reject callback via fake timers (10-minute dream timeout)", async () => {
927
+ vi.resetModules();
928
+ vi.useFakeTimers();
929
+
930
+ vi.doMock("../util/log.js", () => ({
931
+ log: vi.fn(),
932
+ logError: vi.fn(),
933
+ logWarn: vi.fn(),
934
+ }));
935
+ vi.doMock("node:fs", () => ({
936
+ existsSync: vi.fn(() => false),
937
+ readFileSync: vi.fn(() => "null"),
938
+ mkdirSync: vi.fn(),
939
+ appendFileSync: vi.fn(),
940
+ }));
941
+ vi.doMock("write-file-atomic", () => ({ default: { sync: vi.fn() } }));
942
+ vi.doMock("../util/paths.js", () => ({
943
+ files: {
944
+ dreamState: "/fake/.talon/data/dream_state.json",
945
+ memory: "/fake/.talon/workspace/memory/memory.md",
946
+ log: "/fake/.talon/talon.log",
947
+ },
948
+ dirs: {
949
+ root: "/fake/.talon",
950
+ logs: "/fake/.talon/workspace/logs",
951
+ workspace: "/fake/.talon/workspace",
952
+ data: "/fake/.talon/data",
953
+ memory: "/fake/.talon/workspace/memory",
954
+ dailyMemory: "/fake/.talon/workspace/memory/daily",
955
+ },
956
+ }));
957
+ // query never resolves — so the 10-minute timeout wins the race
958
+ vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({
959
+ query: vi.fn(async function* () {
960
+ await new Promise(() => {}); // never yields
961
+ }),
962
+ }));
963
+
964
+ const mod = await import("../core/dream.js");
965
+ mod.initDream({ model: "claude-sonnet-4-6" });
966
+
967
+ const dreamPromise = mod.forceDream().catch(() => {});
968
+ // Advance past DREAM_TIMEOUT_MS (10 minutes) to fire the setTimeout reject callback
969
+ await vi.advanceTimersByTimeAsync(10 * 60 * 1000 + 1);
970
+ await dreamPromise;
971
+
972
+ vi.useRealTimers();
973
+ });
974
+ });
975
+
976
+ describe("maybeStartDream — () => {} catch callback on executeDream rejection", () => {
977
+ it("fires the catch callback silently when executeDream('auto') rejects", async () => {
978
+ vi.resetModules();
979
+
980
+ // Make log throw on first call — that call is at L89 (before the try block)
981
+ // in executeDream, which causes the async function to reject.
982
+ const logMock = vi.fn(() => {
983
+ throw new Error("log forced fail");
984
+ });
985
+ vi.doMock("../util/log.js", () => ({
986
+ log: logMock,
987
+ logError: vi.fn(),
988
+ logWarn: vi.fn(),
989
+ }));
990
+ vi.doMock("node:fs", () => ({
991
+ existsSync: vi.fn(() => false),
992
+ readFileSync: vi.fn(() => "null"),
993
+ mkdirSync: vi.fn(),
994
+ appendFileSync: vi.fn(),
995
+ }));
996
+ vi.doMock("write-file-atomic", () => ({ default: { sync: vi.fn() } }));
997
+ vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({
998
+ query: vi.fn(async function* () {}),
999
+ }));
1000
+ vi.doMock("../util/paths.js", () => ({
1001
+ files: {
1002
+ dreamState: "/fake/.talon/data/dream_state.json",
1003
+ memory: "/fake/.talon/workspace/memory/memory.md",
1004
+ log: "/fake/.talon/talon.log",
1005
+ },
1006
+ dirs: {
1007
+ root: "/fake/.talon",
1008
+ logs: "/fake/.talon/workspace/logs",
1009
+ workspace: "/fake/.talon/workspace",
1010
+ data: "/fake/.talon/data",
1011
+ memory: "/fake/.talon/workspace/memory",
1012
+ dailyMemory: "/fake/.talon/workspace/memory/daily",
1013
+ },
1014
+ }));
1015
+
1016
+ const mod = await import("../core/dream.js");
1017
+ mod.initDream({ model: "claude-sonnet-4-6" });
1018
+
1019
+ // existsSync returns false → state null → elapsed = now >> 12h → executeDream fires
1020
+ // log throws before the try block → executeDream("auto") rejects
1021
+ // .catch(() => {}) swallows the rejection silently
1022
+ expect(() => mod.maybeStartDream()).not.toThrow();
1023
+
1024
+ // Flush microtasks so the .catch(() => {}) callback fires
1025
+ await new Promise((r) => setTimeout(r, 0));
1026
+ // No uncaught rejection — the () => {} catch callback covered
1027
+ });
1028
+ });