@vellumai/assistant 0.5.2 → 0.5.4

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 (144) hide show
  1. package/ARCHITECTURE.md +109 -0
  2. package/docs/architecture/memory.md +105 -0
  3. package/docs/skills.md +100 -0
  4. package/package.json +1 -1
  5. package/src/__tests__/archive-recall.test.ts +560 -0
  6. package/src/__tests__/conversation-agent-loop-overflow.test.ts +7 -0
  7. package/src/__tests__/conversation-agent-loop.test.ts +7 -0
  8. package/src/__tests__/conversation-clear-safety.test.ts +259 -0
  9. package/src/__tests__/conversation-memory-dirty-tail.test.ts +150 -0
  10. package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
  11. package/src/__tests__/conversation-switch-memory-reduction.test.ts +474 -0
  12. package/src/__tests__/conversation-wipe.test.ts +226 -0
  13. package/src/__tests__/db-memory-archive-migration.test.ts +372 -0
  14. package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
  15. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -0
  16. package/src/__tests__/db-schedule-syntax-migration.test.ts +3 -0
  17. package/src/__tests__/inline-command-runner.test.ts +311 -0
  18. package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
  19. package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
  20. package/src/__tests__/list-messages-attachments.test.ts +96 -0
  21. package/src/__tests__/memory-brief-open-loops.test.ts +530 -0
  22. package/src/__tests__/memory-brief-time.test.ts +285 -0
  23. package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
  24. package/src/__tests__/memory-chunk-archive.test.ts +400 -0
  25. package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
  26. package/src/__tests__/memory-episode-archive.test.ts +370 -0
  27. package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
  28. package/src/__tests__/memory-observation-archive.test.ts +375 -0
  29. package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
  30. package/src/__tests__/memory-recall-quality.test.ts +2 -2
  31. package/src/__tests__/memory-reducer-job.test.ts +538 -0
  32. package/src/__tests__/memory-reducer-scheduling.test.ts +473 -0
  33. package/src/__tests__/memory-reducer-store.test.ts +728 -0
  34. package/src/__tests__/memory-reducer-types.test.ts +707 -0
  35. package/src/__tests__/memory-reducer.test.ts +704 -0
  36. package/src/__tests__/memory-regressions.test.ts +30 -8
  37. package/src/__tests__/memory-simplified-config.test.ts +281 -0
  38. package/src/__tests__/parse-identity-fields.test.ts +129 -0
  39. package/src/__tests__/simplified-memory-e2e.test.ts +666 -0
  40. package/src/__tests__/simplified-memory-runtime.test.ts +616 -0
  41. package/src/__tests__/skill-load-inline-command.test.ts +598 -0
  42. package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
  43. package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
  44. package/src/__tests__/skills-transitive-hash.test.ts +333 -0
  45. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +320 -0
  46. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +4 -4
  47. package/src/cli/commands/conversations.ts +18 -0
  48. package/src/config/bundled-skills/app-builder/SKILL.md +8 -8
  49. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  50. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  51. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  52. package/src/config/feature-flag-registry.json +16 -0
  53. package/src/config/raw-config-utils.ts +28 -0
  54. package/src/config/schema.ts +12 -0
  55. package/src/config/schemas/memory-simplified.ts +101 -0
  56. package/src/config/schemas/memory.ts +4 -0
  57. package/src/config/skills.ts +50 -4
  58. package/src/daemon/conversation-agent-loop-handlers.ts +8 -3
  59. package/src/daemon/conversation-agent-loop.ts +71 -1
  60. package/src/daemon/conversation-lifecycle.ts +11 -1
  61. package/src/daemon/conversation-memory.ts +117 -0
  62. package/src/daemon/conversation-runtime-assembly.ts +3 -1
  63. package/src/daemon/conversation-surfaces.ts +31 -8
  64. package/src/daemon/conversation.ts +40 -23
  65. package/src/daemon/handlers/config-embeddings.ts +10 -2
  66. package/src/daemon/handlers/config-model.ts +0 -9
  67. package/src/daemon/handlers/conversations.ts +11 -0
  68. package/src/daemon/handlers/identity.ts +12 -1
  69. package/src/daemon/lifecycle.ts +52 -1
  70. package/src/daemon/message-types/conversations.ts +0 -1
  71. package/src/daemon/server.ts +1 -1
  72. package/src/followups/followup-store.ts +47 -1
  73. package/src/memory/archive-recall.ts +516 -0
  74. package/src/memory/archive-store.ts +400 -0
  75. package/src/memory/brief-formatting.ts +33 -0
  76. package/src/memory/brief-open-loops.ts +266 -0
  77. package/src/memory/brief-time.ts +162 -0
  78. package/src/memory/brief.ts +75 -0
  79. package/src/memory/conversation-crud.ts +455 -101
  80. package/src/memory/conversation-key-store.ts +33 -4
  81. package/src/memory/db-init.ts +16 -0
  82. package/src/memory/indexer.ts +106 -15
  83. package/src/memory/job-handlers/backfill-simplified-memory.ts +462 -0
  84. package/src/memory/job-handlers/conversation-starters.ts +9 -3
  85. package/src/memory/job-handlers/embedding.test.ts +1 -0
  86. package/src/memory/job-handlers/embedding.ts +83 -0
  87. package/src/memory/job-handlers/reduce-conversation-memory.ts +229 -0
  88. package/src/memory/job-utils.ts +1 -1
  89. package/src/memory/jobs-store.ts +8 -0
  90. package/src/memory/jobs-worker.ts +20 -0
  91. package/src/memory/migrations/036-normalize-phone-identities.ts +49 -14
  92. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +9 -1
  93. package/src/memory/migrations/141-rename-verification-table.ts +8 -0
  94. package/src/memory/migrations/142-rename-verification-session-id-column.ts +7 -2
  95. package/src/memory/migrations/174-rename-thread-starters-table.ts +8 -0
  96. package/src/memory/migrations/185-memory-brief-state.ts +52 -0
  97. package/src/memory/migrations/186-memory-archive.ts +109 -0
  98. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
  99. package/src/memory/migrations/188-schedule-quiet-flag.ts +13 -0
  100. package/src/memory/migrations/index.ts +4 -0
  101. package/src/memory/qdrant-client.ts +23 -4
  102. package/src/memory/reducer-scheduler.ts +242 -0
  103. package/src/memory/reducer-store.ts +271 -0
  104. package/src/memory/reducer-types.ts +106 -0
  105. package/src/memory/reducer.ts +467 -0
  106. package/src/memory/schema/conversations.ts +3 -0
  107. package/src/memory/schema/index.ts +2 -0
  108. package/src/memory/schema/infrastructure.ts +1 -0
  109. package/src/memory/schema/memory-archive.ts +121 -0
  110. package/src/memory/schema/memory-brief.ts +55 -0
  111. package/src/memory/search/semantic.ts +17 -4
  112. package/src/oauth/oauth-store.ts +3 -1
  113. package/src/permissions/checker.ts +89 -6
  114. package/src/permissions/defaults.ts +14 -0
  115. package/src/runtime/auth/route-policy.ts +10 -1
  116. package/src/runtime/routes/conversation-management-routes.ts +94 -2
  117. package/src/runtime/routes/conversation-query-routes.ts +7 -0
  118. package/src/runtime/routes/conversation-routes.ts +52 -5
  119. package/src/runtime/routes/guardian-bootstrap-routes.ts +19 -7
  120. package/src/runtime/routes/identity-routes.ts +2 -35
  121. package/src/runtime/routes/llm-context-normalization.ts +14 -1
  122. package/src/runtime/routes/memory-item-routes.ts +90 -5
  123. package/src/runtime/routes/secret-routes.ts +3 -0
  124. package/src/runtime/routes/surface-action-routes.ts +68 -1
  125. package/src/schedule/schedule-store.ts +28 -0
  126. package/src/schedule/scheduler.ts +6 -2
  127. package/src/skills/inline-command-expansions.ts +204 -0
  128. package/src/skills/inline-command-render.ts +127 -0
  129. package/src/skills/inline-command-runner.ts +242 -0
  130. package/src/skills/transitive-version-hash.ts +88 -0
  131. package/src/tasks/task-store.ts +43 -1
  132. package/src/telemetry/usage-telemetry-reporter.ts +1 -1
  133. package/src/tools/filesystem/edit.ts +6 -1
  134. package/src/tools/filesystem/read.ts +6 -1
  135. package/src/tools/filesystem/write.ts +6 -1
  136. package/src/tools/memory/handlers.ts +129 -1
  137. package/src/tools/permission-checker.ts +8 -1
  138. package/src/tools/schedule/create.ts +3 -0
  139. package/src/tools/schedule/list.ts +5 -1
  140. package/src/tools/schedule/update.ts +6 -0
  141. package/src/tools/skills/load.ts +140 -6
  142. package/src/util/platform.ts +18 -0
  143. package/src/workspace/migrations/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +1 -1
  144. package/src/workspace/migrations/registry.ts +1 -1
