@vellumai/assistant 0.5.2 → 0.5.3

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 (108) hide show
  1. package/ARCHITECTURE.md +109 -0
  2. package/docs/skills.md +100 -0
  3. package/package.json +1 -1
  4. package/src/__tests__/conversation-agent-loop-overflow.test.ts +7 -0
  5. package/src/__tests__/conversation-agent-loop.test.ts +7 -0
  6. package/src/__tests__/conversation-memory-dirty-tail.test.ts +150 -0
  7. package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
  8. package/src/__tests__/conversation-wipe.test.ts +226 -0
  9. package/src/__tests__/db-memory-archive-migration.test.ts +372 -0
  10. package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
  11. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -0
  12. package/src/__tests__/inline-command-runner.test.ts +311 -0
  13. package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
  14. package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
  15. package/src/__tests__/list-messages-attachments.test.ts +96 -0
  16. package/src/__tests__/memory-brief-open-loops.test.ts +530 -0
  17. package/src/__tests__/memory-brief-time.test.ts +285 -0
  18. package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
  19. package/src/__tests__/memory-chunk-archive.test.ts +400 -0
  20. package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
  21. package/src/__tests__/memory-episode-archive.test.ts +370 -0
  22. package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
  23. package/src/__tests__/memory-observation-archive.test.ts +375 -0
  24. package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
  25. package/src/__tests__/memory-recall-quality.test.ts +2 -2
  26. package/src/__tests__/memory-reducer-store.test.ts +728 -0
  27. package/src/__tests__/memory-reducer-types.test.ts +699 -0
  28. package/src/__tests__/memory-reducer.test.ts +698 -0
  29. package/src/__tests__/memory-regressions.test.ts +6 -4
  30. package/src/__tests__/memory-simplified-config.test.ts +281 -0
  31. package/src/__tests__/parse-identity-fields.test.ts +129 -0
  32. package/src/__tests__/skill-load-inline-command.test.ts +598 -0
  33. package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
  34. package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
  35. package/src/__tests__/skills-transitive-hash.test.ts +333 -0
  36. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +320 -0
  37. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +4 -4
  38. package/src/config/bundled-skills/app-builder/SKILL.md +8 -8
  39. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  40. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  41. package/src/config/feature-flag-registry.json +16 -0
  42. package/src/config/loader.ts +1 -0
  43. package/src/config/raw-config-utils.ts +28 -0
  44. package/src/config/schema.ts +12 -0
  45. package/src/config/schemas/memory-simplified.ts +101 -0
  46. package/src/config/schemas/memory.ts +4 -0
  47. package/src/config/skills.ts +50 -4
  48. package/src/daemon/conversation-agent-loop-handlers.ts +8 -3
  49. package/src/daemon/conversation-agent-loop.ts +71 -1
  50. package/src/daemon/conversation-lifecycle.ts +11 -1
  51. package/src/daemon/conversation-runtime-assembly.ts +2 -1
  52. package/src/daemon/conversation-surfaces.ts +31 -8
  53. package/src/daemon/conversation.ts +40 -23
  54. package/src/daemon/handlers/config-embeddings.ts +10 -2
  55. package/src/daemon/handlers/config-model.ts +0 -9
  56. package/src/daemon/handlers/identity.ts +12 -1
  57. package/src/daemon/lifecycle.ts +9 -1
  58. package/src/daemon/message-types/conversations.ts +0 -1
  59. package/src/daemon/server.ts +1 -1
  60. package/src/followups/followup-store.ts +47 -1
  61. package/src/memory/archive-store.ts +400 -0
  62. package/src/memory/brief-formatting.ts +33 -0
  63. package/src/memory/brief-open-loops.ts +266 -0
  64. package/src/memory/brief-time.ts +161 -0
  65. package/src/memory/brief.ts +75 -0
  66. package/src/memory/conversation-crud.ts +245 -101
  67. package/src/memory/db-init.ts +12 -0
  68. package/src/memory/indexer.ts +106 -15
  69. package/src/memory/job-handlers/embedding.test.ts +1 -0
  70. package/src/memory/job-handlers/embedding.ts +83 -0
  71. package/src/memory/job-utils.ts +1 -1
  72. package/src/memory/jobs-store.ts +6 -0
  73. package/src/memory/jobs-worker.ts +12 -0
  74. package/src/memory/migrations/185-memory-brief-state.ts +52 -0
  75. package/src/memory/migrations/186-memory-archive.ts +109 -0
  76. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
  77. package/src/memory/migrations/index.ts +3 -0
  78. package/src/memory/qdrant-client.ts +23 -4
  79. package/src/memory/reducer-store.ts +271 -0
  80. package/src/memory/reducer-types.ts +99 -0
  81. package/src/memory/reducer.ts +453 -0
  82. package/src/memory/schema/conversations.ts +3 -0
  83. package/src/memory/schema/index.ts +2 -0
  84. package/src/memory/schema/memory-archive.ts +121 -0
  85. package/src/memory/schema/memory-brief.ts +55 -0
  86. package/src/memory/search/semantic.ts +17 -4
  87. package/src/oauth/oauth-store.ts +3 -1
  88. package/src/permissions/checker.ts +89 -6
  89. package/src/permissions/defaults.ts +14 -0
  90. package/src/runtime/routes/conversation-management-routes.ts +6 -0
  91. package/src/runtime/routes/conversation-query-routes.ts +7 -0
  92. package/src/runtime/routes/conversation-routes.ts +52 -5
  93. package/src/runtime/routes/identity-routes.ts +2 -35
  94. package/src/runtime/routes/llm-context-normalization.ts +14 -1
  95. package/src/runtime/routes/memory-item-routes.ts +90 -5
  96. package/src/runtime/routes/secret-routes.ts +2 -0
  97. package/src/runtime/routes/surface-action-routes.ts +68 -1
  98. package/src/schedule/schedule-store.ts +21 -0
  99. package/src/skills/inline-command-expansions.ts +204 -0
  100. package/src/skills/inline-command-render.ts +127 -0
  101. package/src/skills/inline-command-runner.ts +242 -0
  102. package/src/skills/transitive-version-hash.ts +88 -0
  103. package/src/tasks/task-store.ts +43 -1
  104. package/src/tools/permission-checker.ts +8 -1
  105. package/src/tools/skills/load.ts +140 -6
  106. package/src/util/platform.ts +18 -0
  107. package/src/workspace/migrations/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +1 -1
  108. package/src/workspace/migrations/registry.ts +1 -1
