alvin-bot 4.18.0 → 4.18.2

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 (74) hide show
  1. package/AEC-PLUGINS-SOURCES.md +53 -0
  2. package/CHANGELOG.md +37 -2
  3. package/DESIGN-SKILLS-SOURCES.md +81 -0
  4. package/bin/cli.js +1 -1
  5. package/dist/providers/claude-sdk-provider.js +24 -0
  6. package/package.json +3 -1
  7. package/test/allowed-users-gate.test.ts +0 -98
  8. package/test/alvin-dispatch.test.ts +0 -220
  9. package/test/async-agent-chunk-flow.test.ts +0 -244
  10. package/test/async-agent-parser-staleness.test.ts +0 -412
  11. package/test/async-agent-parser-streamjson.test.ts +0 -273
  12. package/test/async-agent-parser.test.ts +0 -322
  13. package/test/async-agent-watcher.test.ts +0 -229
  14. package/test/background-bypass-integration.test.ts +0 -443
  15. package/test/background-bypass-stress.test.ts +0 -417
  16. package/test/background-bypass.test.ts +0 -127
  17. package/test/browser-webfetch.test.ts +0 -121
  18. package/test/claude-sdk-provider.test.ts +0 -115
  19. package/test/claude-sdk-tool-use-id.test.ts +0 -180
  20. package/test/console-timestamps.test.ts +0 -98
  21. package/test/cron-progress-ticker.test.ts +0 -76
  22. package/test/cron-restart-resilience.test.ts +0 -191
  23. package/test/cron-run-resolver.test.ts +0 -133
  24. package/test/cron-runjobnow-throw.test.ts +0 -100
  25. package/test/debounce.test.ts +0 -60
  26. package/test/delivery-registry.test.ts +0 -71
  27. package/test/exec-guard-metachars.test.ts +0 -110
  28. package/test/file-permissions.test.ts +0 -130
  29. package/test/i18n.test.ts +0 -108
  30. package/test/list-subagents-merged.test.ts +0 -172
  31. package/test/memory-extractor.test.ts +0 -151
  32. package/test/memory-layers.test.ts +0 -169
  33. package/test/memory-sdk-injection.test.ts +0 -146
  34. package/test/memory-stress-restart.test.ts +0 -337
  35. package/test/multi-session-stress.test.ts +0 -255
  36. package/test/platform-session-key.test.ts +0 -69
  37. package/test/process-manager.test.ts +0 -186
  38. package/test/registry.test.ts +0 -201
  39. package/test/session-pending-background.test.ts +0 -59
  40. package/test/session-persistence.test.ts +0 -195
  41. package/test/slack-progress-ticker.test.ts +0 -123
  42. package/test/slack-slash-command.test.ts +0 -61
  43. package/test/slack-test-connection.test.ts +0 -176
  44. package/test/stress-scenarios.test.ts +0 -356
  45. package/test/stuck-timer.test.ts +0 -116
  46. package/test/subagent-delivery-markdown-fallback.test.ts +0 -147
  47. package/test/subagent-delivery-platform-routing.test.ts +0 -232
  48. package/test/subagent-delivery.test.ts +0 -273
  49. package/test/subagent-final-text.test.ts +0 -132
  50. package/test/subagent-stats.test.ts +0 -119
  51. package/test/subagent-toolset-allowlist.test.ts +0 -146
  52. package/test/subagents-commands.test.ts +0 -64
  53. package/test/subagents-config.test.ts +0 -114
  54. package/test/subagents-depth.test.ts +0 -58
  55. package/test/subagents-inheritance.test.ts +0 -67
  56. package/test/subagents-name-resolver.test.ts +0 -122
  57. package/test/subagents-priority-reject.test.ts +0 -88
  58. package/test/subagents-queue.test.ts +0 -127
  59. package/test/subagents-shutdown.test.ts +0 -126
  60. package/test/subagents-toolset.test.ts +0 -71
  61. package/test/sync-task-timeout.test.ts +0 -153
  62. package/test/system-prompt-background-hint.test.ts +0 -65
  63. package/test/telegram-error-filter.test.ts +0 -85
  64. package/test/telegram-workspace-command.test.ts +0 -78
  65. package/test/timing-safe-bearer.test.ts +0 -65
  66. package/test/watchdog-brake.test.ts +0 -157
  67. package/test/watcher-pending-count.test.ts +0 -228
  68. package/test/watcher-zombie-fix.test.ts +0 -252
  69. package/test/web-server-integration.test.ts +0 -189
  70. package/test/web-server-resilience.test.ts +0 -118
  71. package/test/web-server-shutdown.test.ts +0 -117
  72. package/test/whatsapp-auth-resilience.test.ts +0 -96
  73. package/test/workspaces.test.ts +0 -196
  74. 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
- });