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
package/test/i18n.test.ts DELETED
@@ -1,108 +0,0 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import { t, initI18n, setLocale, getLocale, LOCALE_NAMES, LOCALE_FLAGS } from "../src/i18n.js";
3
-
4
- describe("i18n", () => {
5
- beforeEach(() => {
6
- // Reset to a known state before each test
7
- setLocale("en");
8
- });
9
-
10
- describe("LOCALE_NAMES", () => {
11
- it("exposes all four supported locales", () => {
12
- expect(LOCALE_NAMES.en).toBe("English");
13
- expect(LOCALE_NAMES.de).toBe("Deutsch");
14
- expect(LOCALE_NAMES.es).toBe("Español");
15
- expect(LOCALE_NAMES.fr).toBe("Français");
16
- });
17
- });
18
-
19
- describe("LOCALE_FLAGS", () => {
20
- it("has a flag emoji for every locale", () => {
21
- expect(LOCALE_FLAGS.en).toBe("🇬🇧");
22
- expect(LOCALE_FLAGS.de).toBe("🇩🇪");
23
- expect(LOCALE_FLAGS.es).toBe("🇪🇸");
24
- expect(LOCALE_FLAGS.fr).toBe("🇫🇷");
25
- });
26
- });
27
-
28
- describe("t() — translation lookup", () => {
29
- it("returns the English string when locale is en", () => {
30
- const msg = t("bot.cancel.cancelling", "en");
31
- expect(msg).toContain("Cancelling");
32
- });
33
-
34
- it("returns the German string when locale is de", () => {
35
- const msg = t("bot.cancel.cancelling", "de");
36
- expect(msg).toContain("abgebrochen");
37
- });
38
-
39
- it("returns the Spanish string when locale is es", () => {
40
- const msg = t("bot.cancel.cancelling", "es");
41
- expect(msg.toLowerCase()).toContain("cancelando");
42
- });
43
-
44
- it("returns the French string when locale is fr", () => {
45
- const msg = t("bot.cancel.cancelling", "fr");
46
- expect(msg.toLowerCase()).toContain("annulation");
47
- });
48
-
49
- it("falls back to English when locale is missing for a key", () => {
50
- // Use a TUI key which only has en+de — request es, should fall through
51
- // to en since tui.* keys aren't translated for es/fr.
52
- const msg = t("tui.title", "es");
53
- expect(msg).toContain("Alvin Bot TUI");
54
- });
55
-
56
- it("returns the key itself if no locale has it at all", () => {
57
- const msg = t("bot.nonexistent.key.for.test", "en");
58
- expect(msg).toBe("bot.nonexistent.key.for.test");
59
- });
60
-
61
- it("uses the global currentLocale when no locale is passed", () => {
62
- setLocale("de");
63
- const msg = t("bot.cancel.cancelling");
64
- expect(msg).toContain("abgebrochen");
65
- });
66
- });
67
-
68
- describe("t() — interpolation", () => {
69
- it("substitutes a single {var} placeholder", () => {
70
- const msg = t("bot.error.timeoutStuck", "en", { min: 10 });
71
- expect(msg).toContain("10 minutes");
72
- });
73
-
74
- it("substitutes multiple {var} placeholders", () => {
75
- const msg = t("bot.error.midStream", "en", {
76
- name: "claude-sdk",
77
- detail: "connection reset",
78
- });
79
- expect(msg).toContain("claude-sdk");
80
- expect(msg).toContain("connection reset");
81
- });
82
-
83
- it("interpolation works in all four locales", () => {
84
- const vars = { min: 7 };
85
- expect(t("bot.error.timeoutStuck", "en", vars)).toContain("7");
86
- expect(t("bot.error.timeoutStuck", "de", vars)).toContain("7");
87
- expect(t("bot.error.timeoutStuck", "es", vars)).toContain("7");
88
- expect(t("bot.error.timeoutStuck", "fr", vars)).toContain("7");
89
- });
90
-
91
- it("leaves {placeholder} visible if the var is not provided", () => {
92
- const msg = t("bot.error.timeoutStuck", "en", {});
93
- expect(msg).toContain("{min}");
94
- });
95
- });
96
-
97
- describe("initI18n / setLocale / getLocale", () => {
98
- it("initI18n with explicit locale sets currentLocale", () => {
99
- initI18n("fr");
100
- expect(getLocale()).toBe("fr");
101
- });
102
-
103
- it("setLocale updates the global locale", () => {
104
- setLocale("es");
105
- expect(getLocale()).toBe("es");
106
- });
107
- });
108
- });
@@ -1,172 +0,0 @@
1
- /**
2
- * v4.14.1 — `/subagents list` must show v4.13+ dispatch agents too.
3
- *
4
- * Root cause: `listSubAgents()` in subagents.ts only iterates the
5
- * `activeAgents` Map (B1+B2 from v4.0.0). v4.13's `alvin_dispatch_agent`
6
- * MCP tool writes into `async-agent-watcher.ts`'s `pending` Map instead.
7
- * User-facing impact: "no subagents running" while the bot is visibly
8
- * dispatching sub-agents.
9
- *
10
- * Fix strategy: a new `listActiveSubAgents()` helper that merges both
11
- * registries into a unified SubAgentInfo-shaped list. The `/subagents
12
- * list` handler uses this instead of the bare `listSubAgents()`.
13
- * Cancel/result operations keep using the old registry — we can't
14
- * cancel a detached `claude -p` subprocess anyway without knowing its
15
- * PID, which isn't tracked.
16
- */
17
- import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
18
- import fs from "fs";
19
- import os from "os";
20
- import { resolve } from "path";
21
-
22
- const TEST_DATA_DIR = resolve(
23
- os.tmpdir(),
24
- `alvin-list-merged-${process.pid}-${Date.now()}`,
25
- );
26
-
27
- beforeEach(async () => {
28
- if (fs.existsSync(TEST_DATA_DIR)) {
29
- fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
30
- }
31
- fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
32
- process.env.ALVIN_DATA_DIR = TEST_DATA_DIR;
33
- vi.resetModules();
34
-
35
- vi.doMock("../src/services/subagent-delivery.js", () => ({
36
- deliverSubAgentResult: async () => {},
37
- attachBotApi: () => {},
38
- __setBotApiForTest: () => {},
39
- }));
40
- });
41
-
42
- afterEach(async () => {
43
- try {
44
- const mod = await import("../src/services/async-agent-watcher.js");
45
- mod.stopWatcher();
46
- mod.__resetForTest();
47
- } catch {}
48
- });
49
-
50
- describe("listActiveSubAgents merged view (v4.14.1)", () => {
51
- it("returns empty list when neither registry has agents", async () => {
52
- const mod = await import("../src/services/subagents.js");
53
- expect(await mod.listActiveSubAgents()).toEqual([]);
54
- });
55
-
56
- it("includes async-agent-watcher pending agents in the merged list", async () => {
57
- const watcher = await import("../src/services/async-agent-watcher.js");
58
- watcher.registerPendingAgent({
59
- agentId: "alvin-abc123",
60
- outputFile: `${TEST_DATA_DIR}/out.jsonl`,
61
- description: "Research Higgsfield",
62
- prompt: "...",
63
- chatId: "C012SLACK",
64
- userId: "U123",
65
- toolUseId: null,
66
- sessionKey: "slack:C012SLACK",
67
- platform: "slack",
68
- });
69
-
70
- const mod = await import("../src/services/subagents.js");
71
- const agents = await mod.listActiveSubAgents();
72
- expect(agents).toHaveLength(1);
73
- expect(agents[0].id).toBe("alvin-abc123");
74
- expect(agents[0].name).toBe("Research Higgsfield");
75
- expect(agents[0].status).toBe("running");
76
- expect(agents[0].depth).toBe(0);
77
- expect(agents[0].platform).toBe("slack");
78
- });
79
-
80
- it("merges multiple agents from both registries without dupes", async () => {
81
- const watcher = await import("../src/services/async-agent-watcher.js");
82
- watcher.registerPendingAgent({
83
- agentId: "alvin-one",
84
- outputFile: `${TEST_DATA_DIR}/a.jsonl`,
85
- description: "Agent One",
86
- prompt: "p",
87
- chatId: 42,
88
- userId: 42,
89
- toolUseId: null,
90
- sessionKey: "s",
91
- platform: "telegram",
92
- });
93
- watcher.registerPendingAgent({
94
- agentId: "alvin-two",
95
- outputFile: `${TEST_DATA_DIR}/b.jsonl`,
96
- description: "Agent Two",
97
- prompt: "p",
98
- chatId: 42,
99
- userId: 42,
100
- toolUseId: null,
101
- sessionKey: "s",
102
- platform: "telegram",
103
- });
104
-
105
- const mod = await import("../src/services/subagents.js");
106
- const agents = await mod.listActiveSubAgents();
107
- const ids = agents.map((a) => a.id).sort();
108
- expect(ids).toEqual(["alvin-one", "alvin-two"]);
109
- });
110
-
111
- it("preserves startedAt timestamp for age rendering", async () => {
112
- const fixedTs = Date.now() - 45_000; // 45 seconds ago
113
- const watcher = await import("../src/services/async-agent-watcher.js");
114
- watcher.registerPendingAgent({
115
- agentId: "alvin-aged",
116
- outputFile: `${TEST_DATA_DIR}/aged.jsonl`,
117
- description: "Old agent",
118
- prompt: "p",
119
- chatId: 1,
120
- userId: 1,
121
- toolUseId: null,
122
- sessionKey: "s",
123
- platform: "slack",
124
- });
125
-
126
- const mod = await import("../src/services/subagents.js");
127
- const agents = await mod.listActiveSubAgents();
128
- expect(agents[0].startedAt).toBeGreaterThan(fixedTs - 1000);
129
- expect(agents[0].startedAt).toBeLessThan(Date.now() + 1000);
130
- });
131
-
132
- it("tags async dispatch agents with source='cron' (matches v4.12 banner format)", async () => {
133
- const watcher = await import("../src/services/async-agent-watcher.js");
134
- watcher.registerPendingAgent({
135
- agentId: "alvin-sourced",
136
- outputFile: `${TEST_DATA_DIR}/s.jsonl`,
137
- description: "sourced",
138
- prompt: "p",
139
- chatId: 1,
140
- userId: 1,
141
- toolUseId: null,
142
- sessionKey: "s",
143
- platform: "telegram",
144
- });
145
- const mod = await import("../src/services/subagents.js");
146
- const agents = await mod.listActiveSubAgents();
147
- // source='cron' = the ⏰ badge in /subagents list rendering. Matches
148
- // the existing v4.12.x watcher delivery's SubAgentInfo.source value.
149
- expect(agents[0].source).toBe("cron");
150
- });
151
-
152
- it("listSubAgents() (v4.0.0 API) is unchanged and doesn't include pending dispatches", async () => {
153
- const watcher = await import("../src/services/async-agent-watcher.js");
154
- watcher.registerPendingAgent({
155
- agentId: "alvin-isolated",
156
- outputFile: `${TEST_DATA_DIR}/iso.jsonl`,
157
- description: "isolated",
158
- prompt: "p",
159
- chatId: 1,
160
- userId: 1,
161
- toolUseId: null,
162
- sessionKey: "s",
163
- platform: "telegram",
164
- });
165
- const mod = await import("../src/services/subagents.js");
166
- // The original listSubAgents is kept pure — only the merged helper
167
- // returns combined results. Cancel/result paths still use the
168
- // bot-level registry.
169
- expect(mod.listSubAgents()).toHaveLength(0);
170
- expect(await mod.listActiveSubAgents()).toHaveLength(1);
171
- });
172
- });
@@ -1,151 +0,0 @@
1
- /**
2
- * v4.11.0 — Auto-fact-extraction.
3
- *
4
- * When compaction archives messages, instead of just dumping prose into
5
- * the daily log, run a structured extraction pass that pulls user_facts,
6
- * preferences, and decisions out of the chunk and appends them to MEMORY.md
7
- * (de-duplicated by exact-string match).
8
- *
9
- * Marked experimental in v4.11.0. Opt out via MEMORY_EXTRACTION_DISABLED=1.
10
- */
11
- import { describe, it, expect, beforeEach, vi } from "vitest";
12
- import fs from "fs";
13
- import os from "os";
14
- import { resolve } from "path";
15
-
16
- const TEST_DATA_DIR = resolve(os.tmpdir(), `alvin-mem-extract-${process.pid}-${Date.now()}`);
17
-
18
- beforeEach(() => {
19
- if (fs.existsSync(TEST_DATA_DIR)) fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
20
- fs.mkdirSync(resolve(TEST_DATA_DIR, "memory"), { recursive: true });
21
- process.env.ALVIN_DATA_DIR = TEST_DATA_DIR;
22
- delete process.env.MEMORY_EXTRACTION_DISABLED;
23
- vi.resetModules();
24
- });
25
-
26
- describe("memory-extractor (v4.11.0)", () => {
27
- it("parseExtractedFacts handles a clean JSON response", async () => {
28
- const { parseExtractedFacts } = await import("../src/services/memory-extractor.js");
29
- const json = JSON.stringify({
30
- user_facts: ["User User lives in Berlin"],
31
- preferences: ["Replies in German"],
32
- decisions: ["Use VPS VPS for production"],
33
- });
34
- const facts = parseExtractedFacts(json);
35
- expect(facts.user_facts).toEqual(["User User lives in Berlin"]);
36
- expect(facts.preferences).toEqual(["Replies in German"]);
37
- expect(facts.decisions).toEqual(["Use VPS VPS for production"]);
38
- });
39
-
40
- it("parseExtractedFacts handles JSON wrapped in markdown code fences", async () => {
41
- const { parseExtractedFacts } = await import("../src/services/memory-extractor.js");
42
- const wrapped = "```json\n" + JSON.stringify({
43
- user_facts: ["fact 1"],
44
- }) + "\n```";
45
- const facts = parseExtractedFacts(wrapped);
46
- expect(facts.user_facts).toEqual(["fact 1"]);
47
- });
48
-
49
- it("parseExtractedFacts handles JSON with surrounding prose", async () => {
50
- const { parseExtractedFacts } = await import("../src/services/memory-extractor.js");
51
- const messy = `Sure, here are the extracted facts:
52
- ${JSON.stringify({ user_facts: ["x"], preferences: [], decisions: [] })}
53
- Hope this helps!`;
54
- const facts = parseExtractedFacts(messy);
55
- expect(facts.user_facts).toEqual(["x"]);
56
- });
57
-
58
- it("parseExtractedFacts returns empty arrays on garbage input", async () => {
59
- const { parseExtractedFacts } = await import("../src/services/memory-extractor.js");
60
- const facts = parseExtractedFacts("not json at all");
61
- expect(facts.user_facts).toEqual([]);
62
- expect(facts.preferences).toEqual([]);
63
- expect(facts.decisions).toEqual([]);
64
- });
65
-
66
- it("parseExtractedFacts filters non-string entries from arrays", async () => {
67
- const { parseExtractedFacts } = await import("../src/services/memory-extractor.js");
68
- const messy = JSON.stringify({
69
- user_facts: ["good", 42, null, "good2"],
70
- preferences: [],
71
- decisions: [],
72
- });
73
- const facts = parseExtractedFacts(messy);
74
- expect(facts.user_facts).toEqual(["good", "good2"]);
75
- });
76
-
77
- it("appendFactsToMemoryFile writes new facts under structured headers", async () => {
78
- const { appendFactsToMemoryFile } = await import("../src/services/memory-extractor.js");
79
- await appendFactsToMemoryFile({
80
- user_facts: ["User uses launchd for the bot"],
81
- preferences: [],
82
- decisions: ["v4.11.0 ships memory persistence"],
83
- });
84
- const memFile = resolve(TEST_DATA_DIR, "memory", "MEMORY.md");
85
- expect(fs.existsSync(memFile)).toBe(true);
86
- const content = fs.readFileSync(memFile, "utf-8");
87
- expect(content).toMatch(/User uses launchd for the bot/);
88
- expect(content).toMatch(/v4\.11\.0 ships memory persistence/);
89
- });
90
-
91
- it("appendFactsToMemoryFile dedupes on exact-string match", async () => {
92
- const { appendFactsToMemoryFile } = await import("../src/services/memory-extractor.js");
93
- await appendFactsToMemoryFile({
94
- user_facts: ["User uses launchd for the bot"],
95
- preferences: [],
96
- decisions: [],
97
- });
98
- await appendFactsToMemoryFile({
99
- user_facts: ["User uses launchd for the bot", "User drinks coffee"],
100
- preferences: [],
101
- decisions: [],
102
- });
103
- const content = fs.readFileSync(resolve(TEST_DATA_DIR, "memory", "MEMORY.md"), "utf-8");
104
- const matches = content.match(/User uses launchd for the bot/g);
105
- expect(matches).toHaveLength(1); // not duplicated
106
- expect(content).toMatch(/User drinks coffee/);
107
- });
108
-
109
- it("appendFactsToMemoryFile returns 0 when all facts are duplicates", async () => {
110
- const { appendFactsToMemoryFile } = await import("../src/services/memory-extractor.js");
111
- await appendFactsToMemoryFile({
112
- user_facts: ["unique fact"],
113
- preferences: [],
114
- decisions: [],
115
- });
116
- const stored = await appendFactsToMemoryFile({
117
- user_facts: ["unique fact"],
118
- preferences: [],
119
- decisions: [],
120
- });
121
- expect(stored).toBe(0);
122
- });
123
-
124
- it("extractAndStoreFacts is a no-op when MEMORY_EXTRACTION_DISABLED=1", async () => {
125
- process.env.MEMORY_EXTRACTION_DISABLED = "1";
126
- vi.resetModules();
127
- const { extractAndStoreFacts } = await import("../src/services/memory-extractor.js");
128
- const result = await extractAndStoreFacts("some conversation text");
129
- expect(result.disabled).toBe(true);
130
- expect(result.factsStored).toBe(0);
131
- });
132
-
133
- it("extractAndStoreFacts returns 0 stored on too-short input", async () => {
134
- const { extractAndStoreFacts } = await import("../src/services/memory-extractor.js");
135
- const result = await extractAndStoreFacts("hi");
136
- expect(result.disabled).toBe(false);
137
- expect(result.factsStored).toBe(0);
138
- });
139
-
140
- it("extractAndStoreFacts gracefully handles AI provider failure", async () => {
141
- // No API keys in test env — provider will fail, extractor should swallow
142
- const { extractAndStoreFacts } = await import("../src/services/memory-extractor.js");
143
- const result = await extractAndStoreFacts(
144
- "This is a long enough conversation about Berlin, Postgres databases, " +
145
- "and how to set up nginx properly. Should be more than 50 characters easily.",
146
- );
147
- expect(result.disabled).toBe(false);
148
- expect(result).toHaveProperty("factsStored");
149
- // Either succeeded or failed silently — but didn't throw
150
- });
151
- });
@@ -1,169 +0,0 @@
1
- /**
2
- * v4.11.0 — Layered memory loader.
3
- *
4
- * Replaces the monolithic MEMORY.md → System Prompt with a structured
5
- * 4-layer architecture (inspired by mempalace's L0–L3 stack):
6
- *
7
- * L0 identity.md — always loaded, ~200 tokens (who the user is)
8
- * L1 preferences.md — always loaded (how to communicate)
9
- * L1 MEMORY.md — backwards-compat: existing curated knowledge
10
- * L2 projects/*.md — loaded on topic match
11
- * L3 daily logs — only via vector search (existing embeddings.ts)
12
- *
13
- * If the new files don't exist, this falls back to the monolithic MEMORY.md
14
- * so existing setups keep working without migration.
15
- */
16
- import { describe, it, expect, beforeEach, 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-mem-layers-${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
- describe("memory-layers (v4.11.0)", () => {
31
- it("returns empty when nothing exists", async () => {
32
- const { loadMemoryLayers } = await import("../src/services/memory-layers.js");
33
- const layered = loadMemoryLayers();
34
- expect(layered.identity).toBe("");
35
- expect(layered.preferences).toBe("");
36
- expect(layered.longTerm).toBe("");
37
- expect(layered.projects).toEqual([]);
38
- });
39
-
40
- it("loads identity.md as L0 always", async () => {
41
- fs.writeFileSync(
42
- resolve(TEST_DATA_DIR, "memory", "identity.md"),
43
- "# Identity\n\nName: Test User\nLocation: Berlin",
44
- );
45
- const { loadMemoryLayers } = await import("../src/services/memory-layers.js");
46
- const layered = loadMemoryLayers();
47
- expect(layered.identity).toMatch(/Test User/);
48
- });
49
-
50
- it("loads preferences.md as L1 always", async () => {
51
- fs.writeFileSync(
52
- resolve(TEST_DATA_DIR, "memory", "preferences.md"),
53
- "- Reply in German\n- No 'Gerne' in responses",
54
- );
55
- const { loadMemoryLayers } = await import("../src/services/memory-layers.js");
56
- const layered = loadMemoryLayers();
57
- expect(layered.preferences).toMatch(/Reply in German/);
58
- });
59
-
60
- it("falls back to monolithic MEMORY.md when split files are missing", async () => {
61
- fs.writeFileSync(
62
- resolve(TEST_DATA_DIR, "memory", "MEMORY.md"),
63
- "# Old monolithic\n\n- Some legacy fact",
64
- );
65
- const { loadMemoryLayers } = await import("../src/services/memory-layers.js");
66
- const layered = loadMemoryLayers();
67
- expect(layered.longTerm).toMatch(/legacy fact/);
68
- });
69
-
70
- it("loads projects/*.md and exposes them with their filename as topic", async () => {
71
- fs.mkdirSync(resolve(TEST_DATA_DIR, "memory", "projects"), { recursive: true });
72
- fs.writeFileSync(
73
- resolve(TEST_DATA_DIR, "memory", "projects", "my-project.md"),
74
- "# my-project\nVPS: 10.0.0.1, runs nginx + pm2",
75
- );
76
- fs.writeFileSync(
77
- resolve(TEST_DATA_DIR, "memory", "projects", "homes.md"),
78
- "# HOMES\nDB: homes_production (Postgres)",
79
- );
80
- const { loadMemoryLayers } = await import("../src/services/memory-layers.js");
81
- const layered = loadMemoryLayers();
82
- expect(layered.projects).toHaveLength(2);
83
- const topics = layered.projects.map(p => p.topic).sort();
84
- expect(topics).toEqual(["homes", "my-project"]);
85
- expect(layered.projects.find(p => p.topic === "homes")?.content).toMatch(/homes_production/);
86
- });
87
-
88
- it("buildLayeredContext returns all L0+L1 plus matching L2 by topic keyword", async () => {
89
- fs.writeFileSync(
90
- resolve(TEST_DATA_DIR, "memory", "identity.md"),
91
- "Name: User",
92
- );
93
- fs.writeFileSync(
94
- resolve(TEST_DATA_DIR, "memory", "preferences.md"),
95
- "Be terse.",
96
- );
97
- fs.mkdirSync(resolve(TEST_DATA_DIR, "memory", "projects"), { recursive: true });
98
- fs.writeFileSync(
99
- resolve(TEST_DATA_DIR, "memory", "projects", "homes.md"),
100
- "HOMES uses Postgres",
101
- );
102
- fs.writeFileSync(
103
- resolve(TEST_DATA_DIR, "memory", "projects", "my-project.md"),
104
- "my-project uses MySQL",
105
- );
106
-
107
- const { buildLayeredContext } = await import("../src/services/memory-layers.js");
108
-
109
- // Query mentions HOMES → only the homes project should be loaded
110
- const ctx = buildLayeredContext("Tell me about HOMES backups");
111
- expect(ctx).toMatch(/Name: User/); // L0
112
- expect(ctx).toMatch(/Be terse/); // L1
113
- expect(ctx).toMatch(/HOMES uses Postgres/); // L2 matched
114
- expect(ctx).not.toMatch(/my-project uses MySQL/); // L2 not matched
115
- });
116
-
117
- it("buildLayeredContext without a query returns L0+L1 only (boot-up brief)", async () => {
118
- fs.writeFileSync(
119
- resolve(TEST_DATA_DIR, "memory", "identity.md"),
120
- "Name: User",
121
- );
122
- fs.writeFileSync(
123
- resolve(TEST_DATA_DIR, "memory", "preferences.md"),
124
- "Be terse.",
125
- );
126
- fs.mkdirSync(resolve(TEST_DATA_DIR, "memory", "projects"), { recursive: true });
127
- fs.writeFileSync(
128
- resolve(TEST_DATA_DIR, "memory", "projects", "homes.md"),
129
- "HOMES uses Postgres",
130
- );
131
-
132
- const { buildLayeredContext } = await import("../src/services/memory-layers.js");
133
- const ctx = buildLayeredContext();
134
- expect(ctx).toMatch(/Name: User/);
135
- expect(ctx).not.toMatch(/Postgres/); // L2 only loaded with a query
136
- });
137
-
138
- it("token budget: layered context truncates long projects to fit budget", async () => {
139
- fs.writeFileSync(
140
- resolve(TEST_DATA_DIR, "memory", "identity.md"),
141
- "Name: User",
142
- );
143
- fs.mkdirSync(resolve(TEST_DATA_DIR, "memory", "projects"), { recursive: true });
144
- const longContent = "homes ".repeat(2000); // ~10000 chars
145
- fs.writeFileSync(
146
- resolve(TEST_DATA_DIR, "memory", "projects", "homes.md"),
147
- longContent,
148
- );
149
- const { buildLayeredContext } = await import("../src/services/memory-layers.js");
150
- const ctx = buildLayeredContext("HOMES");
151
- // Total context should be capped (~6000 chars max for L0+L1+L2)
152
- expect(ctx.length).toBeLessThan(8000);
153
- });
154
-
155
- it("monolithic MEMORY.md and split files coexist (split takes priority, mono is secondary)", async () => {
156
- fs.writeFileSync(
157
- resolve(TEST_DATA_DIR, "memory", "identity.md"),
158
- "Name: User",
159
- );
160
- fs.writeFileSync(
161
- resolve(TEST_DATA_DIR, "memory", "MEMORY.md"),
162
- "# Legacy\n\n- Old fact still there",
163
- );
164
- const { buildLayeredContext } = await import("../src/services/memory-layers.js");
165
- const ctx = buildLayeredContext("anything");
166
- expect(ctx).toMatch(/Name: User/); // L0
167
- expect(ctx).toMatch(/Old fact still there/); // legacy still included
168
- });
169
- });