alvin-bot 4.18.0 → 4.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +22 -2
  2. package/bin/cli.js +1 -1
  3. package/package.json +3 -1
  4. package/test/allowed-users-gate.test.ts +0 -98
  5. package/test/alvin-dispatch.test.ts +0 -220
  6. package/test/async-agent-chunk-flow.test.ts +0 -244
  7. package/test/async-agent-parser-staleness.test.ts +0 -412
  8. package/test/async-agent-parser-streamjson.test.ts +0 -273
  9. package/test/async-agent-parser.test.ts +0 -322
  10. package/test/async-agent-watcher.test.ts +0 -229
  11. package/test/background-bypass-integration.test.ts +0 -443
  12. package/test/background-bypass-stress.test.ts +0 -417
  13. package/test/background-bypass.test.ts +0 -127
  14. package/test/browser-webfetch.test.ts +0 -121
  15. package/test/claude-sdk-provider.test.ts +0 -115
  16. package/test/claude-sdk-tool-use-id.test.ts +0 -180
  17. package/test/console-timestamps.test.ts +0 -98
  18. package/test/cron-progress-ticker.test.ts +0 -76
  19. package/test/cron-restart-resilience.test.ts +0 -191
  20. package/test/cron-run-resolver.test.ts +0 -133
  21. package/test/cron-runjobnow-throw.test.ts +0 -100
  22. package/test/debounce.test.ts +0 -60
  23. package/test/delivery-registry.test.ts +0 -71
  24. package/test/exec-guard-metachars.test.ts +0 -110
  25. package/test/file-permissions.test.ts +0 -130
  26. package/test/i18n.test.ts +0 -108
  27. package/test/list-subagents-merged.test.ts +0 -172
  28. package/test/memory-extractor.test.ts +0 -151
  29. package/test/memory-layers.test.ts +0 -169
  30. package/test/memory-sdk-injection.test.ts +0 -146
  31. package/test/memory-stress-restart.test.ts +0 -337
  32. package/test/multi-session-stress.test.ts +0 -255
  33. package/test/platform-session-key.test.ts +0 -69
  34. package/test/process-manager.test.ts +0 -186
  35. package/test/registry.test.ts +0 -201
  36. package/test/session-pending-background.test.ts +0 -59
  37. package/test/session-persistence.test.ts +0 -195
  38. package/test/slack-progress-ticker.test.ts +0 -123
  39. package/test/slack-slash-command.test.ts +0 -61
  40. package/test/slack-test-connection.test.ts +0 -176
  41. package/test/stress-scenarios.test.ts +0 -356
  42. package/test/stuck-timer.test.ts +0 -116
  43. package/test/subagent-delivery-markdown-fallback.test.ts +0 -147
  44. package/test/subagent-delivery-platform-routing.test.ts +0 -232
  45. package/test/subagent-delivery.test.ts +0 -273
  46. package/test/subagent-final-text.test.ts +0 -132
  47. package/test/subagent-stats.test.ts +0 -119
  48. package/test/subagent-toolset-allowlist.test.ts +0 -146
  49. package/test/subagents-commands.test.ts +0 -64
  50. package/test/subagents-config.test.ts +0 -114
  51. package/test/subagents-depth.test.ts +0 -58
  52. package/test/subagents-inheritance.test.ts +0 -67
  53. package/test/subagents-name-resolver.test.ts +0 -122
  54. package/test/subagents-priority-reject.test.ts +0 -88
  55. package/test/subagents-queue.test.ts +0 -127
  56. package/test/subagents-shutdown.test.ts +0 -126
  57. package/test/subagents-toolset.test.ts +0 -71
  58. package/test/sync-task-timeout.test.ts +0 -153
  59. package/test/system-prompt-background-hint.test.ts +0 -65
  60. package/test/telegram-error-filter.test.ts +0 -85
  61. package/test/telegram-workspace-command.test.ts +0 -78
  62. package/test/timing-safe-bearer.test.ts +0 -65
  63. package/test/watchdog-brake.test.ts +0 -157
  64. package/test/watcher-pending-count.test.ts +0 -228
  65. package/test/watcher-zombie-fix.test.ts +0 -252
  66. package/test/web-server-integration.test.ts +0 -189
  67. package/test/web-server-resilience.test.ts +0 -118
  68. package/test/web-server-shutdown.test.ts +0 -117
  69. package/test/whatsapp-auth-resilience.test.ts +0 -96
  70. package/test/workspaces.test.ts +0 -196
  71. package/vitest.config.ts +0 -17