@@ -3207,8 +3207,9 @@ describe("Memory regressions", () => {
3207
3207
  .filter((j) => JSON.parse(j.payload).messageId === "msg-untrusted-gate");
3208
3208
  expect(extractJobs.length).toBe(0);
3209
3209
 
3210
- // enqueuedJobs should reflect: embed jobs + summary (1), no extract (0)
3211
- const expectedJobs = result.indexedSegments + 1; // embed per segment + summary
3210
+ // enqueuedJobs reflects legacy embed_segment + archive embed_chunk per
3211
+ // segment, plus the summary job, with extract_items gated off.
3212
+ const expectedJobs = result.indexedSegments * 2 + 1;
3212
3213
  expect(result.enqueuedJobs).toBe(expectedJobs);
3213
3214
  });
3214
3215
 
@@ -3389,8 +3390,9 @@ describe("Memory regressions", () => {
3389
3390
  .filter((j) => JSON.parse(j.payload).messageId === "msg-unverified-gate");
3390
3391
  expect(extractJobs.length).toBe(0);
3391
3392
 
3392
- // enqueuedJobs should reflect: embed jobs + summary (1), no extract (0)
3393
- const expectedJobs = result.indexedSegments + 1; // embed per segment + summary
3393
+ // enqueuedJobs reflects legacy embed_segment + archive embed_chunk per
3394
+ // segment, plus the summary job, with extract_items gated off.
3395
+ const expectedJobs = result.indexedSegments * 2 + 1;
3394
3396
  expect(result.enqueuedJobs).toBe(expectedJobs);
3395
3397
  });
3396
3398
 
@@ -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: false,
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: false,
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: false,
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: false,
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
+ });