@vellumai/assistant 0.4.49 → 0.4.50
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 +24 -33
- package/README.md +3 -3
- package/docs/architecture/memory.md +180 -119
- package/package.json +2 -2
- package/src/__tests__/agent-loop.test.ts +3 -1
- package/src/__tests__/anthropic-provider.test.ts +114 -23
- package/src/__tests__/approval-cascade.test.ts +1 -15
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
- package/src/__tests__/canonical-guardian-store.test.ts +95 -0
- package/src/__tests__/checker.test.ts +13 -0
- package/src/__tests__/config-schema.test.ts +1 -68
- package/src/__tests__/context-memory-e2e.test.ts +11 -100
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +4 -0
- package/src/__tests__/credential-vault.test.ts +13 -1
- package/src/__tests__/cu-unified-flow.test.ts +532 -0
- package/src/__tests__/date-context.test.ts +93 -77
- package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
- package/src/__tests__/history-repair.test.ts +245 -0
- package/src/__tests__/host-cu-proxy.test.ts +165 -3
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/invite-redemption-service.test.ts +65 -1
- package/src/__tests__/keychain-broker-client.test.ts +4 -4
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
- package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
- package/src/__tests__/memory-recall-quality.test.ts +244 -407
- package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
- package/src/__tests__/memory-regressions.test.ts +477 -2841
- package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
- package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
- package/src/__tests__/mime-builder.test.ts +28 -0
- package/src/__tests__/native-web-search.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +572 -5
- package/src/__tests__/oauth-store.test.ts +120 -6
- package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
- package/src/__tests__/registry.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +46 -1
- package/src/__tests__/schedule-tools.test.ts +32 -0
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -0
- package/src/__tests__/secure-keys.test.ts +7 -2
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/session-abort-tool-results.test.ts +1 -14
- package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
- package/src/__tests__/session-agent-loop.test.ts +19 -15
- package/src/__tests__/session-confirmation-signals.test.ts +1 -15
- package/src/__tests__/session-error.test.ts +124 -2
- package/src/__tests__/session-history-web-search.test.ts +918 -0
- package/src/__tests__/session-pre-run-repair.test.ts +1 -14
- package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
- package/src/__tests__/session-queue.test.ts +37 -27
- package/src/__tests__/session-runtime-assembly.test.ts +54 -0
- package/src/__tests__/session-slash-known.test.ts +1 -15
- package/src/__tests__/session-slash-queue.test.ts +1 -15
- package/src/__tests__/session-slash-unknown.test.ts +1 -15
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
- package/src/__tests__/session-workspace-injection.test.ts +3 -37
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
- package/src/__tests__/skills-install-extract.test.ts +93 -0
- package/src/__tests__/skillssh-registry.test.ts +451 -0
- package/src/__tests__/trust-store.test.ts +15 -0
- package/src/__tests__/voice-invite-redemption.test.ts +32 -1
- package/src/agent/ax-tree-compaction.test.ts +51 -0
- package/src/agent/loop.ts +39 -12
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-request-resolvers.ts +14 -2
- package/src/bundler/compiler-tools.ts +66 -2
- package/src/calls/call-domain.ts +132 -0
- package/src/calls/call-store.ts +6 -0
- package/src/calls/relay-server.ts +43 -5
- package/src/calls/relay-setup-router.ts +17 -1
- package/src/calls/twilio-config.ts +1 -1
- package/src/calls/types.ts +3 -1
- package/src/cli/commands/doctor.ts +4 -3
- package/src/cli/commands/mcp.ts +46 -59
- package/src/cli/commands/memory.ts +16 -165
- package/src/cli/commands/oauth/apps.ts +31 -2
- package/src/cli/commands/oauth/connections.ts +431 -97
- package/src/cli/commands/oauth/providers.ts +15 -1
- package/src/cli/commands/sessions.ts +5 -2
- package/src/cli/commands/skills.ts +173 -1
- package/src/cli/http-client.ts +0 -20
- package/src/cli/main-screen.tsx +2 -2
- package/src/cli/program.ts +5 -6
- package/src/cli.ts +4 -10
- package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
- package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
- package/src/config/bundled-tool-registry.ts +2 -5
- package/src/config/schema.ts +1 -12
- package/src/config/schemas/memory-lifecycle.ts +0 -9
- package/src/config/schemas/memory-processing.ts +0 -180
- package/src/config/schemas/memory-retrieval.ts +32 -104
- package/src/config/schemas/memory.ts +0 -10
- package/src/config/types.ts +0 -4
- package/src/context/window-manager.ts +4 -1
- package/src/daemon/config-watcher.ts +61 -3
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/date-context.ts +114 -31
- package/src/daemon/handlers/sessions.ts +18 -13
- package/src/daemon/handlers/skills.ts +20 -1
- package/src/daemon/history-repair.ts +72 -8
- package/src/daemon/host-cu-proxy.ts +55 -26
- package/src/daemon/lifecycle.ts +31 -3
- package/src/daemon/mcp-reload-service.ts +2 -2
- package/src/daemon/message-types/computer-use.ts +1 -12
- package/src/daemon/message-types/memory.ts +4 -16
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/sessions.ts +4 -0
- package/src/daemon/server.ts +12 -1
- package/src/daemon/session-agent-loop-handlers.ts +38 -0
- package/src/daemon/session-agent-loop.ts +334 -48
- package/src/daemon/session-error.ts +89 -6
- package/src/daemon/session-history.ts +17 -7
- package/src/daemon/session-media-retry.ts +6 -2
- package/src/daemon/session-memory.ts +69 -149
- package/src/daemon/session-process.ts +10 -1
- package/src/daemon/session-runtime-assembly.ts +49 -19
- package/src/daemon/session-surfaces.ts +4 -1
- package/src/daemon/session-tool-setup.ts +7 -1
- package/src/daemon/session.ts +12 -2
- package/src/instrument.ts +61 -1
- package/src/memory/admin.ts +2 -191
- package/src/memory/canonical-guardian-store.ts +38 -2
- package/src/memory/conversation-crud.ts +0 -33
- package/src/memory/conversation-queries.ts +22 -3
- package/src/memory/db-init.ts +28 -0
- package/src/memory/embedding-backend.ts +84 -8
- package/src/memory/embedding-types.ts +9 -1
- package/src/memory/indexer.ts +7 -46
- package/src/memory/items-extractor.ts +274 -76
- package/src/memory/job-handlers/backfill.ts +2 -127
- package/src/memory/job-handlers/cleanup.ts +2 -16
- package/src/memory/job-handlers/extraction.ts +2 -138
- package/src/memory/job-handlers/index-maintenance.ts +1 -6
- package/src/memory/job-handlers/summarization.ts +3 -148
- package/src/memory/job-utils.ts +21 -59
- package/src/memory/jobs-store.ts +1 -159
- package/src/memory/jobs-worker.ts +9 -52
- package/src/memory/migrations/104-core-indexes.ts +3 -3
- package/src/memory/migrations/149-oauth-tables.ts +2 -0
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
- package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
- package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
- package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
- package/src/memory/migrations/154-drop-fts.ts +20 -0
- package/src/memory/migrations/155-drop-conflicts.ts +7 -0
- package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
- package/src/memory/migrations/index.ts +7 -0
- package/src/memory/qdrant-client.ts +148 -51
- package/src/memory/raw-query.ts +1 -1
- package/src/memory/retriever.test.ts +294 -273
- package/src/memory/retriever.ts +421 -645
- package/src/memory/schema/calls.ts +2 -0
- package/src/memory/schema/memory-core.ts +3 -48
- package/src/memory/schema/oauth.ts +2 -0
- package/src/memory/search/formatting.ts +263 -176
- package/src/memory/search/lexical.ts +1 -254
- package/src/memory/search/ranking.ts +0 -455
- package/src/memory/search/semantic.ts +100 -14
- package/src/memory/search/staleness.ts +47 -0
- package/src/memory/search/tier-classifier.ts +21 -0
- package/src/memory/search/types.ts +15 -77
- package/src/memory/task-memory-cleanup.ts +4 -6
- package/src/messaging/providers/gmail/mime-builder.ts +17 -7
- package/src/oauth/byo-connection.test.ts +8 -1
- package/src/oauth/oauth-store.ts +113 -27
- package/src/oauth/seed-providers.ts +6 -0
- package/src/oauth/token-persistence.ts +11 -3
- package/src/permissions/defaults.ts +1 -0
- package/src/permissions/trust-store.ts +23 -1
- package/src/playbooks/playbook-compiler.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -2
- package/src/providers/anthropic/client.ts +56 -126
- package/src/providers/types.ts +7 -1
- package/src/runtime/AGENTS.md +9 -0
- package/src/runtime/auth/route-policy.ts +6 -3
- package/src/runtime/guardian-reply-router.ts +24 -22
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/invite-redemption-service.ts +19 -1
- package/src/runtime/invite-service.ts +25 -0
- package/src/runtime/pending-interactions.ts +2 -2
- package/src/runtime/routes/brain-graph-routes.ts +10 -90
- package/src/runtime/routes/conversation-routes.ts +9 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
- package/src/runtime/routes/memory-item-routes.test.ts +754 -0
- package/src/runtime/routes/memory-item-routes.ts +503 -0
- package/src/runtime/routes/session-management-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +2 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -0
- package/src/runtime/routes/workspace-routes.ts +2 -1
- package/src/security/keychain-broker-client.ts +17 -4
- package/src/security/secure-keys.ts +25 -3
- package/src/security/token-manager.ts +36 -36
- package/src/skills/catalog-install.ts +74 -18
- package/src/skills/skillssh-registry.ts +503 -0
- package/src/tools/assets/search.ts +5 -1
- package/src/tools/computer-use/definitions.ts +0 -10
- package/src/tools/computer-use/registry.ts +1 -1
- package/src/tools/credentials/vault.ts +1 -3
- package/src/tools/memory/definitions.ts +4 -13
- package/src/tools/memory/handlers.test.ts +83 -103
- package/src/tools/memory/handlers.ts +50 -85
- package/src/tools/schedule/create.ts +8 -1
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/skills/load.ts +25 -2
- package/src/__tests__/clarification-resolver.test.ts +0 -193
- package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
- package/src/__tests__/conflict-policy.test.ts +0 -269
- package/src/__tests__/conflict-store.test.ts +0 -372
- package/src/__tests__/contradiction-checker.test.ts +0 -361
- package/src/__tests__/entity-extractor.test.ts +0 -211
- package/src/__tests__/entity-search.test.ts +0 -1117
- package/src/__tests__/profile-compiler.test.ts +0 -392
- package/src/__tests__/session-conflict-gate.test.ts +0 -1228
- package/src/__tests__/session-profile-injection.test.ts +0 -557
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
- package/src/daemon/session-conflict-gate.ts +0 -167
- package/src/daemon/session-dynamic-profile.ts +0 -77
- package/src/memory/clarification-resolver.ts +0 -417
- package/src/memory/conflict-intent.ts +0 -205
- package/src/memory/conflict-policy.ts +0 -127
- package/src/memory/conflict-store.ts +0 -410
- package/src/memory/contradiction-checker.ts +0 -508
- package/src/memory/entity-extractor.ts +0 -535
- package/src/memory/format-recall.ts +0 -47
- package/src/memory/fts-reconciler.ts +0 -165
- package/src/memory/job-handlers/conflict.ts +0 -200
- package/src/memory/profile-compiler.ts +0 -195
- package/src/memory/recall-cache.ts +0 -117
- package/src/memory/search/entity.ts +0 -535
- package/src/memory/search/query-expansion.test.ts +0 -70
- package/src/memory/search/query-expansion.ts +0 -118
- package/src/runtime/routes/mcp-routes.ts +0 -20
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for memory item CRUD HTTP endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Covers: list with filters, get by ID, create + duplicate rejection,
|
|
5
|
+
* update + fingerprint collision, delete + 404.
|
|
6
|
+
*/
|
|
7
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import {
|
|
11
|
+
afterAll,
|
|
12
|
+
beforeAll,
|
|
13
|
+
beforeEach,
|
|
14
|
+
describe,
|
|
15
|
+
expect,
|
|
16
|
+
mock,
|
|
17
|
+
test,
|
|
18
|
+
} from "bun:test";
|
|
19
|
+
|
|
20
|
+
const testDir = mkdtempSync(join(tmpdir(), "memory-item-routes-test-"));
|
|
21
|
+
|
|
22
|
+
mock.module("../../util/platform.js", () => ({
|
|
23
|
+
getDataDir: () => testDir,
|
|
24
|
+
isMacOS: () => process.platform === "darwin",
|
|
25
|
+
isLinux: () => process.platform === "linux",
|
|
26
|
+
isWindows: () => process.platform === "win32",
|
|
27
|
+
getPidPath: () => join(testDir, "test.pid"),
|
|
28
|
+
getDbPath: () => join(testDir, "test.db"),
|
|
29
|
+
getLogPath: () => join(testDir, "test.log"),
|
|
30
|
+
ensureDataDir: () => {},
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
mock.module("../../util/logger.js", () => ({
|
|
34
|
+
getLogger: () =>
|
|
35
|
+
new Proxy({} as Record<string, unknown>, {
|
|
36
|
+
get: () => () => {},
|
|
37
|
+
}),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
// Stub config loader
|
|
41
|
+
mock.module("../../config/loader.js", () => ({
|
|
42
|
+
loadConfig: () => ({}),
|
|
43
|
+
getConfig: () => ({}),
|
|
44
|
+
invalidateConfigCache: () => {},
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
import { and, eq } from "drizzle-orm";
|
|
48
|
+
|
|
49
|
+
import { getDb, initializeDb, resetDb } from "../../memory/db.js";
|
|
50
|
+
import {
|
|
51
|
+
memoryEmbeddings,
|
|
52
|
+
memoryItems,
|
|
53
|
+
memoryJobs,
|
|
54
|
+
} from "../../memory/schema.js";
|
|
55
|
+
import type { RouteContext } from "../http-router.js";
|
|
56
|
+
import { memoryItemRouteDefinitions } from "./memory-item-routes.js";
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Helpers
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
function getHandler(endpoint: string, method: string) {
|
|
63
|
+
const routes = memoryItemRouteDefinitions();
|
|
64
|
+
const route = routes.find(
|
|
65
|
+
(r) => r.endpoint === endpoint && r.method === method,
|
|
66
|
+
);
|
|
67
|
+
if (!route) throw new Error(`No route: ${method} ${endpoint}`);
|
|
68
|
+
return route.handler;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function makeCtx(
|
|
72
|
+
searchParams: Record<string, string> = {},
|
|
73
|
+
params: Record<string, string> = {},
|
|
74
|
+
): RouteContext {
|
|
75
|
+
const url = new URL("http://localhost/v1/memory-items");
|
|
76
|
+
for (const [k, v] of Object.entries(searchParams)) {
|
|
77
|
+
url.searchParams.set(k, v);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
url,
|
|
81
|
+
req: new Request(url),
|
|
82
|
+
server: {} as ReturnType<typeof Bun.serve>,
|
|
83
|
+
authContext: {} as never,
|
|
84
|
+
params,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function makeJsonCtx(
|
|
89
|
+
endpoint: string,
|
|
90
|
+
method: string,
|
|
91
|
+
body: unknown,
|
|
92
|
+
params: Record<string, string> = {},
|
|
93
|
+
): RouteContext {
|
|
94
|
+
const url = new URL(`http://localhost/v1/${endpoint}`);
|
|
95
|
+
return {
|
|
96
|
+
url,
|
|
97
|
+
req: new Request(url, {
|
|
98
|
+
method,
|
|
99
|
+
headers: { "Content-Type": "application/json" },
|
|
100
|
+
body: JSON.stringify(body),
|
|
101
|
+
}),
|
|
102
|
+
server: {} as ReturnType<typeof Bun.serve>,
|
|
103
|
+
authContext: {} as never,
|
|
104
|
+
params,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function insertItem(opts: {
|
|
109
|
+
id: string;
|
|
110
|
+
kind: string;
|
|
111
|
+
subject: string;
|
|
112
|
+
statement: string;
|
|
113
|
+
status?: string;
|
|
114
|
+
importance?: number;
|
|
115
|
+
firstSeenAt?: number;
|
|
116
|
+
lastSeenAt?: number;
|
|
117
|
+
supersedes?: string;
|
|
118
|
+
supersededBy?: string;
|
|
119
|
+
}) {
|
|
120
|
+
const db = getDb();
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
db.insert(memoryItems)
|
|
123
|
+
.values({
|
|
124
|
+
id: opts.id,
|
|
125
|
+
kind: opts.kind,
|
|
126
|
+
subject: opts.subject,
|
|
127
|
+
statement: opts.statement,
|
|
128
|
+
status: opts.status ?? "active",
|
|
129
|
+
confidence: 0.95,
|
|
130
|
+
importance: opts.importance ?? 0.8,
|
|
131
|
+
fingerprint: `fp-${opts.id}`,
|
|
132
|
+
verificationState: "user_confirmed",
|
|
133
|
+
scopeId: "default",
|
|
134
|
+
firstSeenAt: opts.firstSeenAt ?? now,
|
|
135
|
+
lastSeenAt: opts.lastSeenAt ?? now,
|
|
136
|
+
lastUsedAt: null,
|
|
137
|
+
})
|
|
138
|
+
.run();
|
|
139
|
+
|
|
140
|
+
if (opts.supersedes || opts.supersededBy) {
|
|
141
|
+
const set: Record<string, unknown> = {};
|
|
142
|
+
if (opts.supersedes) set.supersedes = opts.supersedes;
|
|
143
|
+
if (opts.supersededBy) set.supersededBy = opts.supersededBy;
|
|
144
|
+
db.update(memoryItems).set(set).where(eq(memoryItems.id, opts.id)).run();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Suite
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
describe("Memory Item Routes", () => {
|
|
153
|
+
beforeAll(() => {
|
|
154
|
+
initializeDb();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
beforeEach(() => {
|
|
158
|
+
const db = getDb();
|
|
159
|
+
db.run("DELETE FROM memory_embeddings");
|
|
160
|
+
db.run("DELETE FROM memory_item_sources");
|
|
161
|
+
db.run("DELETE FROM memory_items");
|
|
162
|
+
db.run("DELETE FROM memory_jobs");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
afterAll(() => {
|
|
166
|
+
resetDb();
|
|
167
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// =========================================================================
|
|
171
|
+
// GET /v1/memory-items (list)
|
|
172
|
+
// =========================================================================
|
|
173
|
+
|
|
174
|
+
describe("GET /v1/memory-items", () => {
|
|
175
|
+
const handler = getHandler("memory-items", "GET");
|
|
176
|
+
|
|
177
|
+
test("returns empty list when no items", async () => {
|
|
178
|
+
const ctx = makeCtx();
|
|
179
|
+
const res = await handler(ctx);
|
|
180
|
+
expect(res.status).toBe(200);
|
|
181
|
+
const body = (await res.json()) as { items: unknown[]; total: number };
|
|
182
|
+
expect(body.items).toEqual([]);
|
|
183
|
+
expect(body.total).toBe(0);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("returns all active items by default", async () => {
|
|
187
|
+
insertItem({
|
|
188
|
+
id: "i1",
|
|
189
|
+
kind: "preference",
|
|
190
|
+
subject: "s1",
|
|
191
|
+
statement: "st1",
|
|
192
|
+
});
|
|
193
|
+
insertItem({
|
|
194
|
+
id: "i2",
|
|
195
|
+
kind: "identity",
|
|
196
|
+
subject: "s2",
|
|
197
|
+
statement: "st2",
|
|
198
|
+
status: "deleted",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const ctx = makeCtx();
|
|
202
|
+
const res = await handler(ctx);
|
|
203
|
+
expect(res.status).toBe(200);
|
|
204
|
+
const body = (await res.json()) as {
|
|
205
|
+
items: Array<{ id: string }>;
|
|
206
|
+
total: number;
|
|
207
|
+
};
|
|
208
|
+
expect(body.total).toBe(1);
|
|
209
|
+
expect(body.items.length).toBe(1);
|
|
210
|
+
expect(body.items[0].id).toBe("i1");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("returns items of all statuses when status=all", async () => {
|
|
214
|
+
insertItem({
|
|
215
|
+
id: "i1",
|
|
216
|
+
kind: "preference",
|
|
217
|
+
subject: "s1",
|
|
218
|
+
statement: "st1",
|
|
219
|
+
status: "active",
|
|
220
|
+
});
|
|
221
|
+
insertItem({
|
|
222
|
+
id: "i2",
|
|
223
|
+
kind: "identity",
|
|
224
|
+
subject: "s2",
|
|
225
|
+
statement: "st2",
|
|
226
|
+
status: "deleted",
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const ctx = makeCtx({ status: "all" });
|
|
230
|
+
const res = await handler(ctx);
|
|
231
|
+
expect(res.status).toBe(200);
|
|
232
|
+
const body = (await res.json()) as {
|
|
233
|
+
items: Array<{ id: string }>;
|
|
234
|
+
total: number;
|
|
235
|
+
};
|
|
236
|
+
expect(body.total).toBe(2);
|
|
237
|
+
expect(body.items.length).toBe(2);
|
|
238
|
+
const ids = body.items.map((i) => i.id).sort();
|
|
239
|
+
expect(ids).toEqual(["i1", "i2"]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("filters by kind", async () => {
|
|
243
|
+
insertItem({
|
|
244
|
+
id: "i1",
|
|
245
|
+
kind: "preference",
|
|
246
|
+
subject: "s1",
|
|
247
|
+
statement: "st1",
|
|
248
|
+
});
|
|
249
|
+
insertItem({
|
|
250
|
+
id: "i2",
|
|
251
|
+
kind: "identity",
|
|
252
|
+
subject: "s2",
|
|
253
|
+
statement: "st2",
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const ctx = makeCtx({ kind: "preference" });
|
|
257
|
+
const res = await handler(ctx);
|
|
258
|
+
const body = (await res.json()) as {
|
|
259
|
+
items: Array<{ id: string }>;
|
|
260
|
+
total: number;
|
|
261
|
+
};
|
|
262
|
+
expect(body.total).toBe(1);
|
|
263
|
+
expect(body.items[0].id).toBe("i1");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("filters by search on subject and statement", async () => {
|
|
267
|
+
insertItem({
|
|
268
|
+
id: "i1",
|
|
269
|
+
kind: "preference",
|
|
270
|
+
subject: "dark mode",
|
|
271
|
+
statement: "User prefers dark mode",
|
|
272
|
+
});
|
|
273
|
+
insertItem({
|
|
274
|
+
id: "i2",
|
|
275
|
+
kind: "identity",
|
|
276
|
+
subject: "name",
|
|
277
|
+
statement: "User name is Alice",
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const ctx = makeCtx({ search: "dark" });
|
|
281
|
+
const res = await handler(ctx);
|
|
282
|
+
const body = (await res.json()) as {
|
|
283
|
+
items: Array<{ id: string }>;
|
|
284
|
+
total: number;
|
|
285
|
+
};
|
|
286
|
+
expect(body.total).toBe(1);
|
|
287
|
+
expect(body.items[0].id).toBe("i1");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("supports pagination with limit and offset", async () => {
|
|
291
|
+
insertItem({
|
|
292
|
+
id: "i1",
|
|
293
|
+
kind: "preference",
|
|
294
|
+
subject: "s1",
|
|
295
|
+
statement: "st1",
|
|
296
|
+
lastSeenAt: 1000,
|
|
297
|
+
});
|
|
298
|
+
insertItem({
|
|
299
|
+
id: "i2",
|
|
300
|
+
kind: "preference",
|
|
301
|
+
subject: "s2",
|
|
302
|
+
statement: "st2",
|
|
303
|
+
lastSeenAt: 2000,
|
|
304
|
+
});
|
|
305
|
+
insertItem({
|
|
306
|
+
id: "i3",
|
|
307
|
+
kind: "preference",
|
|
308
|
+
subject: "s3",
|
|
309
|
+
statement: "st3",
|
|
310
|
+
lastSeenAt: 3000,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const ctx = makeCtx({ limit: "1", offset: "1" });
|
|
314
|
+
const res = await handler(ctx);
|
|
315
|
+
const body = (await res.json()) as {
|
|
316
|
+
items: Array<{ id: string }>;
|
|
317
|
+
total: number;
|
|
318
|
+
};
|
|
319
|
+
expect(body.total).toBe(3);
|
|
320
|
+
expect(body.items.length).toBe(1);
|
|
321
|
+
// Default sort is lastSeenAt desc, so offset 1 should be i2
|
|
322
|
+
expect(body.items[0].id).toBe("i2");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("supports sort by firstSeenAt ascending", async () => {
|
|
326
|
+
insertItem({
|
|
327
|
+
id: "i1",
|
|
328
|
+
kind: "preference",
|
|
329
|
+
subject: "s1",
|
|
330
|
+
statement: "st1",
|
|
331
|
+
firstSeenAt: 3000,
|
|
332
|
+
});
|
|
333
|
+
insertItem({
|
|
334
|
+
id: "i2",
|
|
335
|
+
kind: "preference",
|
|
336
|
+
subject: "s2",
|
|
337
|
+
statement: "st2",
|
|
338
|
+
firstSeenAt: 1000,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const ctx = makeCtx({ sort: "firstSeenAt", order: "asc" });
|
|
342
|
+
const res = await handler(ctx);
|
|
343
|
+
const body = (await res.json()) as {
|
|
344
|
+
items: Array<{ id: string }>;
|
|
345
|
+
};
|
|
346
|
+
expect(body.items[0].id).toBe("i2");
|
|
347
|
+
expect(body.items[1].id).toBe("i1");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("rejects invalid kind filter", async () => {
|
|
351
|
+
const ctx = makeCtx({ kind: "bogus" });
|
|
352
|
+
const res = await handler(ctx);
|
|
353
|
+
expect(res.status).toBe(400);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("rejects invalid sort field", async () => {
|
|
357
|
+
const ctx = makeCtx({ sort: "bogus" });
|
|
358
|
+
const res = await handler(ctx);
|
|
359
|
+
expect(res.status).toBe(400);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// =========================================================================
|
|
364
|
+
// GET /v1/memory-items/:id
|
|
365
|
+
// =========================================================================
|
|
366
|
+
|
|
367
|
+
describe("GET /v1/memory-items/:id", () => {
|
|
368
|
+
const handler = getHandler("memory-items/:id", "GET");
|
|
369
|
+
|
|
370
|
+
test("returns item by ID", async () => {
|
|
371
|
+
insertItem({
|
|
372
|
+
id: "i1",
|
|
373
|
+
kind: "preference",
|
|
374
|
+
subject: "dark mode",
|
|
375
|
+
statement: "Prefers dark mode",
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const ctx = makeCtx({}, { id: "i1" });
|
|
379
|
+
const res = await handler(ctx);
|
|
380
|
+
expect(res.status).toBe(200);
|
|
381
|
+
const body = (await res.json()) as {
|
|
382
|
+
item: { id: string; subject: string };
|
|
383
|
+
};
|
|
384
|
+
expect(body.item.id).toBe("i1");
|
|
385
|
+
expect(body.item.subject).toBe("dark mode");
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("returns 404 for non-existent item", async () => {
|
|
389
|
+
const ctx = makeCtx({}, { id: "nonexistent" });
|
|
390
|
+
const res = await handler(ctx);
|
|
391
|
+
expect(res.status).toBe(404);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("includes supersedesSubject when supersedes is set", async () => {
|
|
395
|
+
insertItem({
|
|
396
|
+
id: "old",
|
|
397
|
+
kind: "preference",
|
|
398
|
+
subject: "old pref",
|
|
399
|
+
statement: "old",
|
|
400
|
+
});
|
|
401
|
+
insertItem({
|
|
402
|
+
id: "new",
|
|
403
|
+
kind: "preference",
|
|
404
|
+
subject: "new pref",
|
|
405
|
+
statement: "new",
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// Set supersedes relationship manually
|
|
409
|
+
getDb()
|
|
410
|
+
.update(memoryItems)
|
|
411
|
+
.set({ supersedes: "old" })
|
|
412
|
+
.where(eq(memoryItems.id, "new"))
|
|
413
|
+
.run();
|
|
414
|
+
|
|
415
|
+
const ctx = makeCtx({}, { id: "new" });
|
|
416
|
+
const res = await handler(ctx);
|
|
417
|
+
const body = (await res.json()) as {
|
|
418
|
+
item: { supersedesSubject?: string };
|
|
419
|
+
};
|
|
420
|
+
expect(body.item.supersedesSubject).toBe("old pref");
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// =========================================================================
|
|
425
|
+
// POST /v1/memory-items
|
|
426
|
+
// =========================================================================
|
|
427
|
+
|
|
428
|
+
describe("POST /v1/memory-items", () => {
|
|
429
|
+
const handler = getHandler("memory-items", "POST");
|
|
430
|
+
|
|
431
|
+
test("creates a new memory item", async () => {
|
|
432
|
+
const ctx = makeJsonCtx("memory-items", "POST", {
|
|
433
|
+
kind: "preference",
|
|
434
|
+
subject: "dark mode",
|
|
435
|
+
statement: "User prefers dark mode",
|
|
436
|
+
});
|
|
437
|
+
const res = await handler(ctx);
|
|
438
|
+
expect(res.status).toBe(201);
|
|
439
|
+
const body = (await res.json()) as {
|
|
440
|
+
item: { id: string; kind: string; subject: string; statement: string };
|
|
441
|
+
};
|
|
442
|
+
expect(body.item.kind).toBe("preference");
|
|
443
|
+
expect(body.item.subject).toBe("dark mode");
|
|
444
|
+
expect(body.item.statement).toBe("User prefers dark mode");
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("uses custom importance when provided", async () => {
|
|
448
|
+
const ctx = makeJsonCtx("memory-items", "POST", {
|
|
449
|
+
kind: "preference",
|
|
450
|
+
subject: "importance test",
|
|
451
|
+
statement: "Testing custom importance",
|
|
452
|
+
importance: 0.5,
|
|
453
|
+
});
|
|
454
|
+
const res = await handler(ctx);
|
|
455
|
+
expect(res.status).toBe(201);
|
|
456
|
+
const body = (await res.json()) as {
|
|
457
|
+
item: { importance: number };
|
|
458
|
+
};
|
|
459
|
+
expect(body.item.importance).toBe(0.5);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("rejects duplicate fingerprint", async () => {
|
|
463
|
+
const payload = {
|
|
464
|
+
kind: "preference",
|
|
465
|
+
subject: "dark mode",
|
|
466
|
+
statement: "User prefers dark mode",
|
|
467
|
+
};
|
|
468
|
+
const ctx1 = makeJsonCtx("memory-items", "POST", payload);
|
|
469
|
+
const res1 = await handler(ctx1);
|
|
470
|
+
expect(res1.status).toBe(201);
|
|
471
|
+
|
|
472
|
+
const ctx2 = makeJsonCtx("memory-items", "POST", payload);
|
|
473
|
+
const res2 = await handler(ctx2);
|
|
474
|
+
expect(res2.status).toBe(409);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test("rejects invalid kind", async () => {
|
|
478
|
+
const ctx = makeJsonCtx("memory-items", "POST", {
|
|
479
|
+
kind: "bogus",
|
|
480
|
+
subject: "test",
|
|
481
|
+
statement: "test",
|
|
482
|
+
});
|
|
483
|
+
const res = await handler(ctx);
|
|
484
|
+
expect(res.status).toBe(400);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("rejects missing subject", async () => {
|
|
488
|
+
const ctx = makeJsonCtx("memory-items", "POST", {
|
|
489
|
+
kind: "preference",
|
|
490
|
+
statement: "test",
|
|
491
|
+
});
|
|
492
|
+
const res = await handler(ctx);
|
|
493
|
+
expect(res.status).toBe(400);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("rejects missing statement", async () => {
|
|
497
|
+
const ctx = makeJsonCtx("memory-items", "POST", {
|
|
498
|
+
kind: "preference",
|
|
499
|
+
subject: "test",
|
|
500
|
+
});
|
|
501
|
+
const res = await handler(ctx);
|
|
502
|
+
expect(res.status).toBe(400);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("truncates long subject and statement", async () => {
|
|
506
|
+
const longSubject = "a".repeat(200);
|
|
507
|
+
const longStatement = "b".repeat(1000);
|
|
508
|
+
const ctx = makeJsonCtx("memory-items", "POST", {
|
|
509
|
+
kind: "preference",
|
|
510
|
+
subject: longSubject,
|
|
511
|
+
statement: longStatement,
|
|
512
|
+
});
|
|
513
|
+
const res = await handler(ctx);
|
|
514
|
+
expect(res.status).toBe(201);
|
|
515
|
+
const body = (await res.json()) as {
|
|
516
|
+
item: { subject: string; statement: string };
|
|
517
|
+
};
|
|
518
|
+
expect(body.item.subject.length).toBeLessThanOrEqual(80);
|
|
519
|
+
expect(body.item.statement.length).toBeLessThanOrEqual(500);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("enqueues embed job on create", async () => {
|
|
523
|
+
const ctx = makeJsonCtx("memory-items", "POST", {
|
|
524
|
+
kind: "preference",
|
|
525
|
+
subject: "embed test",
|
|
526
|
+
statement: "Should enqueue embed job",
|
|
527
|
+
});
|
|
528
|
+
await handler(ctx);
|
|
529
|
+
|
|
530
|
+
// Verify a memory job was enqueued
|
|
531
|
+
const db = getDb();
|
|
532
|
+
const jobs = db.select().from(memoryJobs).all();
|
|
533
|
+
const embedJobs = jobs.filter(
|
|
534
|
+
(j) => j.type === "embed_item" && j.status === "pending",
|
|
535
|
+
);
|
|
536
|
+
expect(embedJobs.length).toBeGreaterThanOrEqual(1);
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// =========================================================================
|
|
541
|
+
// PATCH /v1/memory-items/:id
|
|
542
|
+
// =========================================================================
|
|
543
|
+
|
|
544
|
+
describe("PATCH /v1/memory-items/:id", () => {
|
|
545
|
+
const handler = getHandler("memory-items/:id", "PATCH");
|
|
546
|
+
|
|
547
|
+
test("updates subject and statement", async () => {
|
|
548
|
+
insertItem({
|
|
549
|
+
id: "i1",
|
|
550
|
+
kind: "preference",
|
|
551
|
+
subject: "old subject",
|
|
552
|
+
statement: "old statement",
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
const ctx = makeJsonCtx(
|
|
556
|
+
"memory-items/i1",
|
|
557
|
+
"PATCH",
|
|
558
|
+
{ subject: "new subject", statement: "new statement" },
|
|
559
|
+
{ id: "i1" },
|
|
560
|
+
);
|
|
561
|
+
const res = await handler(ctx);
|
|
562
|
+
expect(res.status).toBe(200);
|
|
563
|
+
const body = (await res.json()) as {
|
|
564
|
+
item: { subject: string; statement: string };
|
|
565
|
+
};
|
|
566
|
+
expect(body.item.subject).toBe("new subject");
|
|
567
|
+
expect(body.item.statement).toBe("new statement");
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test("returns 404 for non-existent item", async () => {
|
|
571
|
+
const ctx = makeJsonCtx(
|
|
572
|
+
"memory-items/nonexistent",
|
|
573
|
+
"PATCH",
|
|
574
|
+
{ subject: "test" },
|
|
575
|
+
{ id: "nonexistent" },
|
|
576
|
+
);
|
|
577
|
+
const res = await handler(ctx);
|
|
578
|
+
expect(res.status).toBe(404);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test("detects fingerprint collision on update", async () => {
|
|
582
|
+
insertItem({
|
|
583
|
+
id: "i1",
|
|
584
|
+
kind: "preference",
|
|
585
|
+
subject: "first",
|
|
586
|
+
statement: "first statement",
|
|
587
|
+
});
|
|
588
|
+
// Insert a second item using the create handler to get a real fingerprint
|
|
589
|
+
const createHandler = getHandler("memory-items", "POST");
|
|
590
|
+
const createCtx = makeJsonCtx("memory-items", "POST", {
|
|
591
|
+
kind: "preference",
|
|
592
|
+
subject: "second",
|
|
593
|
+
statement: "second statement",
|
|
594
|
+
});
|
|
595
|
+
await createHandler(createCtx);
|
|
596
|
+
|
|
597
|
+
// Now try to update i1 to match the second item's content
|
|
598
|
+
// This should produce the same fingerprint as the second item
|
|
599
|
+
const ctx = makeJsonCtx(
|
|
600
|
+
"memory-items/i1",
|
|
601
|
+
"PATCH",
|
|
602
|
+
{ subject: "second", statement: "second statement" },
|
|
603
|
+
{ id: "i1" },
|
|
604
|
+
);
|
|
605
|
+
const res = await handler(ctx);
|
|
606
|
+
expect(res.status).toBe(409);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test("allows updating kind", async () => {
|
|
610
|
+
insertItem({
|
|
611
|
+
id: "i1",
|
|
612
|
+
kind: "preference",
|
|
613
|
+
subject: "test",
|
|
614
|
+
statement: "test",
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const ctx = makeJsonCtx(
|
|
618
|
+
"memory-items/i1",
|
|
619
|
+
"PATCH",
|
|
620
|
+
{ kind: "identity" },
|
|
621
|
+
{ id: "i1" },
|
|
622
|
+
);
|
|
623
|
+
const res = await handler(ctx);
|
|
624
|
+
expect(res.status).toBe(200);
|
|
625
|
+
const body = (await res.json()) as { item: { kind: string } };
|
|
626
|
+
expect(body.item.kind).toBe("identity");
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
test("rejects invalid kind on update", async () => {
|
|
630
|
+
insertItem({
|
|
631
|
+
id: "i1",
|
|
632
|
+
kind: "preference",
|
|
633
|
+
subject: "test",
|
|
634
|
+
statement: "test",
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
const ctx = makeJsonCtx(
|
|
638
|
+
"memory-items/i1",
|
|
639
|
+
"PATCH",
|
|
640
|
+
{ kind: "bogus" },
|
|
641
|
+
{ id: "i1" },
|
|
642
|
+
);
|
|
643
|
+
const res = await handler(ctx);
|
|
644
|
+
expect(res.status).toBe(400);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
test("enqueues embed job when statement changes", async () => {
|
|
648
|
+
insertItem({
|
|
649
|
+
id: "i1",
|
|
650
|
+
kind: "preference",
|
|
651
|
+
subject: "test",
|
|
652
|
+
statement: "old statement",
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// Clear jobs first
|
|
656
|
+
getDb().run("DELETE FROM memory_jobs");
|
|
657
|
+
|
|
658
|
+
const ctx = makeJsonCtx(
|
|
659
|
+
"memory-items/i1",
|
|
660
|
+
"PATCH",
|
|
661
|
+
{ statement: "new statement" },
|
|
662
|
+
{ id: "i1" },
|
|
663
|
+
);
|
|
664
|
+
await handler(ctx);
|
|
665
|
+
|
|
666
|
+
const db = getDb();
|
|
667
|
+
const jobs = db.select().from(memoryJobs).all();
|
|
668
|
+
const embedJobs = jobs.filter(
|
|
669
|
+
(j) => j.type === "embed_item" && j.status === "pending",
|
|
670
|
+
);
|
|
671
|
+
expect(embedJobs.length).toBe(1);
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// =========================================================================
|
|
676
|
+
// DELETE /v1/memory-items/:id
|
|
677
|
+
// =========================================================================
|
|
678
|
+
|
|
679
|
+
describe("DELETE /v1/memory-items/:id", () => {
|
|
680
|
+
const handler = getHandler("memory-items/:id", "DELETE");
|
|
681
|
+
|
|
682
|
+
test("deletes item and returns 204", async () => {
|
|
683
|
+
insertItem({
|
|
684
|
+
id: "i1",
|
|
685
|
+
kind: "preference",
|
|
686
|
+
subject: "test",
|
|
687
|
+
statement: "test",
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
const ctx = makeJsonCtx("memory-items/i1", "DELETE", null, { id: "i1" });
|
|
691
|
+
const res = await handler(ctx);
|
|
692
|
+
expect(res.status).toBe(204);
|
|
693
|
+
|
|
694
|
+
// Verify the item is gone
|
|
695
|
+
const db = getDb();
|
|
696
|
+
const item = db
|
|
697
|
+
.select()
|
|
698
|
+
.from(memoryItems)
|
|
699
|
+
.where(eq(memoryItems.id, "i1"))
|
|
700
|
+
.get();
|
|
701
|
+
expect(item).toBeUndefined();
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test("returns 404 for non-existent item", async () => {
|
|
705
|
+
const ctx = makeJsonCtx("memory-items/nonexistent", "DELETE", null, {
|
|
706
|
+
id: "nonexistent",
|
|
707
|
+
});
|
|
708
|
+
const res = await handler(ctx);
|
|
709
|
+
expect(res.status).toBe(404);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
test("also deletes associated embeddings", async () => {
|
|
713
|
+
insertItem({
|
|
714
|
+
id: "i1",
|
|
715
|
+
kind: "preference",
|
|
716
|
+
subject: "test",
|
|
717
|
+
statement: "test",
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// Insert an embedding for this item
|
|
721
|
+
const db = getDb();
|
|
722
|
+
db.insert(memoryEmbeddings)
|
|
723
|
+
.values({
|
|
724
|
+
id: "emb-1",
|
|
725
|
+
targetType: "item",
|
|
726
|
+
targetId: "i1",
|
|
727
|
+
provider: "test",
|
|
728
|
+
model: "test-model",
|
|
729
|
+
dimensions: 384,
|
|
730
|
+
vectorJson: "[]",
|
|
731
|
+
createdAt: Date.now(),
|
|
732
|
+
updatedAt: Date.now(),
|
|
733
|
+
})
|
|
734
|
+
.run();
|
|
735
|
+
|
|
736
|
+
const ctx = makeJsonCtx("memory-items/i1", "DELETE", null, { id: "i1" });
|
|
737
|
+
const res = await handler(ctx);
|
|
738
|
+
expect(res.status).toBe(204);
|
|
739
|
+
|
|
740
|
+
// Verify embedding is also gone
|
|
741
|
+
const emb = db
|
|
742
|
+
.select()
|
|
743
|
+
.from(memoryEmbeddings)
|
|
744
|
+
.where(
|
|
745
|
+
and(
|
|
746
|
+
eq(memoryEmbeddings.targetType, "item"),
|
|
747
|
+
eq(memoryEmbeddings.targetId, "i1"),
|
|
748
|
+
),
|
|
749
|
+
)
|
|
750
|
+
.get();
|
|
751
|
+
expect(emb).toBeUndefined();
|
|
752
|
+
});
|
|
753
|
+
});
|
|
754
|
+
});
|