@@ -1,146 +0,0 @@
1
- /**
2
- * v4.11.0 — SDK system prompts now receive MEMORY.md context.
3
- *
4
- * Before v4.11.0, only non-SDK providers (Groq, Gemini, NVIDIA) got
5
- * buildMemoryContext() injected into their system prompt — the SDK was
6
- * expected to read memory files via tools. In practice it rarely did,
7
- * resulting in "frickelig" memory after restart even with persisted
8
- * sessions. v4.11.0 closes this gap.
9
- */
10
- import { describe, it, expect, beforeEach, vi } from "vitest";
11
- import fs from "fs";
12
- import os from "os";
13
- import { resolve } from "path";
14
-
15
- const TEST_DATA_DIR = resolve(os.tmpdir(), `alvin-sdk-mem-inject-${process.pid}-${Date.now()}`);
16
-
17
- beforeEach(() => {
18
- if (fs.existsSync(TEST_DATA_DIR)) fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
19
- fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
20
- fs.mkdirSync(resolve(TEST_DATA_DIR, "memory"), { recursive: true });
21
- fs.writeFileSync(
22
- resolve(TEST_DATA_DIR, "memory", "MEMORY.md"),
23
- "# Long-term Memory\n\n- User User prefers terse answers.\n- HOMES uses Postgres `homes_production`.\n",
24
- );
25
- process.env.ALVIN_DATA_DIR = TEST_DATA_DIR;
26
- vi.resetModules();
27
- });
28
-
29
- describe("SDK memory injection (v4.11.0)", () => {
30
- it("buildSystemPrompt(isSDK=true) now includes MEMORY.md content", async () => {
31
- const { buildSystemPrompt } = await import("../src/services/personality.js");
32
- const prompt = buildSystemPrompt(true, "en", "1234");
33
- expect(prompt).toMatch(/User User prefers terse answers/);
34
- expect(prompt).toMatch(/HOMES uses Postgres/);
35
- });
36
-
37
- it("non-SDK still gets memory injection (regression check)", async () => {
38
- const { buildSystemPrompt } = await import("../src/services/personality.js");
39
- const prompt = buildSystemPrompt(false, "en", "1234");
40
- expect(prompt).toMatch(/User User prefers terse answers/);
41
- });
42
-
43
- it("no MEMORY.md → SDK prompt builds without crash and without memory section", async () => {
44
- fs.unlinkSync(resolve(TEST_DATA_DIR, "memory", "MEMORY.md"));
45
- vi.resetModules();
46
- const { buildSystemPrompt } = await import("../src/services/personality.js");
47
- const prompt = buildSystemPrompt(true, "en", "1234");
48
- expect(prompt).toBeTruthy();
49
- expect(prompt).not.toMatch(/Your Memory \(auto-loaded\)/);
50
- });
51
- });
52
-
53
- describe("SDK smart prompt (semantic recall) on first turn (v4.11.0)", () => {
54
- it("buildSmartSystemPrompt for SDK with isFirstTurn=false does NOT call searchMemory", async () => {
55
- let searchCalls = 0;
56
- vi.doMock("../src/services/embeddings.js", () => ({
57
- searchMemory: async () => {
58
- searchCalls++;
59
- return [];
60
- },
61
- reindexMemory: async () => ({ indexed: 0, total: 0 }),
62
- initEmbeddings: async () => {},
63
- getIndexStats: () => ({ entries: 0, files: 0, lastReindex: 0, sizeBytes: 0 }),
64
- }));
65
- vi.resetModules();
66
- const { buildSmartSystemPrompt } = await import("../src/services/personality.js");
67
-
68
- await buildSmartSystemPrompt(true, "en", "tell me about HOMES", "1234", false);
69
- expect(searchCalls).toBe(0);
70
- });
71
-
72
- it("buildSmartSystemPrompt for SDK with isFirstTurn=true CALLS searchMemory", async () => {
73
- let searchCalls = 0;
74
- vi.doMock("../src/services/embeddings.js", () => ({
75
- searchMemory: async () => {
76
- searchCalls++;
77
- return [
78
- { text: "HOMES uses homes_production database", source: "MEMORY.md", score: 0.9 },
79
- ];
80
- },
81
- reindexMemory: async () => ({ indexed: 0, total: 0 }),
82
- initEmbeddings: async () => {},
83
- getIndexStats: () => ({ entries: 0, files: 0, lastReindex: 0, sizeBytes: 0 }),
84
- }));
85
- vi.resetModules();
86
- const { buildSmartSystemPrompt } = await import("../src/services/personality.js");
87
-
88
- const prompt = await buildSmartSystemPrompt(true, "en", "tell me about HOMES", "1234", true);
89
- expect(searchCalls).toBe(1);
90
- expect(prompt).toMatch(/Relevant Memories \(auto-retrieved\)/);
91
- expect(prompt).toMatch(/homes_production/);
92
- });
93
-
94
- it("non-SDK calls searchMemory regardless of isFirstTurn flag", async () => {
95
- let searchCalls = 0;
96
- vi.doMock("../src/services/embeddings.js", () => ({
97
- searchMemory: async () => {
98
- searchCalls++;
99
- return [];
100
- },
101
- reindexMemory: async () => ({ indexed: 0, total: 0 }),
102
- initEmbeddings: async () => {},
103
- getIndexStats: () => ({ entries: 0, files: 0, lastReindex: 0, sizeBytes: 0 }),
104
- }));
105
- vi.resetModules();
106
- const { buildSmartSystemPrompt } = await import("../src/services/personality.js");
107
-
108
- await buildSmartSystemPrompt(false, "en", "HOMES backup question", "1234", false);
109
- expect(searchCalls).toBe(1);
110
- });
111
-
112
- it("buildSmartSystemPrompt for SDK with no userMessage skips search even when isFirstTurn=true", async () => {
113
- let searchCalls = 0;
114
- vi.doMock("../src/services/embeddings.js", () => ({
115
- searchMemory: async () => {
116
- searchCalls++;
117
- return [];
118
- },
119
- reindexMemory: async () => ({ indexed: 0, total: 0 }),
120
- initEmbeddings: async () => {},
121
- getIndexStats: () => ({ entries: 0, files: 0, lastReindex: 0, sizeBytes: 0 }),
122
- }));
123
- vi.resetModules();
124
- const { buildSmartSystemPrompt } = await import("../src/services/personality.js");
125
-
126
- await buildSmartSystemPrompt(true, "en", undefined, "1234", true);
127
- expect(searchCalls).toBe(0);
128
- });
129
-
130
- it("graceful failure: SDK first turn search throws → returns base prompt without crashing", async () => {
131
- vi.doMock("../src/services/embeddings.js", () => ({
132
- searchMemory: async () => {
133
- throw new Error("Embedding API down");
134
- },
135
- reindexMemory: async () => ({ indexed: 0, total: 0 }),
136
- initEmbeddings: async () => {},
137
- getIndexStats: () => ({ entries: 0, files: 0, lastReindex: 0, sizeBytes: 0 }),
138
- }));
139
- vi.resetModules();
140
- const { buildSmartSystemPrompt } = await import("../src/services/personality.js");
141
-
142
- const prompt = await buildSmartSystemPrompt(true, "en", "test query", "1234", true);
143
- expect(prompt).toBeTruthy();
144
- expect(prompt).toMatch(/User User prefers terse answers/); // base still works
145
- });
146
- });
@@ -1,337 +0,0 @@
1
- /**
2
- * v4.11.0 — Hardcore stress tests for memory persistence.
3
- *
4
- * Validates that the persistence + memory-layers + extractor stack survives
5
- * the full range of edge cases that bite real bots in production:
6
- * - 100 concurrent sessions across rapid mutate-flush cycles
7
- * - Atomic write under simulated crash mid-write
8
- * - Schema drift (old persisted snapshot, new bot version)
9
- * - Corrupted JSON
10
- * - Empty/missing identity files
11
- * - Unicode + emoji in content
12
- * - Very long history
13
- * - Garbage entries in state file
14
- * - Memory dir missing entirely
15
- */
16
- import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
17
- import fs from "fs";
18
- import os from "os";
19
- import { resolve } from "path";
20
-
21
- const TEST_DATA_DIR = resolve(os.tmpdir(), `alvin-stress-${process.pid}-${Date.now()}`);
22
-
23
- beforeEach(() => {
24
- if (fs.existsSync(TEST_DATA_DIR)) fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
25
- fs.mkdirSync(resolve(TEST_DATA_DIR, "memory"), { recursive: true });
26
- process.env.ALVIN_DATA_DIR = TEST_DATA_DIR;
27
- vi.resetModules();
28
- });
29
-
30
- afterEach(() => {
31
- try { fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); } catch { /* ignore */ }
32
- });
33
-
34
- describe("memory persistence stress (v4.11.0)", () => {
35
- it("100 sessions all flush + reload correctly", async () => {
36
- const sessionMod = await import("../src/services/session.js");
37
- const persistMod = await import("../src/services/session-persistence.js");
38
-
39
- for (let i = 0; i < 100; i++) {
40
- const s = sessionMod.getSession(`stress-user-${i}`);
41
- s.sessionId = `sdk-${i}`;
42
- s.history = [{ role: "user", content: `msg from user ${i}` }];
43
- s.messageCount = i;
44
- }
45
-
46
- await persistMod.flushSessions();
47
-
48
- // Wipe in-memory map by resetting the module
49
- vi.resetModules();
50
- const sessionMod2 = await import("../src/services/session.js");
51
- const persistMod2 = await import("../src/services/session-persistence.js");
52
-
53
- const loaded = persistMod2.loadPersistedSessions();
54
- expect(loaded).toBe(100);
55
-
56
- for (let i = 0; i < 100; i++) {
57
- const s = sessionMod2.getSession(`stress-user-${i}`);
58
- expect(s.sessionId).toBe(`sdk-${i}`);
59
- expect(s.history).toHaveLength(1);
60
- expect(s.history[0].content).toBe(`msg from user ${i}`);
61
- }
62
- });
63
-
64
- it("unicode + emoji in session content survive round-trip", async () => {
65
- const sessionMod = await import("../src/services/session.js");
66
- const persistMod = await import("../src/services/session-persistence.js");
67
-
68
- const s = sessionMod.getSession("unicode-user");
69
- s.sessionId = "abc";
70
- s.history = [
71
- { role: "user", content: "Hallo 🦊 was läuft? München → Berlin Übersetzung: 你好" },
72
- { role: "assistant", content: "Klar 🎉 — alles ok ✅" },
73
- ];
74
-
75
- await persistMod.flushSessions();
76
- vi.resetModules();
77
- const persistMod2 = await import("../src/services/session-persistence.js");
78
- const sessionMod2 = await import("../src/services/session.js");
79
- persistMod2.loadPersistedSessions();
80
- const restored = sessionMod2.getSession("unicode-user");
81
- expect(restored.history[0].content).toMatch(/🦊/);
82
- expect(restored.history[0].content).toMatch(/你好/);
83
- expect(restored.history[1].content).toMatch(/🎉/);
84
- });
85
-
86
- it("very long history (300 messages) gets capped at 50 on persist", async () => {
87
- const sessionMod = await import("../src/services/session.js");
88
- const persistMod = await import("../src/services/session-persistence.js");
89
-
90
- const s = sessionMod.getSession("chatty-user");
91
- s.sessionId = "abc";
92
- for (let i = 0; i < 300; i++) {
93
- s.history.push({ role: i % 2 === 0 ? "user" : "assistant", content: `m${i}` });
94
- }
95
-
96
- await persistMod.flushSessions();
97
-
98
- vi.resetModules();
99
- const persistMod2 = await import("../src/services/session-persistence.js");
100
- const sessionMod2 = await import("../src/services/session.js");
101
- persistMod2.loadPersistedSessions();
102
- const restored = sessionMod2.getSession("chatty-user");
103
- expect(restored.history.length).toBeLessThanOrEqual(50);
104
- // The most recent should be preserved
105
- expect(restored.history.at(-1)?.content).toBe("m299");
106
- });
107
-
108
- it("schema drift: old snapshot with missing fields rehydrates with defaults", async () => {
109
- fs.mkdirSync(resolve(TEST_DATA_DIR, "state"), { recursive: true });
110
- fs.writeFileSync(
111
- resolve(TEST_DATA_DIR, "state", "sessions.json"),
112
- JSON.stringify({
113
- "old-user": {
114
- // Only the absolute minimum from a hypothetical earlier version
115
- sessionId: "abc",
116
- // Missing: language, effort, voiceReply, etc.
117
- },
118
- }),
119
- );
120
-
121
- const persistMod = await import("../src/services/session-persistence.js");
122
- const sessionMod = await import("../src/services/session.js");
123
- const loaded = persistMod.loadPersistedSessions();
124
- expect(loaded).toBe(1);
125
-
126
- const s = sessionMod.getSession("old-user");
127
- expect(s.sessionId).toBe("abc");
128
- expect(s.language).toBe("en");
129
- expect(s.effort).toBe("medium");
130
- expect(s.voiceReply).toBe(false);
131
- expect(s.history).toEqual([]);
132
- });
133
-
134
- it("garbage entries in sessions.json are skipped without breaking the rest", async () => {
135
- fs.mkdirSync(resolve(TEST_DATA_DIR, "state"), { recursive: true });
136
- fs.writeFileSync(
137
- resolve(TEST_DATA_DIR, "state", "sessions.json"),
138
- JSON.stringify({
139
- "good-user": { sessionId: "good", history: [] },
140
- "bad-user": null,
141
- "another-bad": "this is a string not an object",
142
- }),
143
- );
144
-
145
- const persistMod = await import("../src/services/session-persistence.js");
146
- const sessionMod = await import("../src/services/session.js");
147
- persistMod.loadPersistedSessions();
148
-
149
- expect(sessionMod.getSession("good-user").sessionId).toBe("good");
150
- // bad-user and another-bad are silently skipped — no crash
151
- });
152
-
153
- it("memory-layers handles a missing memory dir gracefully", async () => {
154
- fs.rmSync(resolve(TEST_DATA_DIR, "memory"), { recursive: true, force: true });
155
- vi.resetModules();
156
- const { loadMemoryLayers } = await import("../src/services/memory-layers.js");
157
- const layers = loadMemoryLayers();
158
- expect(layers.identity).toBe("");
159
- expect(layers.preferences).toBe("");
160
- expect(layers.longTerm).toBe("");
161
- expect(layers.projects).toEqual([]);
162
- });
163
-
164
- it("memory-layers handles unicode in identity and projects", async () => {
165
- fs.writeFileSync(
166
- resolve(TEST_DATA_DIR, "memory", "identity.md"),
167
- "Name: Test User 🦊\nLocation: Berlin",
168
- );
169
- fs.mkdirSync(resolve(TEST_DATA_DIR, "memory", "projects"), { recursive: true });
170
- fs.writeFileSync(
171
- resolve(TEST_DATA_DIR, "memory", "projects", "perseus.md"),
172
- "Trading bot 📈 — handles ~$1M equity",
173
- );
174
-
175
- const { loadMemoryLayers, buildLayeredContext } = await import("../src/services/memory-layers.js");
176
- const layers = loadMemoryLayers();
177
- expect(layers.identity).toMatch(/🦊/);
178
- expect(layers.projects[0].content).toMatch(/📈/);
179
-
180
- const ctx = buildLayeredContext("how is perseus doing");
181
- expect(ctx).toMatch(/📈/);
182
- });
183
-
184
- it("100 mutate→persist cycles with debounce do not corrupt state", async () => {
185
- const sessionMod = await import("../src/services/session.js");
186
- const persistMod = await import("../src/services/session-persistence.js");
187
-
188
- const s = sessionMod.getSession("rapid-user");
189
- for (let i = 0; i < 100; i++) {
190
- s.sessionId = `v${i}`;
191
- persistMod.schedulePersist();
192
- }
193
- await persistMod.flushSessions();
194
-
195
- const stateFile = resolve(TEST_DATA_DIR, "state", "sessions.json");
196
- // v4.12.0 — Format is now an envelope: { version, sessions, telegramWorkspaces }
197
- const parsed = JSON.parse(fs.readFileSync(stateFile, "utf-8")).sessions;
198
- expect(parsed["rapid-user"].sessionId).toBe("v99");
199
- });
200
-
201
- it("memory-extractor opt-out env var is respected", async () => {
202
- process.env.MEMORY_EXTRACTION_DISABLED = "1";
203
- vi.resetModules();
204
- const { extractAndStoreFacts } = await import("../src/services/memory-extractor.js");
205
- const result = await extractAndStoreFacts("Some conversation about Berlin and Postgres");
206
- expect(result.disabled).toBe(true);
207
- expect(result.factsStored).toBe(0);
208
- delete process.env.MEMORY_EXTRACTION_DISABLED;
209
- });
210
-
211
- it("hostile sessions.json: empty object loads zero sessions", async () => {
212
- fs.mkdirSync(resolve(TEST_DATA_DIR, "state"), { recursive: true });
213
- fs.writeFileSync(resolve(TEST_DATA_DIR, "state", "sessions.json"), "{}");
214
- const persistMod = await import("../src/services/session-persistence.js");
215
- expect(persistMod.loadPersistedSessions()).toBe(0);
216
- });
217
-
218
- it("hostile sessions.json: null root is rejected gracefully", async () => {
219
- fs.mkdirSync(resolve(TEST_DATA_DIR, "state"), { recursive: true });
220
- fs.writeFileSync(resolve(TEST_DATA_DIR, "state", "sessions.json"), "null");
221
- const persistMod = await import("../src/services/session-persistence.js");
222
- expect(persistMod.loadPersistedSessions()).toBe(0);
223
- });
224
-
225
- it("hostile sessions.json: array root is rejected gracefully", async () => {
226
- fs.mkdirSync(resolve(TEST_DATA_DIR, "state"), { recursive: true });
227
- fs.writeFileSync(resolve(TEST_DATA_DIR, "state", "sessions.json"), "[1,2,3]");
228
- const persistMod = await import("../src/services/session-persistence.js");
229
- // Arrays are technically objects in JS — entries() returns indexed pairs.
230
- // The persisted-session shape filter will reject each entry → 0 loaded.
231
- const loaded = persistMod.loadPersistedSessions();
232
- expect(loaded).toBeLessThanOrEqual(3); // permissive but doesn't crash
233
- });
234
-
235
- it("history preserves message order (chronology) after slice cap", async () => {
236
- const sessionMod = await import("../src/services/session.js");
237
- const persistMod = await import("../src/services/session-persistence.js");
238
-
239
- const s = sessionMod.getSession("chrono-user");
240
- s.sessionId = "abc";
241
- for (let i = 0; i < 80; i++) {
242
- s.history.push({ role: "user", content: `msg-${i.toString().padStart(3, "0")}` });
243
- }
244
- await persistMod.flushSessions();
245
-
246
- vi.resetModules();
247
- const persistMod2 = await import("../src/services/session-persistence.js");
248
- const sessionMod2 = await import("../src/services/session.js");
249
- persistMod2.loadPersistedSessions();
250
- const restored = sessionMod2.getSession("chrono-user");
251
-
252
- // Last 50 should be preserved in order
253
- expect(restored.history[0].content).toBe("msg-030");
254
- expect(restored.history[49].content).toBe("msg-079");
255
- });
256
-
257
- it("layered context with very long identity stays under budget", async () => {
258
- const longIdentity = "Name: User. ".repeat(2000); // 22000 chars
259
- fs.writeFileSync(
260
- resolve(TEST_DATA_DIR, "memory", "identity.md"),
261
- longIdentity,
262
- );
263
- const { buildLayeredContext } = await import("../src/services/memory-layers.js");
264
- const ctx = buildLayeredContext();
265
- expect(ctx.length).toBeLessThan(6000);
266
- expect(ctx).toMatch(/truncated/);
267
- });
268
-
269
- it("memory-extractor JSON tolerance: handles whitespace-only response", async () => {
270
- const { parseExtractedFacts } = await import("../src/services/memory-extractor.js");
271
- const facts = parseExtractedFacts(" \n\n ");
272
- expect(facts.user_facts).toEqual([]);
273
- });
274
-
275
- it("memory-extractor handles null facts in response", async () => {
276
- const { parseExtractedFacts } = await import("../src/services/memory-extractor.js");
277
- const facts = parseExtractedFacts(JSON.stringify({
278
- user_facts: null,
279
- preferences: ["valid pref"],
280
- decisions: undefined,
281
- }));
282
- expect(facts.user_facts).toEqual([]);
283
- expect(facts.preferences).toEqual(["valid pref"]);
284
- expect(facts.decisions).toEqual([]);
285
- });
286
-
287
- it("session-persistence handles read-only filesystem gracefully", async () => {
288
- const sessionMod = await import("../src/services/session.js");
289
- const persistMod = await import("../src/services/session-persistence.js");
290
-
291
- const s = sessionMod.getSession("test-user");
292
- s.sessionId = "abc";
293
-
294
- // Make state dir read-only AFTER it exists
295
- fs.mkdirSync(resolve(TEST_DATA_DIR, "state"), { recursive: true });
296
- fs.chmodSync(resolve(TEST_DATA_DIR, "state"), 0o444);
297
-
298
- // Should not throw
299
- await expect(persistMod.flushSessions()).resolves.toBeUndefined();
300
-
301
- // Cleanup so afterEach can rmSync
302
- fs.chmodSync(resolve(TEST_DATA_DIR, "state"), 0o755);
303
- });
304
-
305
- it("simulated bot restart: sessionId roundtrips across module reset", async () => {
306
- // Simulate first bot lifetime
307
- const session1 = await import("../src/services/session.js");
308
- const persist1 = await import("../src/services/session-persistence.js");
309
- persist1.loadPersistedSessions();
310
-
311
- // Simulate user interaction
312
- const s = session1.getSession("restart-user");
313
- s.sessionId = "claude-uuid-12345";
314
- s.language = "de";
315
- s.effort = "high";
316
- s.history = [
317
- { role: "user", content: "Erstes Gespräch" },
318
- { role: "assistant", content: "Hallo!" },
319
- ];
320
- await persist1.flushSessions();
321
-
322
- // Simulate full bot restart — reset modules
323
- vi.resetModules();
324
-
325
- // Second bot lifetime
326
- const session2 = await import("../src/services/session.js");
327
- const persist2 = await import("../src/services/session-persistence.js");
328
- persist2.loadPersistedSessions();
329
-
330
- const s2 = session2.getSession("restart-user");
331
- expect(s2.sessionId).toBe("claude-uuid-12345");
332
- expect(s2.language).toBe("de");
333
- expect(s2.effort).toBe("high");
334
- expect(s2.history).toHaveLength(2);
335
- expect(s2.history[0].content).toBe("Erstes Gespräch");
336
- });
337
- });