comisai 1.0.11 → 1.0.12
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/node_modules/@comis/agent/dist/executor/pi-executor.d.ts +1 -4
- package/node_modules/@comis/agent/package.json +1 -1
- package/node_modules/@comis/channels/package.json +1 -1
- package/node_modules/@comis/cli/package.json +1 -1
- package/node_modules/@comis/core/package.json +1 -1
- package/node_modules/@comis/daemon/package.json +1 -1
- package/node_modules/@comis/gateway/package.json +1 -1
- package/node_modules/@comis/infra/package.json +1 -1
- package/node_modules/@comis/memory/package.json +1 -1
- package/node_modules/@comis/scheduler/package.json +1 -1
- package/node_modules/@comis/shared/package.json +1 -1
- package/node_modules/@comis/skills/package.json +1 -1
- package/package.json +18 -18
- package/node_modules/@comis/core/dist/approval/approval-gate.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/approval/approval-gate.test.js +0 -1003
- package/node_modules/@comis/core/dist/bootstrap.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/bootstrap.test.js +0 -150
- package/node_modules/@comis/core/dist/config/backup.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/backup.test.js +0 -160
- package/node_modules/@comis/core/dist/config/env-substitution.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/env-substitution.test.js +0 -259
- package/node_modules/@comis/core/dist/config/field-metadata.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/field-metadata.test.js +0 -72
- package/node_modules/@comis/core/dist/config/git-manager.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/git-manager.test.js +0 -1042
- package/node_modules/@comis/core/dist/config/immutable-keys.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/immutable-keys.test.js +0 -283
- package/node_modules/@comis/core/dist/config/include-resolver.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/include-resolver.test.js +0 -292
- package/node_modules/@comis/core/dist/config/layered.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/layered.test.js +0 -211
- package/node_modules/@comis/core/dist/config/loader.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/loader.test.js +0 -300
- package/node_modules/@comis/core/dist/config/migrate.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/migrate.test.js +0 -244
- package/node_modules/@comis/core/dist/config/partial-validator.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/partial-validator.test.js +0 -98
- package/node_modules/@comis/core/dist/config/schema-agent-model.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-agent-model.test.js +0 -279
- package/node_modules/@comis/core/dist/config/schema-agent-session.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-agent-session.test.js +0 -242
- package/node_modules/@comis/core/dist/config/schema-agent.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-agent.test.js +0 -645
- package/node_modules/@comis/core/dist/config/schema-channel.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-channel.test.js +0 -341
- package/node_modules/@comis/core/dist/config/schema-coalescer.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-coalescer.test.js +0 -51
- package/node_modules/@comis/core/dist/config/schema-context-engine.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-context-engine.test.js +0 -797
- package/node_modules/@comis/core/dist/config/schema-context-guard.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-context-guard.test.js +0 -111
- package/node_modules/@comis/core/dist/config/schema-daemon.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-daemon.test.js +0 -107
- package/node_modules/@comis/core/dist/config/schema-delivery-timing.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-delivery-timing.test.js +0 -51
- package/node_modules/@comis/core/dist/config/schema-documentation.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-documentation.test.js +0 -63
- package/node_modules/@comis/core/dist/config/schema-gateway.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-gateway.test.js +0 -219
- package/node_modules/@comis/core/dist/config/schema-gemini-cache.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-gemini-cache.test.js +0 -41
- package/node_modules/@comis/core/dist/config/schema-integrations.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-integrations.test.js +0 -92
- package/node_modules/@comis/core/dist/config/schema-lifecycle-reactions.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-lifecycle-reactions.test.js +0 -61
- package/node_modules/@comis/core/dist/config/schema-misc.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-misc.test.js +0 -394
- package/node_modules/@comis/core/dist/config/schema-observability.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-observability.test.js +0 -76
- package/node_modules/@comis/core/dist/config/schema-providers.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-providers.test.js +0 -294
- package/node_modules/@comis/core/dist/config/schema-queue.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-queue.test.js +0 -234
- package/node_modules/@comis/core/dist/config/schema-response-prefix.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-response-prefix.test.js +0 -36
- package/node_modules/@comis/core/dist/config/schema-security.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-security.test.js +0 -54
- package/node_modules/@comis/core/dist/config/schema-sender-trust-display.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-sender-trust-display.test.js +0 -60
- package/node_modules/@comis/core/dist/config/schema-serializer.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-serializer.test.js +0 -73
- package/node_modules/@comis/core/dist/config/schema-telegram-file-guard.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-telegram-file-guard.test.js +0 -39
- package/node_modules/@comis/core/dist/context/context.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/context/context.test.js +0 -276
- package/node_modules/@comis/core/dist/domain/agent-response.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/domain/agent-response.test.js +0 -159
- package/node_modules/@comis/core/dist/domain/credential-mapping.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/domain/credential-mapping.test.js +0 -135
- package/node_modules/@comis/core/dist/domain/delivery-origin.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/domain/delivery-origin.test.js +0 -68
- package/node_modules/@comis/core/dist/domain/execution-graph.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/domain/execution-graph.test.js +0 -441
- package/node_modules/@comis/core/dist/domain/memory-entry.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/domain/memory-entry.test.js +0 -193
- package/node_modules/@comis/core/dist/domain/normalized-message.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/domain/normalized-message.test.js +0 -255
- package/node_modules/@comis/core/dist/domain/session-key.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/domain/session-key.test.js +0 -291
- package/node_modules/@comis/core/dist/domain/subagent-context-config.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/domain/subagent-context-config.test.js +0 -131
- package/node_modules/@comis/core/dist/domain/subagent-context-types.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/domain/subagent-context-types.test.js +0 -182
- package/node_modules/@comis/core/dist/event-bus/bus.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/event-bus/bus.test.js +0 -152
- package/node_modules/@comis/core/dist/event-bus/events-agent.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/event-bus/events-agent.test.js +0 -409
- package/node_modules/@comis/core/dist/event-bus/events-channel.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/event-bus/events-channel.test.js +0 -240
- package/node_modules/@comis/core/dist/event-bus/events-infra.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/event-bus/events-infra.test.js +0 -416
- package/node_modules/@comis/core/dist/event-bus/events-messaging.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/event-bus/events-messaging.test.js +0 -433
- package/node_modules/@comis/core/dist/event-bus/events.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/event-bus/events.test.js +0 -93
- package/node_modules/@comis/core/dist/hooks/hook-runner-global.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/hooks/hook-runner-global.test.js +0 -29
- package/node_modules/@comis/core/dist/hooks/hook-runner.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/hooks/hook-runner.test.js +0 -490
- package/node_modules/@comis/core/dist/hooks/hook-strategies.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/hooks/hook-strategies.test.js +0 -209
- package/node_modules/@comis/core/dist/hooks/integration.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/hooks/integration.test.js +0 -235
- package/node_modules/@comis/core/dist/hooks/plugin-registry.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/hooks/plugin-registry.test.js +0 -470
- package/node_modules/@comis/core/dist/load-env.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/load-env.test.js +0 -21
- package/node_modules/@comis/core/dist/security/action-classifier.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/action-classifier.test.js +0 -298
- package/node_modules/@comis/core/dist/security/audit-aggregator.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/audit-aggregator.test.js +0 -128
- package/node_modules/@comis/core/dist/security/audit.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/audit.test.js +0 -154
- package/node_modules/@comis/core/dist/security/canary-token.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/canary-token.test.js +0 -45
- package/node_modules/@comis/core/dist/security/config-redaction.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/config-redaction.test.js +0 -107
- package/node_modules/@comis/core/dist/security/external-content.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/external-content.test.js +0 -210
- package/node_modules/@comis/core/dist/security/injection-patterns.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/injection-patterns.test.js +0 -213
- package/node_modules/@comis/core/dist/security/injection-rate-limiter.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/injection-rate-limiter.test.js +0 -194
- package/node_modules/@comis/core/dist/security/input-guard.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/input-guard.test.js +0 -215
- package/node_modules/@comis/core/dist/security/input-security-guard.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/input-security-guard.test.js +0 -215
- package/node_modules/@comis/core/dist/security/input-validator.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/input-validator.test.js +0 -133
- package/node_modules/@comis/core/dist/security/log-sanitizer.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/log-sanitizer.test.js +0 -260
- package/node_modules/@comis/core/dist/security/memory-write-validator.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/memory-write-validator.test.js +0 -62
- package/node_modules/@comis/core/dist/security/output-guard.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/output-guard.test.js +0 -397
- package/node_modules/@comis/core/dist/security/safe-path.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/safe-path.test.js +0 -95
- package/node_modules/@comis/core/dist/security/scoped-secret-manager.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/scoped-secret-manager.test.js +0 -231
- package/node_modules/@comis/core/dist/security/secret-access.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/secret-access.test.js +0 -41
- package/node_modules/@comis/core/dist/security/secret-crypto.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/secret-crypto.test.js +0 -113
- package/node_modules/@comis/core/dist/security/secret-manager.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/secret-manager.test.js +0 -394
- package/node_modules/@comis/core/dist/security/secret-ref-resolver.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/secret-ref-resolver.test.js +0 -268
- package/node_modules/@comis/core/dist/security/secrets-audit.test.d.ts +0 -10
- package/node_modules/@comis/core/dist/security/secrets-audit.test.js +0 -295
- package/node_modules/@comis/core/dist/security/ssrf-guard.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/ssrf-guard.test.js +0 -127
- package/node_modules/@comis/core/dist/security/token-generator.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/token-generator.test.js +0 -35
- package/node_modules/@comis/core/dist/tool-metadata.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/tool-metadata.test.js +0 -112
- package/node_modules/@comis/memory/dist/compaction.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/compaction.test.js +0 -298
- package/node_modules/@comis/memory/dist/context-schema.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/context-schema.test.js +0 -246
- package/node_modules/@comis/memory/dist/context-store.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/context-store.test.js +0 -708
- package/node_modules/@comis/memory/dist/credential-mapping-schema.test.d.ts +0 -7
- package/node_modules/@comis/memory/dist/credential-mapping-schema.test.js +0 -73
- package/node_modules/@comis/memory/dist/credential-mapping-store.test.d.ts +0 -8
- package/node_modules/@comis/memory/dist/credential-mapping-store.test.js +0 -340
- package/node_modules/@comis/memory/dist/delivery-mirror-adapter.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/delivery-mirror-adapter.test.js +0 -165
- package/node_modules/@comis/memory/dist/delivery-queue-adapter.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/delivery-queue-adapter.test.js +0 -263
- package/node_modules/@comis/memory/dist/embedding-batch-indexer.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/embedding-batch-indexer.test.js +0 -202
- package/node_modules/@comis/memory/dist/embedding-cache-lru.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/embedding-cache-lru.test.js +0 -241
- package/node_modules/@comis/memory/dist/embedding-cache-sqlite.test.d.ts +0 -8
- package/node_modules/@comis/memory/dist/embedding-cache-sqlite.test.js +0 -678
- package/node_modules/@comis/memory/dist/embedding-cache.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/embedding-cache.test.js +0 -213
- package/node_modules/@comis/memory/dist/embedding-fingerprint.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/embedding-fingerprint.test.js +0 -87
- package/node_modules/@comis/memory/dist/embedding-hash.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/embedding-hash.test.js +0 -31
- package/node_modules/@comis/memory/dist/embedding-integration.test.d.ts +0 -8
- package/node_modules/@comis/memory/dist/embedding-integration.test.js +0 -232
- package/node_modules/@comis/memory/dist/embedding-provider-factory.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/embedding-provider-factory.test.js +0 -176
- package/node_modules/@comis/memory/dist/embedding-provider-local.test.d.ts +0 -8
- package/node_modules/@comis/memory/dist/embedding-provider-local.test.js +0 -76
- package/node_modules/@comis/memory/dist/embedding-provider-openai.test.d.ts +0 -8
- package/node_modules/@comis/memory/dist/embedding-provider-openai.test.js +0 -100
- package/node_modules/@comis/memory/dist/embedding-queue.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/embedding-queue.test.js +0 -236
- package/node_modules/@comis/memory/dist/hybrid-search.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/hybrid-search.test.js +0 -476
- package/node_modules/@comis/memory/dist/identity-link-store.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/identity-link-store.test.js +0 -88
- package/node_modules/@comis/memory/dist/memory-api.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/memory-api.test.js +0 -495
- package/node_modules/@comis/memory/dist/memory-concurrency.test.d.ts +0 -20
- package/node_modules/@comis/memory/dist/memory-concurrency.test.js +0 -222
- package/node_modules/@comis/memory/dist/named-graph-store.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/named-graph-store.test.js +0 -227
- package/node_modules/@comis/memory/dist/observability-store.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/observability-store.test.js +0 -704
- package/node_modules/@comis/memory/dist/row-mapper.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/row-mapper.test.js +0 -409
- package/node_modules/@comis/memory/dist/schema.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/schema.test.js +0 -240
- package/node_modules/@comis/memory/dist/secret-store-schema.test.d.ts +0 -7
- package/node_modules/@comis/memory/dist/secret-store-schema.test.js +0 -90
- package/node_modules/@comis/memory/dist/session-store.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/session-store.test.js +0 -262
- package/node_modules/@comis/memory/dist/setup-secrets.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/setup-secrets.test.js +0 -140
- package/node_modules/@comis/memory/dist/sqlite-memory-adapter.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/sqlite-memory-adapter.test.js +0 -888
- package/node_modules/@comis/memory/dist/sqlite-secret-store.test.d.ts +0 -8
- package/node_modules/@comis/memory/dist/sqlite-secret-store.test.js +0 -245
- package/node_modules/@comis/shared/dist/abort.test.d.ts +0 -1
- package/node_modules/@comis/shared/dist/abort.test.js +0 -71
- package/node_modules/@comis/shared/dist/result.test.d.ts +0 -1
- package/node_modules/@comis/shared/dist/result.test.js +0 -178
- package/node_modules/@comis/shared/dist/suppress-error.test.d.ts +0 -1
- package/node_modules/@comis/shared/dist/suppress-error.test.js +0 -50
- package/node_modules/@comis/shared/dist/timeout.test.d.ts +0 -1
- package/node_modules/@comis/shared/dist/timeout.test.js +0 -75
- package/node_modules/@comis/shared/dist/ttl-cache.test.d.ts +0 -1
- package/node_modules/@comis/shared/dist/ttl-cache.test.js +0 -81
|
@@ -1,678 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for SQLite L2 embedding cache adapter (Phase 513).
|
|
3
|
-
*
|
|
4
|
-
* Uses in-memory SQLite database with initSchema() to create all tables.
|
|
5
|
-
* Verifies DDL, BLOB round-trip, cache hit/miss, UPSERT, composite key
|
|
6
|
-
* isolation, embedBatch delegation, and dispose forwarding.
|
|
7
|
-
*/
|
|
8
|
-
import Database from "better-sqlite3";
|
|
9
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
10
|
-
import { ok, err } from "@comis/shared";
|
|
11
|
-
import { createHash } from "node:crypto";
|
|
12
|
-
import { initSchema } from "./schema.js";
|
|
13
|
-
import { createSqliteEmbeddingCache } from "./embedding-cache-sqlite.js";
|
|
14
|
-
import { computeEmbeddingIdentityHash } from "./embedding-hash.js";
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
// Test helpers
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
const DIMS = 384;
|
|
19
|
-
/** Create a deterministic test vector of given length. */
|
|
20
|
-
function makeVector(dims, seed = 1) {
|
|
21
|
-
return Array.from({ length: dims }, (_, i) => (i + seed) * 0.001);
|
|
22
|
-
}
|
|
23
|
-
/** Create a mock inner EmbeddingPort. */
|
|
24
|
-
function createMockInner(overrides) {
|
|
25
|
-
return {
|
|
26
|
-
provider: "test",
|
|
27
|
-
modelId: "test-model",
|
|
28
|
-
dimensions: DIMS,
|
|
29
|
-
embed: vi.fn(async (_text) => {
|
|
30
|
-
return ok(makeVector(DIMS));
|
|
31
|
-
}),
|
|
32
|
-
embedBatch: vi.fn(async (texts) => {
|
|
33
|
-
return ok(texts.map((_, i) => makeVector(DIMS, i + 1)));
|
|
34
|
-
}),
|
|
35
|
-
dispose: vi.fn(async () => { }),
|
|
36
|
-
...overrides,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
/** Create a fresh in-memory database with schema. */
|
|
40
|
-
function createTestDb() {
|
|
41
|
-
const db = new Database(":memory:");
|
|
42
|
-
initSchema(db, DIMS);
|
|
43
|
-
return db;
|
|
44
|
-
}
|
|
45
|
-
/** Create default cache options for tests. */
|
|
46
|
-
function createTestOptions(db) {
|
|
47
|
-
return { db, maxEntries: 50_000 };
|
|
48
|
-
}
|
|
49
|
-
/** SHA-256 hex hash (mirrors internal hashText). */
|
|
50
|
-
function hashText(text) {
|
|
51
|
-
return createHash("sha256").update(text).digest("hex");
|
|
52
|
-
}
|
|
53
|
-
/** Insert a cache row directly via SQL with controlled timestamps. */
|
|
54
|
-
function insertRawRow(db, text, opts = {}) {
|
|
55
|
-
const { provider = "test", model = "test-model", configHash = computeEmbeddingIdentityHash("test-model", DIMS), createdAt = Date.now(), accessedAt = createdAt, hitCount = 0, seed = 1, } = opts;
|
|
56
|
-
const textHash = hashText(text);
|
|
57
|
-
const f32 = new Float32Array(makeVector(DIMS, seed));
|
|
58
|
-
const blob = Buffer.from(f32.buffer, f32.byteOffset, f32.byteLength);
|
|
59
|
-
db.prepare(`INSERT INTO embedding_cache
|
|
60
|
-
(provider, model, config_hash, text_hash, embedding, dims, hit_count, created_at, accessed_at)
|
|
61
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(provider, model, configHash, textHash, blob, DIMS, hitCount, createdAt, accessedAt);
|
|
62
|
-
}
|
|
63
|
-
/** Small async delay helper for timer-based tests. */
|
|
64
|
-
function delay(ms) {
|
|
65
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
66
|
-
}
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
// Tests
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
describe("embedding_cache DDL", () => {
|
|
71
|
-
it("creates embedding_cache table with correct columns", () => {
|
|
72
|
-
const db = createTestDb();
|
|
73
|
-
const columns = db.prepare("PRAGMA table_info(embedding_cache)").all();
|
|
74
|
-
const colMap = new Map(columns.map((c) => [c.name, c]));
|
|
75
|
-
expect(colMap.get("provider")?.type).toBe("TEXT");
|
|
76
|
-
expect(colMap.get("provider")?.notnull).toBe(1);
|
|
77
|
-
expect(colMap.get("model")?.type).toBe("TEXT");
|
|
78
|
-
expect(colMap.get("config_hash")?.type).toBe("TEXT");
|
|
79
|
-
expect(colMap.get("text_hash")?.type).toBe("TEXT");
|
|
80
|
-
expect(colMap.get("embedding")?.type).toBe("BLOB");
|
|
81
|
-
expect(colMap.get("dims")?.type).toBe("INTEGER");
|
|
82
|
-
expect(colMap.get("hit_count")?.type).toBe("INTEGER");
|
|
83
|
-
expect(colMap.get("created_at")?.type).toBe("INTEGER");
|
|
84
|
-
expect(colMap.get("accessed_at")?.type).toBe("INTEGER");
|
|
85
|
-
// Composite PK: all 4 columns should have pk > 0
|
|
86
|
-
expect(colMap.get("provider")?.pk).toBeGreaterThan(0);
|
|
87
|
-
expect(colMap.get("model")?.pk).toBeGreaterThan(0);
|
|
88
|
-
expect(colMap.get("config_hash")?.pk).toBeGreaterThan(0);
|
|
89
|
-
expect(colMap.get("text_hash")?.pk).toBeGreaterThan(0);
|
|
90
|
-
db.close();
|
|
91
|
-
});
|
|
92
|
-
it("creates embedding_provider_meta table in initSchema", () => {
|
|
93
|
-
const db = createTestDb();
|
|
94
|
-
const columns = db.prepare("PRAGMA table_info(embedding_provider_meta)").all();
|
|
95
|
-
const colMap = new Map(columns.map((c) => [c.name, c]));
|
|
96
|
-
expect(colMap.get("key")?.type).toBe("TEXT");
|
|
97
|
-
expect(colMap.get("value")?.type).toBe("TEXT");
|
|
98
|
-
db.close();
|
|
99
|
-
});
|
|
100
|
-
it("creates accessed_at index", () => {
|
|
101
|
-
const db = createTestDb();
|
|
102
|
-
const indexes = db.prepare("PRAGMA index_list(embedding_cache)").all();
|
|
103
|
-
const indexNames = indexes.map((idx) => idx.name);
|
|
104
|
-
expect(indexNames).toContain("idx_embedding_cache_accessed");
|
|
105
|
-
db.close();
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
describe("createSqliteEmbeddingCache", () => {
|
|
109
|
-
let db;
|
|
110
|
-
let inner;
|
|
111
|
-
let cache;
|
|
112
|
-
beforeEach(() => {
|
|
113
|
-
db = createTestDb();
|
|
114
|
-
inner = createMockInner();
|
|
115
|
-
cache = createSqliteEmbeddingCache(inner, createTestOptions(db));
|
|
116
|
-
});
|
|
117
|
-
it("forwards provider, modelId, dimensions from inner", () => {
|
|
118
|
-
expect(cache.provider).toBe("test");
|
|
119
|
-
expect(cache.modelId).toBe("test-model");
|
|
120
|
-
expect(cache.dimensions).toBe(DIMS);
|
|
121
|
-
});
|
|
122
|
-
it("stores BLOB and retrieves with correct Float32 values", async () => {
|
|
123
|
-
const vector = makeVector(DIMS);
|
|
124
|
-
inner.embed.mockResolvedValueOnce(ok(vector));
|
|
125
|
-
// First call: cache miss, stores BLOB
|
|
126
|
-
const result1 = await cache.embed("hello world");
|
|
127
|
-
expect(result1.ok).toBe(true);
|
|
128
|
-
if (!result1.ok)
|
|
129
|
-
throw new Error("unexpected");
|
|
130
|
-
// Verify values are close (Float32 may lose precision)
|
|
131
|
-
for (let i = 0; i < DIMS; i++) {
|
|
132
|
-
expect(result1.value[i]).toBeCloseTo(vector[i], 5);
|
|
133
|
-
}
|
|
134
|
-
// Second call: cache hit, should return from BLOB
|
|
135
|
-
const result2 = await cache.embed("hello world");
|
|
136
|
-
expect(result2.ok).toBe(true);
|
|
137
|
-
if (!result2.ok)
|
|
138
|
-
throw new Error("unexpected");
|
|
139
|
-
for (let i = 0; i < DIMS; i++) {
|
|
140
|
-
expect(result2.value[i]).toBeCloseTo(vector[i], 5);
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
it("cache hit does not call inner provider", async () => {
|
|
144
|
-
const embedSpy = inner.embed;
|
|
145
|
-
// First call: miss
|
|
146
|
-
await cache.embed("cached text");
|
|
147
|
-
expect(embedSpy).toHaveBeenCalledTimes(1);
|
|
148
|
-
// Second call: hit
|
|
149
|
-
await cache.embed("cached text");
|
|
150
|
-
expect(embedSpy).toHaveBeenCalledTimes(1); // Still 1 -- no extra call
|
|
151
|
-
});
|
|
152
|
-
it("UPSERT overwrites on re-embed with different vector", async () => {
|
|
153
|
-
const embedSpy = inner.embed;
|
|
154
|
-
const vector1 = makeVector(DIMS, 1);
|
|
155
|
-
const vector2 = makeVector(DIMS, 99);
|
|
156
|
-
embedSpy.mockResolvedValueOnce(ok(vector1));
|
|
157
|
-
// Initial embed
|
|
158
|
-
await cache.embed("update-test");
|
|
159
|
-
expect(embedSpy).toHaveBeenCalledTimes(1);
|
|
160
|
-
// Simulate provider returning a different vector and force a miss by
|
|
161
|
-
// creating a fresh cache instance (new prepared statements, same db)
|
|
162
|
-
embedSpy.mockResolvedValueOnce(ok(vector2));
|
|
163
|
-
const cache2 = createSqliteEmbeddingCache(inner, createTestOptions(db));
|
|
164
|
-
// The first cache stored vector1. The new cache sees the same row.
|
|
165
|
-
// To test UPSERT, we directly insert with a known text_hash to force
|
|
166
|
-
// ON CONFLICT. We'll verify via raw SQL.
|
|
167
|
-
await cache2.embed("update-test"); // hits the existing cache entry
|
|
168
|
-
// It should be a HIT (returns vector1 from cache), not calling inner again
|
|
169
|
-
expect(embedSpy).toHaveBeenCalledTimes(1); // Still 1 from the vector2 mock not being used
|
|
170
|
-
// Now verify UPSERT by inserting via raw SQL to the same key with different data
|
|
171
|
-
const row = db.prepare(`SELECT embedding, dims FROM embedding_cache
|
|
172
|
-
WHERE provider = 'test' AND model = 'test-model' AND text_hash = ?`).get(createHash("sha256").update("update-test").digest("hex"));
|
|
173
|
-
expect(row).toBeDefined();
|
|
174
|
-
expect(row.dims).toBe(DIMS);
|
|
175
|
-
// Verify the stored vector is vector1 (from the first embed)
|
|
176
|
-
const stored = new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength / Float32Array.BYTES_PER_ELEMENT);
|
|
177
|
-
expect(stored.length).toBe(DIMS);
|
|
178
|
-
expect(stored[0]).toBeCloseTo(vector1[0], 5);
|
|
179
|
-
});
|
|
180
|
-
it("UPSERT preserves rowid on conflict (ON CONFLICT DO UPDATE)", async () => {
|
|
181
|
-
const embedSpy = inner.embed;
|
|
182
|
-
const vector1 = makeVector(DIMS, 1);
|
|
183
|
-
const vector2 = makeVector(DIMS, 42);
|
|
184
|
-
embedSpy.mockResolvedValueOnce(ok(vector1));
|
|
185
|
-
await cache.embed("rowid-test");
|
|
186
|
-
// Get the rowid
|
|
187
|
-
const row1 = db.prepare("SELECT rowid FROM embedding_cache WHERE text_hash = ?").get(createHash("sha256").update("rowid-test").digest("hex"));
|
|
188
|
-
// Force re-embed by using the upsert stmt directly via a second cache miss
|
|
189
|
-
// We need the cache to miss, so use a new cache instance that skips the LRU
|
|
190
|
-
// But since the cache checks SQLite, it will hit. Instead, directly call the
|
|
191
|
-
// upsert statement via the db to test rowid stability.
|
|
192
|
-
const configHash = computeEmbeddingIdentityHash("test-model", DIMS);
|
|
193
|
-
const textHash = createHash("sha256").update("rowid-test").digest("hex");
|
|
194
|
-
const blob = Buffer.from(new Float32Array(vector2).buffer);
|
|
195
|
-
const now = Date.now();
|
|
196
|
-
db.prepare(`INSERT INTO embedding_cache
|
|
197
|
-
(provider, model, config_hash, text_hash, embedding, dims, hit_count, created_at, accessed_at)
|
|
198
|
-
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
199
|
-
ON CONFLICT(provider, model, config_hash, text_hash) DO UPDATE SET
|
|
200
|
-
embedding = excluded.embedding,
|
|
201
|
-
dims = excluded.dims,
|
|
202
|
-
hit_count = 0,
|
|
203
|
-
accessed_at = excluded.accessed_at`).run("test", "test-model", configHash, textHash, blob, DIMS, now, now);
|
|
204
|
-
const row2 = db.prepare("SELECT rowid FROM embedding_cache WHERE text_hash = ?").get(textHash);
|
|
205
|
-
// Rowid should be the same (ON CONFLICT DO UPDATE preserves rowid)
|
|
206
|
-
expect(row2.rowid).toBe(row1.rowid);
|
|
207
|
-
});
|
|
208
|
-
it("composite key isolates entries by provider", async () => {
|
|
209
|
-
const innerA = createMockInner({ provider: "provider-a" });
|
|
210
|
-
const innerB = createMockInner({ provider: "provider-b" });
|
|
211
|
-
const vectorA = makeVector(DIMS, 10);
|
|
212
|
-
const vectorB = makeVector(DIMS, 20);
|
|
213
|
-
innerA.embed.mockResolvedValueOnce(ok(vectorA));
|
|
214
|
-
innerB.embed.mockResolvedValueOnce(ok(vectorB));
|
|
215
|
-
const cacheA = createSqliteEmbeddingCache(innerA, createTestOptions(db));
|
|
216
|
-
const cacheB = createSqliteEmbeddingCache(innerB, createTestOptions(db));
|
|
217
|
-
await cacheA.embed("shared-text");
|
|
218
|
-
await cacheB.embed("shared-text");
|
|
219
|
-
// Both providers should have stored separate entries
|
|
220
|
-
const count = db.prepare("SELECT COUNT(*) as cnt FROM embedding_cache WHERE text_hash = ?")
|
|
221
|
-
.get(createHash("sha256").update("shared-text").digest("hex"));
|
|
222
|
-
expect(count.cnt).toBe(2);
|
|
223
|
-
});
|
|
224
|
-
it("composite key isolates entries by model", async () => {
|
|
225
|
-
const innerA = createMockInner({ modelId: "model-a" });
|
|
226
|
-
const innerB = createMockInner({ modelId: "model-b" });
|
|
227
|
-
innerA.embed.mockResolvedValueOnce(ok(makeVector(DIMS, 1)));
|
|
228
|
-
innerB.embed.mockResolvedValueOnce(ok(makeVector(DIMS, 2)));
|
|
229
|
-
const cacheA = createSqliteEmbeddingCache(innerA, createTestOptions(db));
|
|
230
|
-
const cacheB = createSqliteEmbeddingCache(innerB, createTestOptions(db));
|
|
231
|
-
await cacheA.embed("shared-text");
|
|
232
|
-
await cacheB.embed("shared-text");
|
|
233
|
-
const count = db.prepare("SELECT COUNT(*) as cnt FROM embedding_cache").get();
|
|
234
|
-
expect(count.cnt).toBe(2);
|
|
235
|
-
});
|
|
236
|
-
// -------------------------------------------------------------------------
|
|
237
|
-
// embedBatch: batch SELECT hit/miss splitting (Plan 02)
|
|
238
|
-
// -------------------------------------------------------------------------
|
|
239
|
-
it("embedBatch splits hits and misses correctly", async () => {
|
|
240
|
-
const embedSpy = inner.embed;
|
|
241
|
-
const batchSpy = inner.embedBatch;
|
|
242
|
-
// Pre-populate cache with 3 embeddings via single embed() calls
|
|
243
|
-
const vectors = [makeVector(DIMS, 10), makeVector(DIMS, 20), makeVector(DIMS, 30)];
|
|
244
|
-
embedSpy
|
|
245
|
-
.mockResolvedValueOnce(ok(vectors[0]))
|
|
246
|
-
.mockResolvedValueOnce(ok(vectors[1]))
|
|
247
|
-
.mockResolvedValueOnce(ok(vectors[2]));
|
|
248
|
-
await cache.embed("cached-a");
|
|
249
|
-
await cache.embed("cached-b");
|
|
250
|
-
await cache.embed("cached-c");
|
|
251
|
-
// Mock the batch response for the 2 misses
|
|
252
|
-
const missVectors = [makeVector(DIMS, 40), makeVector(DIMS, 50)];
|
|
253
|
-
batchSpy.mockResolvedValueOnce(ok(missVectors));
|
|
254
|
-
// Call embedBatch with 5 texts (3 cached + 2 new)
|
|
255
|
-
const result = await cache.embedBatch([
|
|
256
|
-
"cached-a", "new-d", "cached-b", "new-e", "cached-c",
|
|
257
|
-
]);
|
|
258
|
-
expect(result.ok).toBe(true);
|
|
259
|
-
if (!result.ok)
|
|
260
|
-
throw new Error("unexpected");
|
|
261
|
-
// inner.embedBatch called with only the 2 miss texts
|
|
262
|
-
expect(batchSpy).toHaveBeenCalledTimes(1);
|
|
263
|
-
expect(batchSpy).toHaveBeenCalledWith(["new-d", "new-e"]);
|
|
264
|
-
// Verify all 5 results in original order
|
|
265
|
-
expect(result.value.length).toBe(5);
|
|
266
|
-
expect(result.value[0][0]).toBeCloseTo(vectors[0][0], 5); // cached-a
|
|
267
|
-
expect(result.value[1][0]).toBeCloseTo(missVectors[0][0], 5); // new-d
|
|
268
|
-
expect(result.value[2][0]).toBeCloseTo(vectors[1][0], 5); // cached-b
|
|
269
|
-
expect(result.value[3][0]).toBeCloseTo(missVectors[1][0], 5); // new-e
|
|
270
|
-
expect(result.value[4][0]).toBeCloseTo(vectors[2][0], 5); // cached-c
|
|
271
|
-
});
|
|
272
|
-
it("embedBatch all hits returns without calling provider", async () => {
|
|
273
|
-
const embedSpy = inner.embed;
|
|
274
|
-
const batchSpy = inner.embedBatch;
|
|
275
|
-
// Pre-populate cache with 3 embeddings
|
|
276
|
-
const vectors = [makeVector(DIMS, 1), makeVector(DIMS, 2), makeVector(DIMS, 3)];
|
|
277
|
-
embedSpy
|
|
278
|
-
.mockResolvedValueOnce(ok(vectors[0]))
|
|
279
|
-
.mockResolvedValueOnce(ok(vectors[1]))
|
|
280
|
-
.mockResolvedValueOnce(ok(vectors[2]));
|
|
281
|
-
await cache.embed("hit-a");
|
|
282
|
-
await cache.embed("hit-b");
|
|
283
|
-
await cache.embed("hit-c");
|
|
284
|
-
// Call embedBatch with the same 3 texts
|
|
285
|
-
const result = await cache.embedBatch(["hit-a", "hit-b", "hit-c"]);
|
|
286
|
-
expect(result.ok).toBe(true);
|
|
287
|
-
if (!result.ok)
|
|
288
|
-
throw new Error("unexpected");
|
|
289
|
-
// inner.embedBatch NOT called -- all cache hits
|
|
290
|
-
expect(batchSpy).not.toHaveBeenCalled();
|
|
291
|
-
// Results match cached values
|
|
292
|
-
expect(result.value.length).toBe(3);
|
|
293
|
-
expect(result.value[0][0]).toBeCloseTo(vectors[0][0], 5);
|
|
294
|
-
expect(result.value[1][0]).toBeCloseTo(vectors[1][0], 5);
|
|
295
|
-
expect(result.value[2][0]).toBeCloseTo(vectors[2][0], 5);
|
|
296
|
-
});
|
|
297
|
-
it("embedBatch all misses calls provider for full batch", async () => {
|
|
298
|
-
const batchSpy = inner.embedBatch;
|
|
299
|
-
// Empty cache -- all misses
|
|
300
|
-
const vectors = [makeVector(DIMS, 1), makeVector(DIMS, 2), makeVector(DIMS, 3)];
|
|
301
|
-
batchSpy.mockResolvedValueOnce(ok(vectors));
|
|
302
|
-
const result = await cache.embedBatch(["miss-a", "miss-b", "miss-c"]);
|
|
303
|
-
expect(result.ok).toBe(true);
|
|
304
|
-
if (!result.ok)
|
|
305
|
-
throw new Error("unexpected");
|
|
306
|
-
// Provider called with all 3 texts
|
|
307
|
-
expect(batchSpy).toHaveBeenCalledTimes(1);
|
|
308
|
-
expect(batchSpy).toHaveBeenCalledWith(["miss-a", "miss-b", "miss-c"]);
|
|
309
|
-
// Verify all stored in cache -- subsequent call returns from cache
|
|
310
|
-
batchSpy.mockClear();
|
|
311
|
-
const result2 = await cache.embedBatch(["miss-a", "miss-b", "miss-c"]);
|
|
312
|
-
expect(result2.ok).toBe(true);
|
|
313
|
-
expect(batchSpy).not.toHaveBeenCalled(); // All hits now
|
|
314
|
-
});
|
|
315
|
-
it("embedBatch empty array returns empty array", async () => {
|
|
316
|
-
const batchSpy = inner.embedBatch;
|
|
317
|
-
const result = await cache.embedBatch([]);
|
|
318
|
-
expect(result.ok).toBe(true);
|
|
319
|
-
if (!result.ok)
|
|
320
|
-
throw new Error("unexpected");
|
|
321
|
-
expect(result.value).toEqual([]);
|
|
322
|
-
// Provider NOT called
|
|
323
|
-
expect(batchSpy).not.toHaveBeenCalled();
|
|
324
|
-
});
|
|
325
|
-
it("embedBatch UPSERT is transactional (all-or-nothing)", async () => {
|
|
326
|
-
const batchSpy = inner.embedBatch;
|
|
327
|
-
const vectors = [makeVector(DIMS, 1), makeVector(DIMS, 2), makeVector(DIMS, 3)];
|
|
328
|
-
batchSpy.mockResolvedValueOnce(ok(vectors));
|
|
329
|
-
// Call embedBatch with 3 texts
|
|
330
|
-
await cache.embedBatch(["tx-a", "tx-b", "tx-c"]);
|
|
331
|
-
// Verify all 3 entries exist via direct SQL SELECT
|
|
332
|
-
const count = db.prepare("SELECT COUNT(*) as cnt FROM embedding_cache").get();
|
|
333
|
-
expect(count.cnt).toBe(3);
|
|
334
|
-
// Verify each entry individually
|
|
335
|
-
for (const text of ["tx-a", "tx-b", "tx-c"]) {
|
|
336
|
-
const hash = createHash("sha256").update(text).digest("hex");
|
|
337
|
-
const row = db.prepare("SELECT embedding FROM embedding_cache WHERE text_hash = ?").get(hash);
|
|
338
|
-
expect(row).toBeDefined();
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
it("embedBatch preserves ordering with mixed hit/miss pattern", async () => {
|
|
342
|
-
const embedSpy = inner.embed;
|
|
343
|
-
const batchSpy = inner.embedBatch;
|
|
344
|
-
// Pre-populate A and C (not B, D, E)
|
|
345
|
-
const vecA = makeVector(DIMS, 100);
|
|
346
|
-
const vecC = makeVector(DIMS, 300);
|
|
347
|
-
embedSpy
|
|
348
|
-
.mockResolvedValueOnce(ok(vecA))
|
|
349
|
-
.mockResolvedValueOnce(ok(vecC));
|
|
350
|
-
await cache.embed("order-A");
|
|
351
|
-
await cache.embed("order-C");
|
|
352
|
-
// Mock batch response for misses B, D, E
|
|
353
|
-
const vecB = makeVector(DIMS, 200);
|
|
354
|
-
const vecD = makeVector(DIMS, 400);
|
|
355
|
-
const vecE = makeVector(DIMS, 500);
|
|
356
|
-
batchSpy.mockResolvedValueOnce(ok([vecB, vecD, vecE]));
|
|
357
|
-
// Call embedBatch([A, B, C, D, E]) -- A,C are hits; B,D,E are misses
|
|
358
|
-
const result = await cache.embedBatch([
|
|
359
|
-
"order-A", "order-B", "order-C", "order-D", "order-E",
|
|
360
|
-
]);
|
|
361
|
-
expect(result.ok).toBe(true);
|
|
362
|
-
if (!result.ok)
|
|
363
|
-
throw new Error("unexpected");
|
|
364
|
-
expect(result.value.length).toBe(5);
|
|
365
|
-
// result[0] = A's embedding (cached hit)
|
|
366
|
-
expect(result.value[0][0]).toBeCloseTo(vecA[0], 5);
|
|
367
|
-
// result[1] = B's embedding (miss, first from provider batch)
|
|
368
|
-
expect(result.value[1][0]).toBeCloseTo(vecB[0], 5);
|
|
369
|
-
// result[2] = C's embedding (cached hit)
|
|
370
|
-
expect(result.value[2][0]).toBeCloseTo(vecC[0], 5);
|
|
371
|
-
// result[3] = D's embedding (miss, second from provider batch)
|
|
372
|
-
expect(result.value[3][0]).toBeCloseTo(vecD[0], 5);
|
|
373
|
-
// result[4] = E's embedding (miss, third from provider batch)
|
|
374
|
-
expect(result.value[4][0]).toBeCloseTo(vecE[0], 5);
|
|
375
|
-
// Provider called with only the miss texts in order
|
|
376
|
-
expect(batchSpy).toHaveBeenCalledWith(["order-B", "order-D", "order-E"]);
|
|
377
|
-
});
|
|
378
|
-
it("embedBatch with moderate batch validates batch SELECT works", async () => {
|
|
379
|
-
const embedSpy = inner.embed;
|
|
380
|
-
const batchSpy = inner.embedBatch;
|
|
381
|
-
// Pre-populate 5 entries via single embed()
|
|
382
|
-
const cachedVecs = [];
|
|
383
|
-
for (let i = 0; i < 5; i++) {
|
|
384
|
-
const vec = makeVector(DIMS, i + 1);
|
|
385
|
-
cachedVecs.push(vec);
|
|
386
|
-
embedSpy.mockResolvedValueOnce(ok(vec));
|
|
387
|
-
await cache.embed(`batch-text-${i}`);
|
|
388
|
-
}
|
|
389
|
-
// Build a batch of 10: 5 hits + 5 misses
|
|
390
|
-
const texts = [];
|
|
391
|
-
for (let i = 0; i < 10; i++) {
|
|
392
|
-
texts.push(`batch-text-${i}`);
|
|
393
|
-
}
|
|
394
|
-
// Mock provider response for the 5 misses
|
|
395
|
-
const missVecs = Array.from({ length: 5 }, (_, i) => makeVector(DIMS, i + 100));
|
|
396
|
-
batchSpy.mockResolvedValueOnce(ok(missVecs));
|
|
397
|
-
const result = await cache.embedBatch(texts);
|
|
398
|
-
expect(result.ok).toBe(true);
|
|
399
|
-
if (!result.ok)
|
|
400
|
-
throw new Error("unexpected");
|
|
401
|
-
// Verify only 5 misses sent to provider
|
|
402
|
-
expect(batchSpy).toHaveBeenCalledTimes(1);
|
|
403
|
-
const callArgs = batchSpy.mock.calls[0][0];
|
|
404
|
-
expect(callArgs.length).toBe(5);
|
|
405
|
-
// Verify all 10 results returned
|
|
406
|
-
expect(result.value.length).toBe(10);
|
|
407
|
-
// First 5 should match cached vectors (Float32 precision)
|
|
408
|
-
for (let i = 0; i < 5; i++) {
|
|
409
|
-
expect(result.value[i][0]).toBeCloseTo(cachedVecs[i][0], 5);
|
|
410
|
-
}
|
|
411
|
-
// Last 5 should match miss vectors
|
|
412
|
-
for (let i = 0; i < 5; i++) {
|
|
413
|
-
expect(result.value[i + 5][0]).toBeCloseTo(missVecs[i][0], 5);
|
|
414
|
-
}
|
|
415
|
-
});
|
|
416
|
-
it("embedBatch returns error if inner.embedBatch fails", async () => {
|
|
417
|
-
const batchSpy = inner.embedBatch;
|
|
418
|
-
batchSpy.mockResolvedValueOnce(err(new Error("provider down")));
|
|
419
|
-
const result = await cache.embedBatch(["fail-a", "fail-b"]);
|
|
420
|
-
expect(result.ok).toBe(false);
|
|
421
|
-
if (result.ok)
|
|
422
|
-
throw new Error("unexpected");
|
|
423
|
-
expect(result.error.message).toBe("provider down");
|
|
424
|
-
});
|
|
425
|
-
it("dispose forwards to inner", async () => {
|
|
426
|
-
const disposeSpy = inner.dispose;
|
|
427
|
-
await cache.dispose?.();
|
|
428
|
-
expect(disposeSpy).toHaveBeenCalledTimes(1);
|
|
429
|
-
});
|
|
430
|
-
it("does not cache on inner embed error", async () => {
|
|
431
|
-
const embedSpy = inner.embed;
|
|
432
|
-
embedSpy.mockResolvedValueOnce(err(new Error("API error")));
|
|
433
|
-
const result = await cache.embed("error-text");
|
|
434
|
-
expect(result.ok).toBe(false);
|
|
435
|
-
// Verify nothing was stored
|
|
436
|
-
const count = db.prepare("SELECT COUNT(*) as cnt FROM embedding_cache").get();
|
|
437
|
-
expect(count.cnt).toBe(0);
|
|
438
|
-
});
|
|
439
|
-
});
|
|
440
|
-
// ---------------------------------------------------------------------------
|
|
441
|
-
// Cache maintenance: lazy buffer, LRU prune, TTL, WAL checkpoint, dispose
|
|
442
|
-
// (Plan 03)
|
|
443
|
-
// ---------------------------------------------------------------------------
|
|
444
|
-
describe("cache maintenance", () => {
|
|
445
|
-
let db;
|
|
446
|
-
let inner;
|
|
447
|
-
beforeEach(() => {
|
|
448
|
-
db = createTestDb();
|
|
449
|
-
inner = createMockInner();
|
|
450
|
-
});
|
|
451
|
-
afterEach(() => {
|
|
452
|
-
// Ensure db is closed to avoid leaking handles
|
|
453
|
-
try {
|
|
454
|
-
db.close();
|
|
455
|
-
}
|
|
456
|
-
catch { /* already closed */ }
|
|
457
|
-
});
|
|
458
|
-
it("recordAccess buffers and does not write on every hit", async () => {
|
|
459
|
-
const cache = createSqliteEmbeddingCache(inner, {
|
|
460
|
-
db,
|
|
461
|
-
maxEntries: 50_000,
|
|
462
|
-
pruneIntervalMs: 600_000, // large -- prevent timer interference
|
|
463
|
-
});
|
|
464
|
-
// Seed the cache with one entry via embed() (cache miss)
|
|
465
|
-
const embedSpy = inner.embed;
|
|
466
|
-
embedSpy.mockResolvedValueOnce(ok(makeVector(DIMS)));
|
|
467
|
-
await cache.embed("buffered-text");
|
|
468
|
-
// Record the accessed_at after the initial insert
|
|
469
|
-
const textHash = hashText("buffered-text");
|
|
470
|
-
const initialRow = db.prepare("SELECT accessed_at FROM embedding_cache WHERE text_hash = ?").get(textHash);
|
|
471
|
-
const initialAccessed = initialRow.accessed_at;
|
|
472
|
-
// Wait a small moment so Date.now() differs
|
|
473
|
-
await delay(5);
|
|
474
|
-
// Hit the cache 5 more times (under the 100 threshold)
|
|
475
|
-
for (let i = 0; i < 5; i++) {
|
|
476
|
-
await cache.embed("buffered-text");
|
|
477
|
-
}
|
|
478
|
-
// Buffer has NOT flushed yet (only 5 hits, threshold is 100)
|
|
479
|
-
// So accessed_at in SQLite should still be the original value
|
|
480
|
-
const afterRow = db.prepare("SELECT accessed_at, hit_count FROM embedding_cache WHERE text_hash = ?").get(textHash);
|
|
481
|
-
expect(afterRow.accessed_at).toBe(initialAccessed);
|
|
482
|
-
expect(afterRow.hit_count).toBe(0); // Not flushed yet
|
|
483
|
-
await cache.dispose?.();
|
|
484
|
-
});
|
|
485
|
-
it("lazy buffer flushes at 100-entry threshold", async () => {
|
|
486
|
-
const cache = createSqliteEmbeddingCache(inner, {
|
|
487
|
-
db,
|
|
488
|
-
maxEntries: 50_000,
|
|
489
|
-
pruneIntervalMs: 600_000,
|
|
490
|
-
});
|
|
491
|
-
const embedSpy = inner.embed;
|
|
492
|
-
// Pre-populate 101 distinct texts
|
|
493
|
-
for (let i = 0; i < 101; i++) {
|
|
494
|
-
embedSpy.mockResolvedValueOnce(ok(makeVector(DIMS, i + 1)));
|
|
495
|
-
await cache.embed(`threshold-text-${i}`);
|
|
496
|
-
}
|
|
497
|
-
// Now hit each of the first 100 texts (cache hits) to build up the buffer
|
|
498
|
-
// After the 100th *distinct* text_hash enters the buffer, it should auto-flush
|
|
499
|
-
for (let i = 0; i < 100; i++) {
|
|
500
|
-
await cache.embed(`threshold-text-${i}`);
|
|
501
|
-
}
|
|
502
|
-
// The flush should have happened at the 100th entry
|
|
503
|
-
// Check that at least some entries have hit_count > 0
|
|
504
|
-
const updated = db.prepare("SELECT COUNT(*) as cnt FROM embedding_cache WHERE hit_count > 0").get();
|
|
505
|
-
// All 100 buffered entries should have been flushed (hit_count incremented)
|
|
506
|
-
expect(updated.cnt).toBe(100);
|
|
507
|
-
await cache.dispose?.();
|
|
508
|
-
});
|
|
509
|
-
it("LRU prune removes least-recently-accessed entries", async () => {
|
|
510
|
-
// Use short pruneIntervalMs to trigger prune via timer
|
|
511
|
-
const cache = createSqliteEmbeddingCache(inner, {
|
|
512
|
-
db,
|
|
513
|
-
maxEntries: 5,
|
|
514
|
-
pruneIntervalMs: 30,
|
|
515
|
-
});
|
|
516
|
-
// Insert 10 entries with staggered accessed_at times via raw SQL
|
|
517
|
-
const baseTime = Date.now() - 10_000;
|
|
518
|
-
for (let i = 0; i < 10; i++) {
|
|
519
|
-
insertRawRow(db, `lru-text-${i}`, {
|
|
520
|
-
createdAt: baseTime + i * 100,
|
|
521
|
-
accessedAt: baseTime + i * 100,
|
|
522
|
-
seed: i + 1,
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
// Verify we have 10 entries
|
|
526
|
-
const before = db.prepare("SELECT COUNT(*) as cnt FROM embedding_cache").get();
|
|
527
|
-
expect(before.cnt).toBe(10);
|
|
528
|
-
// Wait for prune timer to fire
|
|
529
|
-
await delay(100);
|
|
530
|
-
// After prune, only maxEntries (5) should remain -- the 5 most recently accessed
|
|
531
|
-
const after = db.prepare("SELECT COUNT(*) as cnt FROM embedding_cache").get();
|
|
532
|
-
expect(after.cnt).toBe(5);
|
|
533
|
-
// The surviving entries should be the ones with highest accessed_at (texts 5-9)
|
|
534
|
-
for (let i = 5; i < 10; i++) {
|
|
535
|
-
const row = db.prepare("SELECT 1 as found FROM embedding_cache WHERE text_hash = ?").get(hashText(`lru-text-${i}`));
|
|
536
|
-
expect(row).toBeDefined();
|
|
537
|
-
}
|
|
538
|
-
// The oldest entries (0-4) should have been evicted
|
|
539
|
-
for (let i = 0; i < 5; i++) {
|
|
540
|
-
const row = db.prepare("SELECT 1 as found FROM embedding_cache WHERE text_hash = ?").get(hashText(`lru-text-${i}`));
|
|
541
|
-
expect(row).toBeUndefined();
|
|
542
|
-
}
|
|
543
|
-
await cache.dispose?.();
|
|
544
|
-
});
|
|
545
|
-
it("prune flushes lazy buffer before eviction (Pitfall 6)", async () => {
|
|
546
|
-
// maxEntries: 3, insert 5 texts, hit the oldest one to refresh it
|
|
547
|
-
const cache = createSqliteEmbeddingCache(inner, {
|
|
548
|
-
db,
|
|
549
|
-
maxEntries: 3,
|
|
550
|
-
pruneIntervalMs: 30,
|
|
551
|
-
});
|
|
552
|
-
// Insert 5 entries with staggered times. Entry #0 is the oldest.
|
|
553
|
-
const baseTime = Date.now() - 10_000;
|
|
554
|
-
for (let i = 0; i < 5; i++) {
|
|
555
|
-
insertRawRow(db, `pitfall-text-${i}`, {
|
|
556
|
-
createdAt: baseTime + i * 100,
|
|
557
|
-
accessedAt: baseTime + i * 100,
|
|
558
|
-
seed: i + 1,
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
// Hit text #0 (oldest by created_at/accessed_at) to make it recently-accessed
|
|
562
|
-
// This access goes into the lazy buffer -- NOT yet persisted to SQLite
|
|
563
|
-
const embedSpy = inner.embed;
|
|
564
|
-
await cache.embed(`pitfall-text-0`); // cache hit -> recordAccess(textHash)
|
|
565
|
-
// Wait for prune timer to fire
|
|
566
|
-
await delay(100);
|
|
567
|
-
// Text #0 should survive because prune flushes the buffer first,
|
|
568
|
-
// updating its accessed_at to a recent time
|
|
569
|
-
const row0 = db.prepare("SELECT 1 as found FROM embedding_cache WHERE text_hash = ?").get(hashText("pitfall-text-0"));
|
|
570
|
-
expect(row0).toBeDefined();
|
|
571
|
-
// Only 3 entries should remain total
|
|
572
|
-
const count = db.prepare("SELECT COUNT(*) as cnt FROM embedding_cache").get();
|
|
573
|
-
expect(count.cnt).toBe(3);
|
|
574
|
-
// Texts #1 and #2 (the next oldest, NOT recently accessed) should be evicted
|
|
575
|
-
const row1 = db.prepare("SELECT 1 as found FROM embedding_cache WHERE text_hash = ?").get(hashText("pitfall-text-1"));
|
|
576
|
-
const row2 = db.prepare("SELECT 1 as found FROM embedding_cache WHERE text_hash = ?").get(hashText("pitfall-text-2"));
|
|
577
|
-
expect(row1).toBeUndefined();
|
|
578
|
-
expect(row2).toBeUndefined();
|
|
579
|
-
await cache.dispose?.();
|
|
580
|
-
});
|
|
581
|
-
it("TTL expiration removes old entries", async () => {
|
|
582
|
-
const cache = createSqliteEmbeddingCache(inner, {
|
|
583
|
-
db,
|
|
584
|
-
maxEntries: 50_000,
|
|
585
|
-
ttlMs: 100, // 100ms TTL for test speed
|
|
586
|
-
pruneIntervalMs: 30,
|
|
587
|
-
});
|
|
588
|
-
// Insert 3 "old" entries with created_at in the past (> 100ms ago)
|
|
589
|
-
const oldTime = Date.now() - 500;
|
|
590
|
-
for (let i = 0; i < 3; i++) {
|
|
591
|
-
insertRawRow(db, `old-text-${i}`, {
|
|
592
|
-
createdAt: oldTime,
|
|
593
|
-
accessedAt: oldTime,
|
|
594
|
-
seed: i + 1,
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
// Insert 1 "recent" entry with current timestamp
|
|
598
|
-
insertRawRow(db, "recent-text", {
|
|
599
|
-
createdAt: Date.now(),
|
|
600
|
-
accessedAt: Date.now(),
|
|
601
|
-
seed: 99,
|
|
602
|
-
});
|
|
603
|
-
// Verify 4 entries before prune
|
|
604
|
-
const before = db.prepare("SELECT COUNT(*) as cnt FROM embedding_cache").get();
|
|
605
|
-
expect(before.cnt).toBe(4);
|
|
606
|
-
// Wait for prune timer to fire
|
|
607
|
-
await delay(100);
|
|
608
|
-
// The 3 old entries should be deleted, the recent one should remain
|
|
609
|
-
const after = db.prepare("SELECT COUNT(*) as cnt FROM embedding_cache").get();
|
|
610
|
-
expect(after.cnt).toBe(1);
|
|
611
|
-
const recent = db.prepare("SELECT 1 as found FROM embedding_cache WHERE text_hash = ?").get(hashText("recent-text"));
|
|
612
|
-
expect(recent).toBeDefined();
|
|
613
|
-
await cache.dispose?.();
|
|
614
|
-
});
|
|
615
|
-
it("WAL checkpoint runs after prune without error", async () => {
|
|
616
|
-
// This test verifies prune + WAL checkpoint completes without throwing
|
|
617
|
-
const cache = createSqliteEmbeddingCache(inner, {
|
|
618
|
-
db,
|
|
619
|
-
maxEntries: 2,
|
|
620
|
-
pruneIntervalMs: 30,
|
|
621
|
-
});
|
|
622
|
-
// Insert 5 entries (exceeds maxEntries of 2)
|
|
623
|
-
for (let i = 0; i < 5; i++) {
|
|
624
|
-
insertRawRow(db, `wal-text-${i}`, {
|
|
625
|
-
createdAt: Date.now() - 1000 + i * 100,
|
|
626
|
-
accessedAt: Date.now() - 1000 + i * 100,
|
|
627
|
-
seed: i + 1,
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
// Wait for prune to fire and complete (including WAL checkpoint)
|
|
631
|
-
await delay(100);
|
|
632
|
-
// If WAL checkpoint threw, the test would fail. Verify prune happened.
|
|
633
|
-
const count = db.prepare("SELECT COUNT(*) as cnt FROM embedding_cache").get();
|
|
634
|
-
expect(count.cnt).toBe(2);
|
|
635
|
-
await cache.dispose?.();
|
|
636
|
-
});
|
|
637
|
-
it("dispose flushes buffer and clears timer", async () => {
|
|
638
|
-
const disposeSpy = inner.dispose;
|
|
639
|
-
const cache = createSqliteEmbeddingCache(inner, {
|
|
640
|
-
db,
|
|
641
|
-
maxEntries: 50_000,
|
|
642
|
-
pruneIntervalMs: 600_000,
|
|
643
|
-
});
|
|
644
|
-
const embedSpy = inner.embed;
|
|
645
|
-
// Populate and hit several texts (builds buffer without flushing)
|
|
646
|
-
for (let i = 0; i < 5; i++) {
|
|
647
|
-
embedSpy.mockResolvedValueOnce(ok(makeVector(DIMS, i + 1)));
|
|
648
|
-
await cache.embed(`dispose-text-${i}`);
|
|
649
|
-
}
|
|
650
|
-
// Hit them again (cache hits -> buffer entries)
|
|
651
|
-
for (let i = 0; i < 5; i++) {
|
|
652
|
-
await cache.embed(`dispose-text-${i}`);
|
|
653
|
-
}
|
|
654
|
-
// Before dispose: buffer is populated but NOT flushed (under threshold)
|
|
655
|
-
const beforeFlush = db.prepare("SELECT COUNT(*) as cnt FROM embedding_cache WHERE hit_count > 0").get();
|
|
656
|
-
expect(beforeFlush.cnt).toBe(0); // Not flushed yet
|
|
657
|
-
// Call dispose
|
|
658
|
-
await cache.dispose?.();
|
|
659
|
-
// After dispose: buffer should have been flushed
|
|
660
|
-
const afterFlush = db.prepare("SELECT COUNT(*) as cnt FROM embedding_cache WHERE hit_count > 0").get();
|
|
661
|
-
expect(afterFlush.cnt).toBe(5); // All 5 entries had their access flushed
|
|
662
|
-
// inner.dispose was called
|
|
663
|
-
expect(disposeSpy).toHaveBeenCalledTimes(1);
|
|
664
|
-
});
|
|
665
|
-
it("dispose forwards to inner.dispose exactly once", async () => {
|
|
666
|
-
const disposeSpy = inner.dispose;
|
|
667
|
-
const cache = createSqliteEmbeddingCache(inner, {
|
|
668
|
-
db,
|
|
669
|
-
maxEntries: 50_000,
|
|
670
|
-
pruneIntervalMs: 600_000,
|
|
671
|
-
});
|
|
672
|
-
await cache.dispose?.();
|
|
673
|
-
expect(disposeSpy).toHaveBeenCalledTimes(1);
|
|
674
|
-
// Second dispose is a no-op (double-dispose safe)
|
|
675
|
-
await cache.dispose?.();
|
|
676
|
-
expect(disposeSpy).toHaveBeenCalledTimes(1); // Still 1
|
|
677
|
-
});
|
|
678
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|