@@ -540,13 +540,23 @@ describe("Memory regressions", () => {
540
540
 
541
541
  test("memory_save sets verificationState to user_confirmed", async () => {
542
542
  const { handleMemorySave } = await import("../tools/memory/handlers.js");
543
+ const legacyConfig = {
544
+ ...DEFAULT_CONFIG,
545
+ memory: {
546
+ ...DEFAULT_CONFIG.memory,
547
+ simplified: {
548
+ ...DEFAULT_CONFIG.memory.simplified,
549
+ enabled: false,
550
+ },
551
+ },
552
+ };
543
553
 
544
554
  const result = await handleMemorySave(
545
555
  {
546
556
  statement: "User explicitly saved this preference",
547
557
  kind: "preference",
548
558
  },
549
- DEFAULT_CONFIG,
559
+ legacyConfig,
550
560
  "conv-verify-save",
551
561
  "msg-verify-save",
552
562
  );
@@ -563,13 +573,23 @@ describe("Memory regressions", () => {
563
573
 
564
574
  test("memory_save in different scopes creates separate items", async () => {
565
575
  const { handleMemorySave } = await import("../tools/memory/handlers.js");
576
+ const legacyConfig = {
577
+ ...DEFAULT_CONFIG,
578
+ memory: {
579
+ ...DEFAULT_CONFIG.memory,
580
+ simplified: {
581
+ ...DEFAULT_CONFIG.memory.simplified,
582
+ enabled: false,
583
+ },
584
+ },
585
+ };
566
586
 
567
587
  const sharedArgs = { statement: "I prefer dark mode", kind: "preference" };
568
588
 
569
589
  // Save in the default scope
570
590
  const r1 = await handleMemorySave(
571
591
  sharedArgs,
572
- DEFAULT_CONFIG,
592
+ legacyConfig,
573
593
  "conv-scope-1",
574
594
  "msg-scope-1",
575
595
  "default",
@@ -580,7 +600,7 @@ describe("Memory regressions", () => {
580
600
  // Save the identical statement in a private scope
581
601
  const r2 = await handleMemorySave(
582
602
  sharedArgs,
583
- DEFAULT_CONFIG,
603
+ legacyConfig,
584
604
  "conv-scope-2",
585
605
  "msg-scope-2",
586
606
  "private-abc",
@@ -604,7 +624,7 @@ describe("Memory regressions", () => {
604
624
  // Saving the same statement again in default scope should dedup (not create a third)
605
625
  const r3 = await handleMemorySave(
606
626
  sharedArgs,
607
- DEFAULT_CONFIG,
627
+ legacyConfig,
608
628
  "conv-scope-3",
609
629
  "msg-scope-3",
610
630
  "default",
@@ -3207,8 +3227,9 @@ describe("Memory regressions", () => {
3207
3227
  .filter((j) => JSON.parse(j.payload).messageId === "msg-untrusted-gate");
3208
3228
  expect(extractJobs.length).toBe(0);
3209
3229
 
3210
- // enqueuedJobs should reflect: embed jobs + summary (1), no extract (0)
3211
- const expectedJobs = result.indexedSegments + 1; // embed per segment + summary
3230
+ // enqueuedJobs reflects legacy embed_segment + archive embed_chunk per
3231
+ // segment, plus the summary job, with extract_items gated off.
3232
+ const expectedJobs = result.indexedSegments * 2 + 1;
3212
3233
  expect(result.enqueuedJobs).toBe(expectedJobs);
3213
3234
  });
3214
3235
 
@@ -3389,8 +3410,9 @@ describe("Memory regressions", () => {
3389
3410
  .filter((j) => JSON.parse(j.payload).messageId === "msg-unverified-gate");
3390
3411
  expect(extractJobs.length).toBe(0);
3391
3412
 
3392
- // enqueuedJobs should reflect: embed jobs + summary (1), no extract (0)
3393
- const expectedJobs = result.indexedSegments + 1; // embed per segment + summary
3413
+ // enqueuedJobs reflects legacy embed_segment + archive embed_chunk per
3414
+ // segment, plus the summary job, with extract_items gated off.
3415
+ const expectedJobs = result.indexedSegments * 2 + 1;
3394
3416
  expect(result.enqueuedJobs).toBe(expectedJobs);
3395
3417
  });
3396
3418
 
@@ -0,0 +1,281 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Mocks — declared before imports that depend on platform/logger
9
+ // ---------------------------------------------------------------------------
10
+
11
+ const TEST_DIR = join(
12
+ tmpdir(),
13
+ `vellum-simplified-mem-test-${randomBytes(4).toString("hex")}`,
14
+ );
15
+ const WORKSPACE_DIR = join(TEST_DIR, "workspace");
16
+ const CONFIG_PATH = join(WORKSPACE_DIR, "config.json");
17
+
18
+ function ensureTestDir(): void {
19
+ const dirs = [
20
+ TEST_DIR,
21
+ WORKSPACE_DIR,
22
+ join(TEST_DIR, "data"),
23
+ join(TEST_DIR, "memory"),
24
+ join(TEST_DIR, "memory", "knowledge"),
25
+ join(TEST_DIR, "logs"),
26
+ ];
27
+ for (const dir of dirs) {
28
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
29
+ }
30
+ }
31
+
32
+ function makeLoggerStub(): Record<string, unknown> {
33
+ const stub: Record<string, unknown> = {};
34
+ for (const m of [
35
+ "info",
36
+ "warn",
37
+ "error",
38
+ "debug",
39
+ "trace",
40
+ "fatal",
41
+ "silent",
42
+ "child",
43
+ ]) {
44
+ stub[m] = m === "child" ? () => makeLoggerStub() : () => {};
45
+ }
46
+ return stub;
47
+ }
48
+
49
+ mock.module("../util/logger.js", () => ({
50
+ getLogger: () => makeLoggerStub(),
51
+ }));
52
+
53
+ mock.module("../util/platform.js", () => ({
54
+ getRootDir: () => TEST_DIR,
55
+ getWorkspaceDir: () => WORKSPACE_DIR,
56
+ getWorkspaceConfigPath: () => CONFIG_PATH,
57
+ getDataDir: () => join(TEST_DIR, "data"),
58
+ getLogPath: () => join(TEST_DIR, "logs", "vellum.log"),
59
+ ensureDataDir: () => ensureTestDir(),
60
+ isMacOS: () => false,
61
+ isLinux: () => false,
62
+ isWindows: () => false,
63
+ }));
64
+
65
+ import { invalidateConfigCache, loadConfig } from "../config/loader.js";
66
+ import { AssistantConfigSchema } from "../config/schema.js";
67
+ import { MemorySimplifiedConfigSchema } from "../config/schemas/memory-simplified.js";
68
+ import { _setStorePath } from "../security/encrypted-store.js";
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Helpers
72
+ // ---------------------------------------------------------------------------
73
+
74
+ function writeConfig(obj: unknown): void {
75
+ writeFileSync(CONFIG_PATH, JSON.stringify(obj));
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Tests: MemorySimplifiedConfigSchema (unit)
80
+ // ---------------------------------------------------------------------------
81
+
82
+ describe("MemorySimplifiedConfigSchema", () => {
83
+ test("parses empty object with all defaults", () => {
84
+ const result = MemorySimplifiedConfigSchema.parse({});
85
+ expect(result).toEqual({
86
+ enabled: true,
87
+ brief: { maxTokens: 4000 },
88
+ reducer: { idleDelayMs: 30_000, switchWaitMs: 5_000 },
89
+ archiveRecall: { maxSnippets: 10 },
90
+ });
91
+ });
92
+
93
+ test("accepts explicit enabled=true", () => {
94
+ const result = MemorySimplifiedConfigSchema.parse({ enabled: true });
95
+ expect(result.enabled).toBe(true);
96
+ });
97
+
98
+ test("accepts custom brief.maxTokens", () => {
99
+ const result = MemorySimplifiedConfigSchema.parse({
100
+ brief: { maxTokens: 8000 },
101
+ });
102
+ expect(result.brief.maxTokens).toBe(8000);
103
+ });
104
+
105
+ test("accepts custom reducer values", () => {
106
+ const result = MemorySimplifiedConfigSchema.parse({
107
+ reducer: { idleDelayMs: 60_000, switchWaitMs: 10_000 },
108
+ });
109
+ expect(result.reducer.idleDelayMs).toBe(60_000);
110
+ expect(result.reducer.switchWaitMs).toBe(10_000);
111
+ });
112
+
113
+ test("accepts custom archiveRecall.maxSnippets", () => {
114
+ const result = MemorySimplifiedConfigSchema.parse({
115
+ archiveRecall: { maxSnippets: 20 },
116
+ });
117
+ expect(result.archiveRecall.maxSnippets).toBe(20);
118
+ });
119
+
120
+ test("rejects non-boolean enabled", () => {
121
+ const result = MemorySimplifiedConfigSchema.safeParse({
122
+ enabled: "yes",
123
+ });
124
+ expect(result.success).toBe(false);
125
+ });
126
+
127
+ test("rejects non-positive brief.maxTokens", () => {
128
+ const result = MemorySimplifiedConfigSchema.safeParse({
129
+ brief: { maxTokens: 0 },
130
+ });
131
+ expect(result.success).toBe(false);
132
+ });
133
+
134
+ test("rejects non-integer brief.maxTokens", () => {
135
+ const result = MemorySimplifiedConfigSchema.safeParse({
136
+ brief: { maxTokens: 3.5 },
137
+ });
138
+ expect(result.success).toBe(false);
139
+ });
140
+
141
+ test("rejects non-positive reducer.idleDelayMs", () => {
142
+ const result = MemorySimplifiedConfigSchema.safeParse({
143
+ reducer: { idleDelayMs: 0 },
144
+ });
145
+ expect(result.success).toBe(false);
146
+ });
147
+
148
+ test("rejects non-positive reducer.switchWaitMs", () => {
149
+ const result = MemorySimplifiedConfigSchema.safeParse({
150
+ reducer: { switchWaitMs: -1 },
151
+ });
152
+ expect(result.success).toBe(false);
153
+ });
154
+
155
+ test("rejects non-positive archiveRecall.maxSnippets", () => {
156
+ const result = MemorySimplifiedConfigSchema.safeParse({
157
+ archiveRecall: { maxSnippets: 0 },
158
+ });
159
+ expect(result.success).toBe(false);
160
+ });
161
+
162
+ test("rejects non-integer archiveRecall.maxSnippets", () => {
163
+ const result = MemorySimplifiedConfigSchema.safeParse({
164
+ archiveRecall: { maxSnippets: 2.5 },
165
+ });
166
+ expect(result.success).toBe(false);
167
+ });
168
+ });
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // Tests: Wired into AssistantConfigSchema
172
+ // ---------------------------------------------------------------------------
173
+
174
+ describe("AssistantConfigSchema memory.simplified", () => {
175
+ test("empty config exposes memory.simplified with defaults", () => {
176
+ const result = AssistantConfigSchema.parse({});
177
+ expect(result.memory.simplified).toEqual({
178
+ enabled: true,
179
+ brief: { maxTokens: 4000 },
180
+ reducer: { idleDelayMs: 30_000, switchWaitMs: 5_000 },
181
+ archiveRecall: { maxSnippets: 10 },
182
+ });
183
+ });
184
+
185
+ test("memory.simplified does not disturb legacy memory config", () => {
186
+ const result = AssistantConfigSchema.parse({});
187
+ // Legacy fields still present with their defaults
188
+ expect(result.memory.enabled).toBe(true);
189
+ expect(result.memory.retrieval).toBeDefined();
190
+ expect(result.memory.jobs).toBeDefined();
191
+ expect(result.memory.cleanup).toBeDefined();
192
+ expect(result.memory.extraction).toBeDefined();
193
+ expect(result.memory.summarization).toBeDefined();
194
+ expect(result.memory.segmentation).toBeDefined();
195
+ expect(result.memory.embeddings).toBeDefined();
196
+ expect(result.memory.qdrant).toBeDefined();
197
+ expect(result.memory.retention).toBeDefined();
198
+ });
199
+
200
+ test("accepts memory.simplified overrides alongside legacy config", () => {
201
+ const result = AssistantConfigSchema.parse({
202
+ memory: {
203
+ enabled: true,
204
+ simplified: {
205
+ enabled: true,
206
+ brief: { maxTokens: 6000 },
207
+ },
208
+ },
209
+ });
210
+ expect(result.memory.enabled).toBe(true);
211
+ expect(result.memory.simplified.enabled).toBe(true);
212
+ expect(result.memory.simplified.brief.maxTokens).toBe(6000);
213
+ // Defaults preserved for unset simplified fields
214
+ expect(result.memory.simplified.reducer.idleDelayMs).toBe(30_000);
215
+ expect(result.memory.simplified.archiveRecall.maxSnippets).toBe(10);
216
+ });
217
+ });
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // Tests: loadConfig integration (empty config file loads cleanly)
221
+ // ---------------------------------------------------------------------------
222
+
223
+ describe("loadConfig with memory.simplified", () => {
224
+ beforeEach(() => {
225
+ ensureTestDir();
226
+ const resetPaths = [
227
+ CONFIG_PATH,
228
+ join(TEST_DIR, "keys.enc"),
229
+ join(TEST_DIR, "data"),
230
+ join(TEST_DIR, "memory"),
231
+ ];
232
+ for (const path of resetPaths) {
233
+ if (existsSync(path)) {
234
+ rmSync(path, { recursive: true, force: true });
235
+ }
236
+ }
237
+ ensureTestDir();
238
+ _setStorePath(join(TEST_DIR, "keys.enc"));
239
+ invalidateConfigCache();
240
+ });
241
+
242
+ afterEach(() => {
243
+ _setStorePath(null);
244
+ invalidateConfigCache();
245
+ });
246
+
247
+ test("empty config file loads cleanly with simplified defaults", () => {
248
+ writeConfig({});
249
+ const config = loadConfig();
250
+ expect(config.memory.simplified).toEqual({
251
+ enabled: true,
252
+ brief: { maxTokens: 4000 },
253
+ reducer: { idleDelayMs: 30_000, switchWaitMs: 5_000 },
254
+ archiveRecall: { maxSnippets: 10 },
255
+ });
256
+ });
257
+
258
+ test("no config file loads cleanly with simplified defaults", () => {
259
+ const config = loadConfig();
260
+ expect(config.memory.simplified).toEqual({
261
+ enabled: true,
262
+ brief: { maxTokens: 4000 },
263
+ reducer: { idleDelayMs: 30_000, switchWaitMs: 5_000 },
264
+ archiveRecall: { maxSnippets: 10 },
265
+ });
266
+ });
267
+
268
+ test("existing memory config with simplified addition loads cleanly", () => {
269
+ writeConfig({
270
+ memory: {
271
+ enabled: true,
272
+ simplified: { enabled: true, brief: { maxTokens: 2000 } },
273
+ },
274
+ });
275
+ const config = loadConfig();
276
+ expect(config.memory.enabled).toBe(true);
277
+ expect(config.memory.simplified.enabled).toBe(true);
278
+ expect(config.memory.simplified.brief.maxTokens).toBe(2000);
279
+ expect(config.memory.simplified.reducer.idleDelayMs).toBe(30_000);
280
+ });
281
+ });
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Unit tests for identity field parsing and template placeholder filtering.
3
+ *
4
+ * Validates that parseIdentityFields correctly extracts real values from
5
+ * IDENTITY.md content while treating template placeholders (e.g.
6
+ * `_(not yet chosen)_`) as empty/unset.
7
+ */
8
+
9
+ import { describe, expect, test } from "bun:test";
10
+
11
+ import {
12
+ isTemplatePlaceholder,
13
+ parseIdentityFields,
14
+ } from "../daemon/handlers/identity.js";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // isTemplatePlaceholder
18
+ // ---------------------------------------------------------------------------
19
+
20
+ describe("isTemplatePlaceholder", () => {
21
+ test("returns true for _(not yet chosen)_", () => {
22
+ expect(isTemplatePlaceholder("_(not yet chosen)_")).toBe(true);
23
+ });
24
+
25
+ test("returns true for _(not yet established)_", () => {
26
+ expect(isTemplatePlaceholder("_(not yet established)_")).toBe(true);
27
+ });
28
+
29
+ test("returns true for any value matching _(…)_ pattern", () => {
30
+ expect(isTemplatePlaceholder("_(something else)_")).toBe(true);
31
+ });
32
+
33
+ test("returns false for normal values", () => {
34
+ expect(isTemplatePlaceholder("Your helpful coding assistant")).toBe(false);
35
+ expect(isTemplatePlaceholder("Jarvis")).toBe(false);
36
+ expect(isTemplatePlaceholder("")).toBe(false);
37
+ });
38
+
39
+ test("returns false for partial matches", () => {
40
+ expect(isTemplatePlaceholder("_(incomplete")).toBe(false);
41
+ expect(isTemplatePlaceholder("incomplete)_")).toBe(false);
42
+ expect(isTemplatePlaceholder("_(")).toBe(false);
43
+ expect(isTemplatePlaceholder(")_")).toBe(false);
44
+ });
45
+ });
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // parseIdentityFields — placeholder filtering
49
+ // ---------------------------------------------------------------------------
50
+
51
+ describe("parseIdentityFields", () => {
52
+ test("returns empty strings for all template placeholder values", () => {
53
+ const content = [
54
+ "- **Name:** _(not yet chosen)_",
55
+ "- **Role:** _(not yet established)_",
56
+ "- **Personality:** _(not yet chosen)_",
57
+ "- **Emoji:** _(not yet chosen)_",
58
+ "- **Home:** _(not yet chosen)_",
59
+ ].join("\n");
60
+
61
+ const fields = parseIdentityFields(content);
62
+ expect(fields.name).toBe("");
63
+ expect(fields.role).toBe("");
64
+ expect(fields.personality).toBe("");
65
+ expect(fields.emoji).toBe("");
66
+ expect(fields.home).toBe("");
67
+ });
68
+
69
+ test("preserves real user-provided values", () => {
70
+ const content = [
71
+ "- **Name:** Jarvis",
72
+ "- **Role:** Coding assistant",
73
+ "- **Personality:** Friendly and helpful",
74
+ "- **Emoji:** 🤖",
75
+ "- **Home:** ~/projects",
76
+ ].join("\n");
77
+
78
+ const fields = parseIdentityFields(content);
79
+ expect(fields.name).toBe("Jarvis");
80
+ expect(fields.role).toBe("Coding assistant");
81
+ expect(fields.personality).toBe("Friendly and helpful");
82
+ expect(fields.emoji).toBe("🤖");
83
+ expect(fields.home).toBe("~/projects");
84
+ });
85
+
86
+ test("handles a mix of real and placeholder values", () => {
87
+ const content = [
88
+ "- **Name:** Jarvis",
89
+ "- **Role:** _(not yet established)_",
90
+ "- **Personality:** Friendly",
91
+ "- **Emoji:** _(not yet chosen)_",
92
+ "- **Home:** ~/dev",
93
+ ].join("\n");
94
+
95
+ const fields = parseIdentityFields(content);
96
+ expect(fields.name).toBe("Jarvis");
97
+ expect(fields.role).toBe("");
98
+ expect(fields.personality).toBe("Friendly");
99
+ expect(fields.emoji).toBe("");
100
+ expect(fields.home).toBe("~/dev");
101
+ });
102
+
103
+ test("returns role: '' when IDENTITY.md contains placeholder role", () => {
104
+ const content = "- **Role:** _(not yet established)_";
105
+ const fields = parseIdentityFields(content);
106
+ expect(fields.role).toBe("");
107
+ });
108
+
109
+ test("returns name: '' when IDENTITY.md contains placeholder name", () => {
110
+ const content = "- **Name:** _(not yet chosen)_";
111
+ const fields = parseIdentityFields(content);
112
+ expect(fields.name).toBe("");
113
+ });
114
+
115
+ test('parses role: "Coding assistant" for real values', () => {
116
+ const content = "- **Role:** Coding assistant";
117
+ const fields = parseIdentityFields(content);
118
+ expect(fields.role).toBe("Coding assistant");
119
+ });
120
+
121
+ test("returns empty strings when content has no identity fields", () => {
122
+ const fields = parseIdentityFields("# Some other content\nHello world");
123
+ expect(fields.name).toBe("");
124
+ expect(fields.role).toBe("");
125
+ expect(fields.personality).toBe("");
126
+ expect(fields.emoji).toBe("");
127
+ expect(fields.home).toBe("");
128
+ });
129
+ });