@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.
- package/ARCHITECTURE.md +109 -0
- package/docs/architecture/memory.md +105 -0
- package/docs/skills.md +100 -0
- package/package.json +1 -1
- package/src/__tests__/archive-recall.test.ts +560 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +7 -0
- package/src/__tests__/conversation-agent-loop.test.ts +7 -0
- package/src/__tests__/conversation-clear-safety.test.ts +259 -0
- package/src/__tests__/conversation-memory-dirty-tail.test.ts +150 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
- package/src/__tests__/conversation-switch-memory-reduction.test.ts +474 -0
- package/src/__tests__/conversation-wipe.test.ts +226 -0
- package/src/__tests__/db-memory-archive-migration.test.ts +372 -0
- package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
- package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +3 -0
- package/src/__tests__/inline-command-runner.test.ts +311 -0
- package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
- package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
- package/src/__tests__/list-messages-attachments.test.ts +96 -0
- package/src/__tests__/memory-brief-open-loops.test.ts +530 -0
- package/src/__tests__/memory-brief-time.test.ts +285 -0
- package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
- package/src/__tests__/memory-chunk-archive.test.ts +400 -0
- package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
- package/src/__tests__/memory-episode-archive.test.ts +370 -0
- package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
- package/src/__tests__/memory-observation-archive.test.ts +375 -0
- package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
- package/src/__tests__/memory-recall-quality.test.ts +2 -2
- package/src/__tests__/memory-reducer-job.test.ts +538 -0
- package/src/__tests__/memory-reducer-scheduling.test.ts +473 -0
- package/src/__tests__/memory-reducer-store.test.ts +728 -0
- package/src/__tests__/memory-reducer-types.test.ts +707 -0
- package/src/__tests__/memory-reducer.test.ts +704 -0
- package/src/__tests__/memory-regressions.test.ts +30 -8
- package/src/__tests__/memory-simplified-config.test.ts +281 -0
- package/src/__tests__/parse-identity-fields.test.ts +129 -0
- package/src/__tests__/simplified-memory-e2e.test.ts +666 -0
- package/src/__tests__/simplified-memory-runtime.test.ts +616 -0
- package/src/__tests__/skill-load-inline-command.test.ts +598 -0
- package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
- package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
- package/src/__tests__/skills-transitive-hash.test.ts +333 -0
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +320 -0
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +4 -4
- package/src/cli/commands/conversations.ts +18 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +8 -8
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
- package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/raw-config-utils.ts +28 -0
- package/src/config/schema.ts +12 -0
- package/src/config/schemas/memory-simplified.ts +101 -0
- package/src/config/schemas/memory.ts +4 -0
- package/src/config/skills.ts +50 -4
- package/src/daemon/conversation-agent-loop-handlers.ts +8 -3
- package/src/daemon/conversation-agent-loop.ts +71 -1
- package/src/daemon/conversation-lifecycle.ts +11 -1
- package/src/daemon/conversation-memory.ts +117 -0
- package/src/daemon/conversation-runtime-assembly.ts +3 -1
- package/src/daemon/conversation-surfaces.ts +31 -8
- package/src/daemon/conversation.ts +40 -23
- package/src/daemon/handlers/config-embeddings.ts +10 -2
- package/src/daemon/handlers/config-model.ts +0 -9
- package/src/daemon/handlers/conversations.ts +11 -0
- package/src/daemon/handlers/identity.ts +12 -1
- package/src/daemon/lifecycle.ts +52 -1
- package/src/daemon/message-types/conversations.ts +0 -1
- package/src/daemon/server.ts +1 -1
- package/src/followups/followup-store.ts +47 -1
- package/src/memory/archive-recall.ts +516 -0
- package/src/memory/archive-store.ts +400 -0
- package/src/memory/brief-formatting.ts +33 -0
- package/src/memory/brief-open-loops.ts +266 -0
- package/src/memory/brief-time.ts +162 -0
- package/src/memory/brief.ts +75 -0
- package/src/memory/conversation-crud.ts +455 -101
- package/src/memory/conversation-key-store.ts +33 -4
- package/src/memory/db-init.ts +16 -0
- package/src/memory/indexer.ts +106 -15
- package/src/memory/job-handlers/backfill-simplified-memory.ts +462 -0
- package/src/memory/job-handlers/conversation-starters.ts +9 -3
- package/src/memory/job-handlers/embedding.test.ts +1 -0
- package/src/memory/job-handlers/embedding.ts +83 -0
- package/src/memory/job-handlers/reduce-conversation-memory.ts +229 -0
- package/src/memory/job-utils.ts +1 -1
- package/src/memory/jobs-store.ts +8 -0
- package/src/memory/jobs-worker.ts +20 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +49 -14
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +9 -1
- package/src/memory/migrations/141-rename-verification-table.ts +8 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +7 -2
- package/src/memory/migrations/174-rename-thread-starters-table.ts +8 -0
- package/src/memory/migrations/185-memory-brief-state.ts +52 -0
- package/src/memory/migrations/186-memory-archive.ts +109 -0
- package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
- package/src/memory/migrations/188-schedule-quiet-flag.ts +13 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/qdrant-client.ts +23 -4
- package/src/memory/reducer-scheduler.ts +242 -0
- package/src/memory/reducer-store.ts +271 -0
- package/src/memory/reducer-types.ts +106 -0
- package/src/memory/reducer.ts +467 -0
- package/src/memory/schema/conversations.ts +3 -0
- package/src/memory/schema/index.ts +2 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/schema/memory-archive.ts +121 -0
- package/src/memory/schema/memory-brief.ts +55 -0
- package/src/memory/search/semantic.ts +17 -4
- package/src/oauth/oauth-store.ts +3 -1
- package/src/permissions/checker.ts +89 -6
- package/src/permissions/defaults.ts +14 -0
- package/src/runtime/auth/route-policy.ts +10 -1
- package/src/runtime/routes/conversation-management-routes.ts +94 -2
- package/src/runtime/routes/conversation-query-routes.ts +7 -0
- package/src/runtime/routes/conversation-routes.ts +52 -5
- package/src/runtime/routes/guardian-bootstrap-routes.ts +19 -7
- package/src/runtime/routes/identity-routes.ts +2 -35
- package/src/runtime/routes/llm-context-normalization.ts +14 -1
- package/src/runtime/routes/memory-item-routes.ts +90 -5
- package/src/runtime/routes/secret-routes.ts +3 -0
- package/src/runtime/routes/surface-action-routes.ts +68 -1
- package/src/schedule/schedule-store.ts +28 -0
- package/src/schedule/scheduler.ts +6 -2
- package/src/skills/inline-command-expansions.ts +204 -0
- package/src/skills/inline-command-render.ts +127 -0
- package/src/skills/inline-command-runner.ts +242 -0
- package/src/skills/transitive-version-hash.ts +88 -0
- package/src/tasks/task-store.ts +43 -1
- package/src/telemetry/usage-telemetry-reporter.ts +1 -1
- package/src/tools/filesystem/edit.ts +6 -1
- package/src/tools/filesystem/read.ts +6 -1
- package/src/tools/filesystem/write.ts +6 -1
- package/src/tools/memory/handlers.ts +129 -1
- package/src/tools/permission-checker.ts +8 -1
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +5 -1
- package/src/tools/schedule/update.ts +6 -0
- package/src/tools/skills/load.ts +140 -6
- package/src/util/platform.ts +18 -0
- package/src/workspace/migrations/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3211
|
-
|
|
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
|
|
3393
|
-
|
|
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
|
+
});
|