@vellumai/assistant 0.5.4 → 0.5.6
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/Dockerfile +17 -27
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +42 -0
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +113 -0
- package/src/__tests__/config-schema.test.ts +2 -2
- package/src/__tests__/context-window-manager.test.ts +78 -0
- package/src/__tests__/conversation-title-service.test.ts +30 -1
- package/src/__tests__/credential-security-invariants.test.ts +2 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +207 -0
- package/src/__tests__/memory-regressions.test.ts +8 -30
- package/src/__tests__/openai-whisper.test.ts +93 -0
- package/src/__tests__/require-fresh-approval.test.ts +4 -0
- package/src/__tests__/slack-messaging-token-resolution.test.ts +319 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +4 -0
- package/src/__tests__/volume-security-guard.test.ts +155 -0
- package/src/cli/commands/conversations.ts +0 -18
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +16 -37
- package/src/config/env-registry.ts +9 -0
- package/src/config/env.ts +8 -2
- package/src/config/feature-flag-registry.json +8 -8
- package/src/config/schema.ts +0 -12
- package/src/config/schemas/memory.ts +0 -4
- package/src/config/schemas/platform.ts +1 -1
- package/src/config/schemas/security.ts +4 -0
- package/src/context/window-manager.ts +53 -2
- package/src/credential-execution/managed-catalog.ts +5 -15
- package/src/daemon/conversation-agent-loop.ts +0 -60
- package/src/daemon/conversation-memory.ts +0 -117
- package/src/daemon/conversation-runtime-assembly.ts +0 -2
- package/src/daemon/daemon-control.ts +7 -0
- package/src/daemon/handlers/conversations.ts +0 -11
- package/src/daemon/lifecycle.ts +10 -47
- package/src/daemon/providers-setup.ts +2 -1
- package/src/followups/followup-store.ts +5 -2
- package/src/hooks/manager.ts +7 -0
- package/src/instrument.ts +33 -1
- package/src/memory/conversation-crud.ts +0 -236
- package/src/memory/conversation-title-service.ts +26 -10
- package/src/memory/db-init.ts +5 -13
- package/src/memory/embedding-local.ts +11 -5
- package/src/memory/indexer.ts +15 -106
- package/src/memory/job-handlers/conversation-starters.ts +24 -36
- package/src/memory/job-handlers/embedding.ts +0 -79
- package/src/memory/job-utils.ts +1 -1
- package/src/memory/jobs-store.ts +0 -8
- package/src/memory/jobs-worker.ts +0 -20
- package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
- package/src/memory/migrations/index.ts +1 -3
- package/src/memory/qdrant-client.ts +4 -6
- package/src/memory/schema/conversations.ts +0 -3
- package/src/memory/schema/index.ts +0 -2
- package/src/messaging/draft-store.ts +2 -2
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/slack/adapter.ts +29 -2
- package/src/oauth/connection-resolver.test.ts +22 -18
- package/src/oauth/connection-resolver.ts +92 -7
- package/src/oauth/platform-connection.test.ts +78 -69
- package/src/oauth/platform-connection.ts +12 -19
- package/src/permissions/defaults.ts +3 -3
- package/src/permissions/trust-client.ts +332 -0
- package/src/permissions/trust-store-interface.ts +105 -0
- package/src/permissions/trust-store.ts +531 -39
- package/src/platform/client.test.ts +148 -0
- package/src/platform/client.ts +71 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +190 -0
- package/src/providers/speech-to-text/openai-whisper.ts +68 -0
- package/src/providers/speech-to-text/resolve.ts +9 -0
- package/src/providers/speech-to-text/types.ts +17 -0
- package/src/runtime/auth/route-policy.ts +14 -0
- package/src/runtime/auth/token-service.ts +133 -0
- package/src/runtime/http-server.ts +4 -2
- package/src/runtime/routes/conversation-management-routes.ts +0 -36
- package/src/runtime/routes/conversation-query-routes.ts +44 -2
- package/src/runtime/routes/conversation-routes.ts +2 -1
- package/src/runtime/routes/inbound-message-handler.ts +27 -3
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -1
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +287 -0
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +122 -0
- package/src/runtime/routes/log-export-routes.ts +1 -0
- package/src/runtime/routes/memory-item-routes.test.ts +221 -3
- package/src/runtime/routes/memory-item-routes.ts +124 -2
- package/src/runtime/routes/secret-routes.ts +4 -1
- package/src/runtime/routes/upgrade-broadcast-routes.ts +151 -0
- package/src/schedule/schedule-store.ts +0 -21
- package/src/security/ces-credential-client.ts +173 -0
- package/src/security/secure-keys.ts +65 -22
- package/src/signals/bash.ts +3 -0
- package/src/signals/cancel.ts +3 -0
- package/src/signals/confirm.ts +3 -0
- package/src/signals/conversation-undo.ts +3 -0
- package/src/signals/event-stream.ts +7 -0
- package/src/signals/shotgun.ts +3 -0
- package/src/signals/trust-rule.ts +3 -0
- package/src/skills/inline-command-render.ts +5 -1
- package/src/skills/inline-command-runner.ts +30 -2
- package/src/telemetry/usage-telemetry-reporter.test.ts +23 -36
- package/src/telemetry/usage-telemetry-reporter.ts +21 -19
- package/src/tools/memory/handlers.ts +1 -129
- package/src/tools/permission-checker.ts +18 -0
- package/src/tools/skills/load.ts +9 -2
- package/src/util/device-id.ts +70 -7
- package/src/util/logger.ts +35 -9
- package/src/util/platform.ts +29 -5
- package/src/util/xml.ts +8 -0
- package/src/workspace/heartbeat-service.ts +5 -24
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +113 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/archive-recall.test.ts +0 -560
- package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
- package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
- package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
- package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
- package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
- package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
- package/src/__tests__/memory-brief-time.test.ts +0 -285
- package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
- package/src/__tests__/memory-chunk-archive.test.ts +0 -400
- package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
- package/src/__tests__/memory-episode-archive.test.ts +0 -370
- package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
- package/src/__tests__/memory-observation-archive.test.ts +0 -375
- package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
- package/src/__tests__/memory-reducer-job.test.ts +0 -538
- package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
- package/src/__tests__/memory-reducer-store.test.ts +0 -728
- package/src/__tests__/memory-reducer-types.test.ts +0 -707
- package/src/__tests__/memory-reducer.test.ts +0 -704
- package/src/__tests__/memory-simplified-config.test.ts +0 -281
- package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
- package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
- package/src/config/schemas/memory-simplified.ts +0 -101
- package/src/memory/archive-recall.ts +0 -516
- package/src/memory/archive-store.ts +0 -400
- package/src/memory/brief-formatting.ts +0 -33
- package/src/memory/brief-open-loops.ts +0 -266
- package/src/memory/brief-time.ts +0 -162
- package/src/memory/brief.ts +0 -75
- package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
- package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
- package/src/memory/migrations/185-memory-brief-state.ts +0 -52
- package/src/memory/migrations/186-memory-archive.ts +0 -109
- package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
- package/src/memory/reducer-scheduler.ts +0 -242
- package/src/memory/reducer-store.ts +0 -271
- package/src/memory/reducer-types.ts +0 -106
- package/src/memory/reducer.ts +0 -467
- package/src/memory/schema/memory-archive.ts +0 -121
- package/src/memory/schema/memory-brief.ts +0 -55
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
afterAll,
|
|
6
|
-
beforeAll,
|
|
7
|
-
beforeEach,
|
|
8
|
-
describe,
|
|
9
|
-
expect,
|
|
10
|
-
mock,
|
|
11
|
-
test,
|
|
12
|
-
} from "bun:test";
|
|
13
|
-
|
|
14
|
-
import { eq } from "drizzle-orm";
|
|
15
|
-
|
|
16
|
-
const testDir = mkdtempSync(join(tmpdir(), "memory-chunk-archive-test-"));
|
|
17
|
-
|
|
18
|
-
mock.module("../util/platform.js", () => ({
|
|
19
|
-
getDataDir: () => testDir,
|
|
20
|
-
isMacOS: () => process.platform === "darwin",
|
|
21
|
-
isLinux: () => process.platform === "linux",
|
|
22
|
-
isWindows: () => process.platform === "win32",
|
|
23
|
-
getPidPath: () => join(testDir, "test.pid"),
|
|
24
|
-
getDbPath: () => join(testDir, "test.db"),
|
|
25
|
-
getLogPath: () => join(testDir, "test.log"),
|
|
26
|
-
ensureDataDir: () => {},
|
|
27
|
-
}));
|
|
28
|
-
|
|
29
|
-
mock.module("../util/logger.js", () => ({
|
|
30
|
-
getLogger: () =>
|
|
31
|
-
new Proxy({} as Record<string, unknown>, {
|
|
32
|
-
get: () => () => {},
|
|
33
|
-
}),
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
// Track calls to embedAndUpsert
|
|
37
|
-
const embedAndUpsertCalls: Array<{
|
|
38
|
-
config: unknown;
|
|
39
|
-
targetType: string;
|
|
40
|
-
targetId: string;
|
|
41
|
-
input: unknown;
|
|
42
|
-
extraPayload: unknown;
|
|
43
|
-
}> = [];
|
|
44
|
-
|
|
45
|
-
mock.module("../memory/job-utils.js", () => ({
|
|
46
|
-
asString: (value: unknown) =>
|
|
47
|
-
typeof value === "string" && value.length > 0 ? value : null,
|
|
48
|
-
embedAndUpsert: async (
|
|
49
|
-
config: unknown,
|
|
50
|
-
targetType: string,
|
|
51
|
-
targetId: string,
|
|
52
|
-
input: unknown,
|
|
53
|
-
extraPayload: unknown,
|
|
54
|
-
) => {
|
|
55
|
-
embedAndUpsertCalls.push({
|
|
56
|
-
config,
|
|
57
|
-
targetType,
|
|
58
|
-
targetId,
|
|
59
|
-
input,
|
|
60
|
-
extraPayload,
|
|
61
|
-
});
|
|
62
|
-
},
|
|
63
|
-
}));
|
|
64
|
-
|
|
65
|
-
import { DEFAULT_CONFIG } from "../config/defaults.js";
|
|
66
|
-
import type { AssistantConfig } from "../config/types.js";
|
|
67
|
-
import {
|
|
68
|
-
computeChunkContentHash,
|
|
69
|
-
estimateTokens,
|
|
70
|
-
getChunkById,
|
|
71
|
-
getChunksByObservationId,
|
|
72
|
-
upsertChunk,
|
|
73
|
-
upsertChunks,
|
|
74
|
-
} from "../memory/archive-store.js";
|
|
75
|
-
import { getDb, initializeDb, resetTestTables } from "../memory/db.js";
|
|
76
|
-
import { embedChunkJob } from "../memory/job-handlers/embedding.js";
|
|
77
|
-
import type { MemoryJob } from "../memory/jobs-store.js";
|
|
78
|
-
import {
|
|
79
|
-
conversations,
|
|
80
|
-
memoryJobs,
|
|
81
|
-
memoryObservations,
|
|
82
|
-
} from "../memory/schema.js";
|
|
83
|
-
|
|
84
|
-
const TEST_CONFIG: AssistantConfig = {
|
|
85
|
-
...DEFAULT_CONFIG,
|
|
86
|
-
memory: {
|
|
87
|
-
...DEFAULT_CONFIG.memory,
|
|
88
|
-
extraction: {
|
|
89
|
-
...DEFAULT_CONFIG.memory.extraction,
|
|
90
|
-
useLLM: false,
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
// ── Helpers ─────────────────────────────────────────────────────────
|
|
96
|
-
|
|
97
|
-
function makeJob(payload: Record<string, unknown>): MemoryJob {
|
|
98
|
-
return {
|
|
99
|
-
id: "job-1",
|
|
100
|
-
type: "embed_chunk",
|
|
101
|
-
payload,
|
|
102
|
-
status: "running",
|
|
103
|
-
attempts: 0,
|
|
104
|
-
deferrals: 0,
|
|
105
|
-
runAfter: 0,
|
|
106
|
-
lastError: null,
|
|
107
|
-
startedAt: Date.now(),
|
|
108
|
-
createdAt: Date.now(),
|
|
109
|
-
updatedAt: Date.now(),
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function seedConversation(id = "conv-1"): void {
|
|
114
|
-
const db = getDb();
|
|
115
|
-
const now = Date.now();
|
|
116
|
-
db.insert(conversations)
|
|
117
|
-
.values({
|
|
118
|
-
id,
|
|
119
|
-
title: "Test Conversation",
|
|
120
|
-
createdAt: now,
|
|
121
|
-
updatedAt: now,
|
|
122
|
-
})
|
|
123
|
-
.onConflictDoNothing()
|
|
124
|
-
.run();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function seedObservation(
|
|
128
|
-
id = "obs-1",
|
|
129
|
-
conversationId = "conv-1",
|
|
130
|
-
content = "The user prefers dark mode.",
|
|
131
|
-
): void {
|
|
132
|
-
const db = getDb();
|
|
133
|
-
const now = Date.now();
|
|
134
|
-
db.insert(memoryObservations)
|
|
135
|
-
.values({
|
|
136
|
-
id,
|
|
137
|
-
scopeId: "default",
|
|
138
|
-
conversationId,
|
|
139
|
-
role: "user",
|
|
140
|
-
content,
|
|
141
|
-
modality: "text",
|
|
142
|
-
createdAt: now,
|
|
143
|
-
})
|
|
144
|
-
.onConflictDoNothing()
|
|
145
|
-
.run();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ── Tests ───────────────────────────────────────────────────────────
|
|
149
|
-
|
|
150
|
-
describe("archive-store chunk helpers", () => {
|
|
151
|
-
beforeAll(() => {
|
|
152
|
-
initializeDb();
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
beforeEach(() => {
|
|
156
|
-
embedAndUpsertCalls.length = 0;
|
|
157
|
-
// Clear tables in FK-dependency order: chunks → observations → jobs, conversations
|
|
158
|
-
resetTestTables(
|
|
159
|
-
"memory_chunks",
|
|
160
|
-
"memory_observations",
|
|
161
|
-
"memory_jobs",
|
|
162
|
-
"conversations",
|
|
163
|
-
);
|
|
164
|
-
seedConversation();
|
|
165
|
-
seedObservation();
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
afterAll(() => {
|
|
169
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// ── computeChunkContentHash ─────────────────────────────────────
|
|
173
|
-
|
|
174
|
-
describe("computeChunkContentHash", () => {
|
|
175
|
-
test("produces deterministic hash for same inputs", () => {
|
|
176
|
-
const h1 = computeChunkContentHash("default", "hello world");
|
|
177
|
-
const h2 = computeChunkContentHash("default", "hello world");
|
|
178
|
-
expect(h1).toBe(h2);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("produces different hash for different scope", () => {
|
|
182
|
-
const h1 = computeChunkContentHash("default", "hello world");
|
|
183
|
-
const h2 = computeChunkContentHash("other-scope", "hello world");
|
|
184
|
-
expect(h1).not.toBe(h2);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test("produces different hash for different content", () => {
|
|
188
|
-
const h1 = computeChunkContentHash("default", "hello world");
|
|
189
|
-
const h2 = computeChunkContentHash("default", "goodbye world");
|
|
190
|
-
expect(h1).not.toBe(h2);
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// ── estimateTokens ─────────────────────────────────────────────
|
|
195
|
-
|
|
196
|
-
describe("estimateTokens", () => {
|
|
197
|
-
test("returns at least 1 for empty string", () => {
|
|
198
|
-
expect(estimateTokens("")).toBe(1);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
test("estimates roughly 4 chars per token", () => {
|
|
202
|
-
const text = "a".repeat(100);
|
|
203
|
-
expect(estimateTokens(text)).toBe(25);
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
// ── upsertChunk ────────────────────────────────────────────────
|
|
208
|
-
|
|
209
|
-
describe("upsertChunk", () => {
|
|
210
|
-
test("inserts a new chunk and enqueues embed_chunk job", () => {
|
|
211
|
-
const result = upsertChunk({
|
|
212
|
-
observationId: "obs-1",
|
|
213
|
-
content: "The user prefers dark mode.",
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
expect(result.inserted).toBe(true);
|
|
217
|
-
expect(result.chunkId).toBeTruthy();
|
|
218
|
-
|
|
219
|
-
// Verify chunk row exists
|
|
220
|
-
const chunk = getChunkById(result.chunkId);
|
|
221
|
-
expect(chunk).toBeDefined();
|
|
222
|
-
expect(chunk!.content).toBe("The user prefers dark mode.");
|
|
223
|
-
expect(chunk!.scopeId).toBe("default");
|
|
224
|
-
expect(chunk!.observationId).toBe("obs-1");
|
|
225
|
-
expect(chunk!.tokenEstimate).toBeGreaterThan(0);
|
|
226
|
-
|
|
227
|
-
// Verify embed_chunk job was enqueued
|
|
228
|
-
const db = getDb();
|
|
229
|
-
const jobs = db
|
|
230
|
-
.select()
|
|
231
|
-
.from(memoryJobs)
|
|
232
|
-
.where(eq(memoryJobs.type, "embed_chunk"))
|
|
233
|
-
.all();
|
|
234
|
-
expect(jobs).toHaveLength(1);
|
|
235
|
-
const payload = JSON.parse(jobs[0].payload);
|
|
236
|
-
expect(payload.chunkId).toBe(result.chunkId);
|
|
237
|
-
expect(payload.scopeId).toBe("default");
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
test("skips insert when content hash already exists (idempotence)", () => {
|
|
241
|
-
const first = upsertChunk({
|
|
242
|
-
observationId: "obs-1",
|
|
243
|
-
content: "The user prefers dark mode.",
|
|
244
|
-
});
|
|
245
|
-
expect(first.inserted).toBe(true);
|
|
246
|
-
|
|
247
|
-
const second = upsertChunk({
|
|
248
|
-
observationId: "obs-1",
|
|
249
|
-
content: "The user prefers dark mode.",
|
|
250
|
-
});
|
|
251
|
-
expect(second.inserted).toBe(false);
|
|
252
|
-
expect(second.chunkId).toBe(first.chunkId);
|
|
253
|
-
|
|
254
|
-
// Only one embed_chunk job should exist
|
|
255
|
-
const db = getDb();
|
|
256
|
-
const jobs = db
|
|
257
|
-
.select()
|
|
258
|
-
.from(memoryJobs)
|
|
259
|
-
.where(eq(memoryJobs.type, "embed_chunk"))
|
|
260
|
-
.all();
|
|
261
|
-
expect(jobs).toHaveLength(1);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test("inserts different chunks for different content", () => {
|
|
265
|
-
const r1 = upsertChunk({
|
|
266
|
-
observationId: "obs-1",
|
|
267
|
-
content: "Chunk A content",
|
|
268
|
-
});
|
|
269
|
-
const r2 = upsertChunk({
|
|
270
|
-
observationId: "obs-1",
|
|
271
|
-
content: "Chunk B content",
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
expect(r1.inserted).toBe(true);
|
|
275
|
-
expect(r2.inserted).toBe(true);
|
|
276
|
-
expect(r1.chunkId).not.toBe(r2.chunkId);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
test("respects custom scopeId", () => {
|
|
280
|
-
const result = upsertChunk({
|
|
281
|
-
scopeId: "scope-42",
|
|
282
|
-
observationId: "obs-1",
|
|
283
|
-
content: "Scoped content",
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
const chunk = getChunkById(result.chunkId);
|
|
287
|
-
expect(chunk!.scopeId).toBe("scope-42");
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
test("uses provided tokenEstimate when given", () => {
|
|
291
|
-
const result = upsertChunk({
|
|
292
|
-
observationId: "obs-1",
|
|
293
|
-
content: "Short text",
|
|
294
|
-
tokenEstimate: 99,
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
const chunk = getChunkById(result.chunkId);
|
|
298
|
-
expect(chunk!.tokenEstimate).toBe(99);
|
|
299
|
-
});
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// ── upsertChunks (batch) ──────────────────────────────────────
|
|
303
|
-
|
|
304
|
-
describe("upsertChunks", () => {
|
|
305
|
-
test("upserts multiple chunks and returns results in order", () => {
|
|
306
|
-
const results = upsertChunks([
|
|
307
|
-
{ observationId: "obs-1", content: "First chunk" },
|
|
308
|
-
{ observationId: "obs-1", content: "Second chunk" },
|
|
309
|
-
{ observationId: "obs-1", content: "Third chunk" },
|
|
310
|
-
]);
|
|
311
|
-
|
|
312
|
-
expect(results).toHaveLength(3);
|
|
313
|
-
expect(results.every((r) => r.inserted)).toBe(true);
|
|
314
|
-
|
|
315
|
-
// All chunk IDs are unique
|
|
316
|
-
const ids = new Set(results.map((r) => r.chunkId));
|
|
317
|
-
expect(ids.size).toBe(3);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
test("batch upsert with duplicate content is idempotent", () => {
|
|
321
|
-
const results = upsertChunks([
|
|
322
|
-
{ observationId: "obs-1", content: "Same content" },
|
|
323
|
-
{ observationId: "obs-1", content: "Same content" },
|
|
324
|
-
]);
|
|
325
|
-
|
|
326
|
-
expect(results[0].inserted).toBe(true);
|
|
327
|
-
expect(results[1].inserted).toBe(false);
|
|
328
|
-
expect(results[0].chunkId).toBe(results[1].chunkId);
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
// ── getChunksByObservationId ──────────────────────────────────
|
|
333
|
-
|
|
334
|
-
describe("getChunksByObservationId", () => {
|
|
335
|
-
test("returns all chunks for an observation", () => {
|
|
336
|
-
upsertChunk({ observationId: "obs-1", content: "Chunk A" });
|
|
337
|
-
upsertChunk({ observationId: "obs-1", content: "Chunk B" });
|
|
338
|
-
|
|
339
|
-
const chunks = getChunksByObservationId("obs-1");
|
|
340
|
-
expect(chunks).toHaveLength(2);
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
test("returns empty array for unknown observation", () => {
|
|
344
|
-
const chunks = getChunksByObservationId("obs-nonexistent");
|
|
345
|
-
expect(chunks).toHaveLength(0);
|
|
346
|
-
});
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
// ── embedChunkJob ─────────────────────────────────────────────
|
|
350
|
-
|
|
351
|
-
describe("embedChunkJob", () => {
|
|
352
|
-
test("skips when chunkId is missing from payload", async () => {
|
|
353
|
-
await embedChunkJob(makeJob({}), TEST_CONFIG);
|
|
354
|
-
expect(embedAndUpsertCalls).toHaveLength(0);
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
test("skips when chunk is not found", async () => {
|
|
358
|
-
await embedChunkJob(makeJob({ chunkId: "nonexistent" }), TEST_CONFIG);
|
|
359
|
-
expect(embedAndUpsertCalls).toHaveLength(0);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
test("embeds chunk with correct targetType and payload", async () => {
|
|
363
|
-
const result = upsertChunk({
|
|
364
|
-
observationId: "obs-1",
|
|
365
|
-
content: "The user prefers dark mode.",
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
await embedChunkJob(makeJob({ chunkId: result.chunkId }), TEST_CONFIG);
|
|
369
|
-
|
|
370
|
-
expect(embedAndUpsertCalls).toHaveLength(1);
|
|
371
|
-
const call = embedAndUpsertCalls[0];
|
|
372
|
-
expect(call.targetType).toBe("chunk");
|
|
373
|
-
expect(call.targetId).toBe(result.chunkId);
|
|
374
|
-
expect(call.input).toBe("The user prefers dark mode.");
|
|
375
|
-
expect(call.extraPayload).toMatchObject({
|
|
376
|
-
observation_id: "obs-1",
|
|
377
|
-
memory_scope_id: "default",
|
|
378
|
-
});
|
|
379
|
-
expect(
|
|
380
|
-
(call.extraPayload as Record<string, unknown>).created_at,
|
|
381
|
-
).toBeGreaterThan(0);
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
test("embeds chunk with correct scopeId in extra payload", async () => {
|
|
385
|
-
const result = upsertChunk({
|
|
386
|
-
scopeId: "custom-scope",
|
|
387
|
-
observationId: "obs-1",
|
|
388
|
-
content: "Scoped chunk content.",
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
await embedChunkJob(makeJob({ chunkId: result.chunkId }), TEST_CONFIG);
|
|
392
|
-
|
|
393
|
-
expect(embedAndUpsertCalls).toHaveLength(1);
|
|
394
|
-
const call = embedAndUpsertCalls[0];
|
|
395
|
-
expect(
|
|
396
|
-
(call.extraPayload as Record<string, unknown>).memory_scope_id,
|
|
397
|
-
).toBe("custom-scope");
|
|
398
|
-
});
|
|
399
|
-
});
|
|
400
|
-
});
|