comisai 1.0.11 → 1.0.13

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