comisai 1.0.10 → 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.
Files changed (251) hide show
  1. package/README.md +3 -3
  2. package/node_modules/@comis/agent/dist/executor/pi-executor.d.ts +1 -4
  3. package/node_modules/@comis/agent/package.json +1 -1
  4. package/node_modules/@comis/channels/package.json +1 -1
  5. package/node_modules/@comis/cli/dist/wizard/steps/06-channels.js +88 -11
  6. package/node_modules/@comis/cli/dist/wizard/steps/10-write-config.js +4 -0
  7. package/node_modules/@comis/cli/dist/wizard/types.d.ts +1 -0
  8. package/node_modules/@comis/cli/package.json +1 -1
  9. package/node_modules/@comis/core/package.json +1 -1
  10. package/node_modules/@comis/daemon/package.json +1 -1
  11. package/node_modules/@comis/gateway/package.json +1 -1
  12. package/node_modules/@comis/infra/package.json +1 -1
  13. package/node_modules/@comis/memory/package.json +1 -1
  14. package/node_modules/@comis/scheduler/package.json +1 -1
  15. package/node_modules/@comis/shared/package.json +1 -1
  16. package/node_modules/@comis/skills/package.json +1 -1
  17. package/package.json +18 -18
  18. package/node_modules/@comis/core/dist/approval/approval-gate.test.d.ts +0 -1
  19. package/node_modules/@comis/core/dist/approval/approval-gate.test.js +0 -1003
  20. package/node_modules/@comis/core/dist/bootstrap.test.d.ts +0 -1
  21. package/node_modules/@comis/core/dist/bootstrap.test.js +0 -150
  22. package/node_modules/@comis/core/dist/config/backup.test.d.ts +0 -1
  23. package/node_modules/@comis/core/dist/config/backup.test.js +0 -160
  24. package/node_modules/@comis/core/dist/config/env-substitution.test.d.ts +0 -1
  25. package/node_modules/@comis/core/dist/config/env-substitution.test.js +0 -259
  26. package/node_modules/@comis/core/dist/config/field-metadata.test.d.ts +0 -1
  27. package/node_modules/@comis/core/dist/config/field-metadata.test.js +0 -72
  28. package/node_modules/@comis/core/dist/config/git-manager.test.d.ts +0 -1
  29. package/node_modules/@comis/core/dist/config/git-manager.test.js +0 -1042
  30. package/node_modules/@comis/core/dist/config/immutable-keys.test.d.ts +0 -1
  31. package/node_modules/@comis/core/dist/config/immutable-keys.test.js +0 -283
  32. package/node_modules/@comis/core/dist/config/include-resolver.test.d.ts +0 -1
  33. package/node_modules/@comis/core/dist/config/include-resolver.test.js +0 -292
  34. package/node_modules/@comis/core/dist/config/layered.test.d.ts +0 -1
  35. package/node_modules/@comis/core/dist/config/layered.test.js +0 -211
  36. package/node_modules/@comis/core/dist/config/loader.test.d.ts +0 -1
  37. package/node_modules/@comis/core/dist/config/loader.test.js +0 -300
  38. package/node_modules/@comis/core/dist/config/migrate.test.d.ts +0 -1
  39. package/node_modules/@comis/core/dist/config/migrate.test.js +0 -244
  40. package/node_modules/@comis/core/dist/config/partial-validator.test.d.ts +0 -1
  41. package/node_modules/@comis/core/dist/config/partial-validator.test.js +0 -98
  42. package/node_modules/@comis/core/dist/config/schema-agent-model.test.d.ts +0 -1
  43. package/node_modules/@comis/core/dist/config/schema-agent-model.test.js +0 -279
  44. package/node_modules/@comis/core/dist/config/schema-agent-session.test.d.ts +0 -1
  45. package/node_modules/@comis/core/dist/config/schema-agent-session.test.js +0 -242
  46. package/node_modules/@comis/core/dist/config/schema-agent.test.d.ts +0 -1
  47. package/node_modules/@comis/core/dist/config/schema-agent.test.js +0 -645
  48. package/node_modules/@comis/core/dist/config/schema-channel.test.d.ts +0 -1
  49. package/node_modules/@comis/core/dist/config/schema-channel.test.js +0 -341
  50. package/node_modules/@comis/core/dist/config/schema-coalescer.test.d.ts +0 -1
  51. package/node_modules/@comis/core/dist/config/schema-coalescer.test.js +0 -51
  52. package/node_modules/@comis/core/dist/config/schema-context-engine.test.d.ts +0 -1
  53. package/node_modules/@comis/core/dist/config/schema-context-engine.test.js +0 -797
  54. package/node_modules/@comis/core/dist/config/schema-context-guard.test.d.ts +0 -1
  55. package/node_modules/@comis/core/dist/config/schema-context-guard.test.js +0 -111
  56. package/node_modules/@comis/core/dist/config/schema-daemon.test.d.ts +0 -1
  57. package/node_modules/@comis/core/dist/config/schema-daemon.test.js +0 -107
  58. package/node_modules/@comis/core/dist/config/schema-delivery-timing.test.d.ts +0 -1
  59. package/node_modules/@comis/core/dist/config/schema-delivery-timing.test.js +0 -51
  60. package/node_modules/@comis/core/dist/config/schema-documentation.test.d.ts +0 -1
  61. package/node_modules/@comis/core/dist/config/schema-documentation.test.js +0 -63
  62. package/node_modules/@comis/core/dist/config/schema-gateway.test.d.ts +0 -1
  63. package/node_modules/@comis/core/dist/config/schema-gateway.test.js +0 -219
  64. package/node_modules/@comis/core/dist/config/schema-gemini-cache.test.d.ts +0 -1
  65. package/node_modules/@comis/core/dist/config/schema-gemini-cache.test.js +0 -41
  66. package/node_modules/@comis/core/dist/config/schema-integrations.test.d.ts +0 -1
  67. package/node_modules/@comis/core/dist/config/schema-integrations.test.js +0 -92
  68. package/node_modules/@comis/core/dist/config/schema-lifecycle-reactions.test.d.ts +0 -1
  69. package/node_modules/@comis/core/dist/config/schema-lifecycle-reactions.test.js +0 -61
  70. package/node_modules/@comis/core/dist/config/schema-misc.test.d.ts +0 -1
  71. package/node_modules/@comis/core/dist/config/schema-misc.test.js +0 -394
  72. package/node_modules/@comis/core/dist/config/schema-observability.test.d.ts +0 -1
  73. package/node_modules/@comis/core/dist/config/schema-observability.test.js +0 -76
  74. package/node_modules/@comis/core/dist/config/schema-providers.test.d.ts +0 -1
  75. package/node_modules/@comis/core/dist/config/schema-providers.test.js +0 -294
  76. package/node_modules/@comis/core/dist/config/schema-queue.test.d.ts +0 -1
  77. package/node_modules/@comis/core/dist/config/schema-queue.test.js +0 -234
  78. package/node_modules/@comis/core/dist/config/schema-response-prefix.test.d.ts +0 -1
  79. package/node_modules/@comis/core/dist/config/schema-response-prefix.test.js +0 -36
  80. package/node_modules/@comis/core/dist/config/schema-security.test.d.ts +0 -1
  81. package/node_modules/@comis/core/dist/config/schema-security.test.js +0 -54
  82. package/node_modules/@comis/core/dist/config/schema-sender-trust-display.test.d.ts +0 -1
  83. package/node_modules/@comis/core/dist/config/schema-sender-trust-display.test.js +0 -60
  84. package/node_modules/@comis/core/dist/config/schema-serializer.test.d.ts +0 -1
  85. package/node_modules/@comis/core/dist/config/schema-serializer.test.js +0 -73
  86. package/node_modules/@comis/core/dist/config/schema-telegram-file-guard.test.d.ts +0 -1
  87. package/node_modules/@comis/core/dist/config/schema-telegram-file-guard.test.js +0 -39
  88. package/node_modules/@comis/core/dist/context/context.test.d.ts +0 -1
  89. package/node_modules/@comis/core/dist/context/context.test.js +0 -276
  90. package/node_modules/@comis/core/dist/domain/agent-response.test.d.ts +0 -1
  91. package/node_modules/@comis/core/dist/domain/agent-response.test.js +0 -159
  92. package/node_modules/@comis/core/dist/domain/credential-mapping.test.d.ts +0 -1
  93. package/node_modules/@comis/core/dist/domain/credential-mapping.test.js +0 -135
  94. package/node_modules/@comis/core/dist/domain/delivery-origin.test.d.ts +0 -1
  95. package/node_modules/@comis/core/dist/domain/delivery-origin.test.js +0 -68
  96. package/node_modules/@comis/core/dist/domain/execution-graph.test.d.ts +0 -1
  97. package/node_modules/@comis/core/dist/domain/execution-graph.test.js +0 -441
  98. package/node_modules/@comis/core/dist/domain/memory-entry.test.d.ts +0 -1
  99. package/node_modules/@comis/core/dist/domain/memory-entry.test.js +0 -193
  100. package/node_modules/@comis/core/dist/domain/normalized-message.test.d.ts +0 -1
  101. package/node_modules/@comis/core/dist/domain/normalized-message.test.js +0 -255
  102. package/node_modules/@comis/core/dist/domain/session-key.test.d.ts +0 -1
  103. package/node_modules/@comis/core/dist/domain/session-key.test.js +0 -291
  104. package/node_modules/@comis/core/dist/domain/subagent-context-config.test.d.ts +0 -1
  105. package/node_modules/@comis/core/dist/domain/subagent-context-config.test.js +0 -131
  106. package/node_modules/@comis/core/dist/domain/subagent-context-types.test.d.ts +0 -1
  107. package/node_modules/@comis/core/dist/domain/subagent-context-types.test.js +0 -182
  108. package/node_modules/@comis/core/dist/event-bus/bus.test.d.ts +0 -1
  109. package/node_modules/@comis/core/dist/event-bus/bus.test.js +0 -152
  110. package/node_modules/@comis/core/dist/event-bus/events-agent.test.d.ts +0 -1
  111. package/node_modules/@comis/core/dist/event-bus/events-agent.test.js +0 -409
  112. package/node_modules/@comis/core/dist/event-bus/events-channel.test.d.ts +0 -1
  113. package/node_modules/@comis/core/dist/event-bus/events-channel.test.js +0 -240
  114. package/node_modules/@comis/core/dist/event-bus/events-infra.test.d.ts +0 -1
  115. package/node_modules/@comis/core/dist/event-bus/events-infra.test.js +0 -416
  116. package/node_modules/@comis/core/dist/event-bus/events-messaging.test.d.ts +0 -1
  117. package/node_modules/@comis/core/dist/event-bus/events-messaging.test.js +0 -433
  118. package/node_modules/@comis/core/dist/event-bus/events.test.d.ts +0 -1
  119. package/node_modules/@comis/core/dist/event-bus/events.test.js +0 -93
  120. package/node_modules/@comis/core/dist/hooks/hook-runner-global.test.d.ts +0 -1
  121. package/node_modules/@comis/core/dist/hooks/hook-runner-global.test.js +0 -29
  122. package/node_modules/@comis/core/dist/hooks/hook-runner.test.d.ts +0 -1
  123. package/node_modules/@comis/core/dist/hooks/hook-runner.test.js +0 -490
  124. package/node_modules/@comis/core/dist/hooks/hook-strategies.test.d.ts +0 -1
  125. package/node_modules/@comis/core/dist/hooks/hook-strategies.test.js +0 -209
  126. package/node_modules/@comis/core/dist/hooks/integration.test.d.ts +0 -1
  127. package/node_modules/@comis/core/dist/hooks/integration.test.js +0 -235
  128. package/node_modules/@comis/core/dist/hooks/plugin-registry.test.d.ts +0 -1
  129. package/node_modules/@comis/core/dist/hooks/plugin-registry.test.js +0 -470
  130. package/node_modules/@comis/core/dist/load-env.test.d.ts +0 -1
  131. package/node_modules/@comis/core/dist/load-env.test.js +0 -21
  132. package/node_modules/@comis/core/dist/security/action-classifier.test.d.ts +0 -1
  133. package/node_modules/@comis/core/dist/security/action-classifier.test.js +0 -298
  134. package/node_modules/@comis/core/dist/security/audit-aggregator.test.d.ts +0 -1
  135. package/node_modules/@comis/core/dist/security/audit-aggregator.test.js +0 -128
  136. package/node_modules/@comis/core/dist/security/audit.test.d.ts +0 -1
  137. package/node_modules/@comis/core/dist/security/audit.test.js +0 -154
  138. package/node_modules/@comis/core/dist/security/canary-token.test.d.ts +0 -1
  139. package/node_modules/@comis/core/dist/security/canary-token.test.js +0 -45
  140. package/node_modules/@comis/core/dist/security/config-redaction.test.d.ts +0 -1
  141. package/node_modules/@comis/core/dist/security/config-redaction.test.js +0 -107
  142. package/node_modules/@comis/core/dist/security/external-content.test.d.ts +0 -1
  143. package/node_modules/@comis/core/dist/security/external-content.test.js +0 -210
  144. package/node_modules/@comis/core/dist/security/injection-patterns.test.d.ts +0 -1
  145. package/node_modules/@comis/core/dist/security/injection-patterns.test.js +0 -213
  146. package/node_modules/@comis/core/dist/security/injection-rate-limiter.test.d.ts +0 -1
  147. package/node_modules/@comis/core/dist/security/injection-rate-limiter.test.js +0 -194
  148. package/node_modules/@comis/core/dist/security/input-guard.test.d.ts +0 -1
  149. package/node_modules/@comis/core/dist/security/input-guard.test.js +0 -215
  150. package/node_modules/@comis/core/dist/security/input-security-guard.test.d.ts +0 -1
  151. package/node_modules/@comis/core/dist/security/input-security-guard.test.js +0 -215
  152. package/node_modules/@comis/core/dist/security/input-validator.test.d.ts +0 -1
  153. package/node_modules/@comis/core/dist/security/input-validator.test.js +0 -133
  154. package/node_modules/@comis/core/dist/security/log-sanitizer.test.d.ts +0 -1
  155. package/node_modules/@comis/core/dist/security/log-sanitizer.test.js +0 -260
  156. package/node_modules/@comis/core/dist/security/memory-write-validator.test.d.ts +0 -1
  157. package/node_modules/@comis/core/dist/security/memory-write-validator.test.js +0 -62
  158. package/node_modules/@comis/core/dist/security/output-guard.test.d.ts +0 -1
  159. package/node_modules/@comis/core/dist/security/output-guard.test.js +0 -397
  160. package/node_modules/@comis/core/dist/security/safe-path.test.d.ts +0 -1
  161. package/node_modules/@comis/core/dist/security/safe-path.test.js +0 -95
  162. package/node_modules/@comis/core/dist/security/scoped-secret-manager.test.d.ts +0 -1
  163. package/node_modules/@comis/core/dist/security/scoped-secret-manager.test.js +0 -231
  164. package/node_modules/@comis/core/dist/security/secret-access.test.d.ts +0 -1
  165. package/node_modules/@comis/core/dist/security/secret-access.test.js +0 -41
  166. package/node_modules/@comis/core/dist/security/secret-crypto.test.d.ts +0 -1
  167. package/node_modules/@comis/core/dist/security/secret-crypto.test.js +0 -113
  168. package/node_modules/@comis/core/dist/security/secret-manager.test.d.ts +0 -1
  169. package/node_modules/@comis/core/dist/security/secret-manager.test.js +0 -394
  170. package/node_modules/@comis/core/dist/security/secret-ref-resolver.test.d.ts +0 -1
  171. package/node_modules/@comis/core/dist/security/secret-ref-resolver.test.js +0 -268
  172. package/node_modules/@comis/core/dist/security/secrets-audit.test.d.ts +0 -10
  173. package/node_modules/@comis/core/dist/security/secrets-audit.test.js +0 -295
  174. package/node_modules/@comis/core/dist/security/ssrf-guard.test.d.ts +0 -1
  175. package/node_modules/@comis/core/dist/security/ssrf-guard.test.js +0 -127
  176. package/node_modules/@comis/core/dist/security/token-generator.test.d.ts +0 -1
  177. package/node_modules/@comis/core/dist/security/token-generator.test.js +0 -35
  178. package/node_modules/@comis/core/dist/tool-metadata.test.d.ts +0 -1
  179. package/node_modules/@comis/core/dist/tool-metadata.test.js +0 -112
  180. package/node_modules/@comis/memory/dist/compaction.test.d.ts +0 -1
  181. package/node_modules/@comis/memory/dist/compaction.test.js +0 -298
  182. package/node_modules/@comis/memory/dist/context-schema.test.d.ts +0 -1
  183. package/node_modules/@comis/memory/dist/context-schema.test.js +0 -246
  184. package/node_modules/@comis/memory/dist/context-store.test.d.ts +0 -1
  185. package/node_modules/@comis/memory/dist/context-store.test.js +0 -708
  186. package/node_modules/@comis/memory/dist/credential-mapping-schema.test.d.ts +0 -7
  187. package/node_modules/@comis/memory/dist/credential-mapping-schema.test.js +0 -73
  188. package/node_modules/@comis/memory/dist/credential-mapping-store.test.d.ts +0 -8
  189. package/node_modules/@comis/memory/dist/credential-mapping-store.test.js +0 -340
  190. package/node_modules/@comis/memory/dist/delivery-mirror-adapter.test.d.ts +0 -1
  191. package/node_modules/@comis/memory/dist/delivery-mirror-adapter.test.js +0 -165
  192. package/node_modules/@comis/memory/dist/delivery-queue-adapter.test.d.ts +0 -1
  193. package/node_modules/@comis/memory/dist/delivery-queue-adapter.test.js +0 -263
  194. package/node_modules/@comis/memory/dist/embedding-batch-indexer.test.d.ts +0 -1
  195. package/node_modules/@comis/memory/dist/embedding-batch-indexer.test.js +0 -202
  196. package/node_modules/@comis/memory/dist/embedding-cache-lru.test.d.ts +0 -1
  197. package/node_modules/@comis/memory/dist/embedding-cache-lru.test.js +0 -241
  198. package/node_modules/@comis/memory/dist/embedding-cache-sqlite.test.d.ts +0 -8
  199. package/node_modules/@comis/memory/dist/embedding-cache-sqlite.test.js +0 -678
  200. package/node_modules/@comis/memory/dist/embedding-cache.test.d.ts +0 -1
  201. package/node_modules/@comis/memory/dist/embedding-cache.test.js +0 -213
  202. package/node_modules/@comis/memory/dist/embedding-fingerprint.test.d.ts +0 -1
  203. package/node_modules/@comis/memory/dist/embedding-fingerprint.test.js +0 -87
  204. package/node_modules/@comis/memory/dist/embedding-hash.test.d.ts +0 -1
  205. package/node_modules/@comis/memory/dist/embedding-hash.test.js +0 -31
  206. package/node_modules/@comis/memory/dist/embedding-integration.test.d.ts +0 -8
  207. package/node_modules/@comis/memory/dist/embedding-integration.test.js +0 -232
  208. package/node_modules/@comis/memory/dist/embedding-provider-factory.test.d.ts +0 -1
  209. package/node_modules/@comis/memory/dist/embedding-provider-factory.test.js +0 -176
  210. package/node_modules/@comis/memory/dist/embedding-provider-local.test.d.ts +0 -8
  211. package/node_modules/@comis/memory/dist/embedding-provider-local.test.js +0 -76
  212. package/node_modules/@comis/memory/dist/embedding-provider-openai.test.d.ts +0 -8
  213. package/node_modules/@comis/memory/dist/embedding-provider-openai.test.js +0 -100
  214. package/node_modules/@comis/memory/dist/embedding-queue.test.d.ts +0 -1
  215. package/node_modules/@comis/memory/dist/embedding-queue.test.js +0 -236
  216. package/node_modules/@comis/memory/dist/hybrid-search.test.d.ts +0 -1
  217. package/node_modules/@comis/memory/dist/hybrid-search.test.js +0 -476
  218. package/node_modules/@comis/memory/dist/identity-link-store.test.d.ts +0 -1
  219. package/node_modules/@comis/memory/dist/identity-link-store.test.js +0 -88
  220. package/node_modules/@comis/memory/dist/memory-api.test.d.ts +0 -1
  221. package/node_modules/@comis/memory/dist/memory-api.test.js +0 -495
  222. package/node_modules/@comis/memory/dist/memory-concurrency.test.d.ts +0 -20
  223. package/node_modules/@comis/memory/dist/memory-concurrency.test.js +0 -222
  224. package/node_modules/@comis/memory/dist/named-graph-store.test.d.ts +0 -1
  225. package/node_modules/@comis/memory/dist/named-graph-store.test.js +0 -227
  226. package/node_modules/@comis/memory/dist/observability-store.test.d.ts +0 -1
  227. package/node_modules/@comis/memory/dist/observability-store.test.js +0 -704
  228. package/node_modules/@comis/memory/dist/row-mapper.test.d.ts +0 -1
  229. package/node_modules/@comis/memory/dist/row-mapper.test.js +0 -409
  230. package/node_modules/@comis/memory/dist/schema.test.d.ts +0 -1
  231. package/node_modules/@comis/memory/dist/schema.test.js +0 -240
  232. package/node_modules/@comis/memory/dist/secret-store-schema.test.d.ts +0 -7
  233. package/node_modules/@comis/memory/dist/secret-store-schema.test.js +0 -90
  234. package/node_modules/@comis/memory/dist/session-store.test.d.ts +0 -1
  235. package/node_modules/@comis/memory/dist/session-store.test.js +0 -262
  236. package/node_modules/@comis/memory/dist/setup-secrets.test.d.ts +0 -1
  237. package/node_modules/@comis/memory/dist/setup-secrets.test.js +0 -140
  238. package/node_modules/@comis/memory/dist/sqlite-memory-adapter.test.d.ts +0 -1
  239. package/node_modules/@comis/memory/dist/sqlite-memory-adapter.test.js +0 -888
  240. package/node_modules/@comis/memory/dist/sqlite-secret-store.test.d.ts +0 -8
  241. package/node_modules/@comis/memory/dist/sqlite-secret-store.test.js +0 -245
  242. package/node_modules/@comis/shared/dist/abort.test.d.ts +0 -1
  243. package/node_modules/@comis/shared/dist/abort.test.js +0 -71
  244. package/node_modules/@comis/shared/dist/result.test.d.ts +0 -1
  245. package/node_modules/@comis/shared/dist/result.test.js +0 -178
  246. package/node_modules/@comis/shared/dist/suppress-error.test.d.ts +0 -1
  247. package/node_modules/@comis/shared/dist/suppress-error.test.js +0 -50
  248. package/node_modules/@comis/shared/dist/timeout.test.d.ts +0 -1
  249. package/node_modules/@comis/shared/dist/timeout.test.js +0 -75
  250. package/node_modules/@comis/shared/dist/ttl-cache.test.d.ts +0 -1
  251. 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
- });