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.
- package/AEC-PLUGINS-SOURCES.md +53 -0
- package/CHANGELOG.md +37 -2
- package/DESIGN-SKILLS-SOURCES.md +81 -0
- package/bin/cli.js +1 -1
- package/dist/providers/claude-sdk-provider.js +24 -0
- package/package.json +3 -1
- package/test/allowed-users-gate.test.ts +0 -98
- package/test/alvin-dispatch.test.ts +0 -220
- package/test/async-agent-chunk-flow.test.ts +0 -244
- package/test/async-agent-parser-staleness.test.ts +0 -412
- package/test/async-agent-parser-streamjson.test.ts +0 -273
- package/test/async-agent-parser.test.ts +0 -322
- package/test/async-agent-watcher.test.ts +0 -229
- package/test/background-bypass-integration.test.ts +0 -443
- package/test/background-bypass-stress.test.ts +0 -417
- package/test/background-bypass.test.ts +0 -127
- package/test/browser-webfetch.test.ts +0 -121
- package/test/claude-sdk-provider.test.ts +0 -115
- package/test/claude-sdk-tool-use-id.test.ts +0 -180
- package/test/console-timestamps.test.ts +0 -98
- package/test/cron-progress-ticker.test.ts +0 -76
- package/test/cron-restart-resilience.test.ts +0 -191
- package/test/cron-run-resolver.test.ts +0 -133
- package/test/cron-runjobnow-throw.test.ts +0 -100
- package/test/debounce.test.ts +0 -60
- package/test/delivery-registry.test.ts +0 -71
- package/test/exec-guard-metachars.test.ts +0 -110
- package/test/file-permissions.test.ts +0 -130
- package/test/i18n.test.ts +0 -108
- package/test/list-subagents-merged.test.ts +0 -172
- package/test/memory-extractor.test.ts +0 -151
- package/test/memory-layers.test.ts +0 -169
- package/test/memory-sdk-injection.test.ts +0 -146
- package/test/memory-stress-restart.test.ts +0 -337
- package/test/multi-session-stress.test.ts +0 -255
- package/test/platform-session-key.test.ts +0 -69
- package/test/process-manager.test.ts +0 -186
- package/test/registry.test.ts +0 -201
- package/test/session-pending-background.test.ts +0 -59
- package/test/session-persistence.test.ts +0 -195
- package/test/slack-progress-ticker.test.ts +0 -123
- package/test/slack-slash-command.test.ts +0 -61
- package/test/slack-test-connection.test.ts +0 -176
- package/test/stress-scenarios.test.ts +0 -356
- package/test/stuck-timer.test.ts +0 -116
- package/test/subagent-delivery-markdown-fallback.test.ts +0 -147
- package/test/subagent-delivery-platform-routing.test.ts +0 -232
- package/test/subagent-delivery.test.ts +0 -273
- package/test/subagent-final-text.test.ts +0 -132
- package/test/subagent-stats.test.ts +0 -119
- package/test/subagent-toolset-allowlist.test.ts +0 -146
- package/test/subagents-commands.test.ts +0 -64
- package/test/subagents-config.test.ts +0 -114
- package/test/subagents-depth.test.ts +0 -58
- package/test/subagents-inheritance.test.ts +0 -67
- package/test/subagents-name-resolver.test.ts +0 -122
- package/test/subagents-priority-reject.test.ts +0 -88
- package/test/subagents-queue.test.ts +0 -127
- package/test/subagents-shutdown.test.ts +0 -126
- package/test/subagents-toolset.test.ts +0 -71
- package/test/sync-task-timeout.test.ts +0 -153
- package/test/system-prompt-background-hint.test.ts +0 -65
- package/test/telegram-error-filter.test.ts +0 -85
- package/test/telegram-workspace-command.test.ts +0 -78
- package/test/timing-safe-bearer.test.ts +0 -65
- package/test/watchdog-brake.test.ts +0 -157
- package/test/watcher-pending-count.test.ts +0 -228
- package/test/watcher-zombie-fix.test.ts +0 -252
- package/test/web-server-integration.test.ts +0 -189
- package/test/web-server-resilience.test.ts +0 -118
- package/test/web-server-shutdown.test.ts +0 -117
- package/test/whatsapp-auth-resilience.test.ts +0 -96
- package/test/workspaces.test.ts +0 -196
- 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
|
-
});
|