comisai 1.0.11 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/node_modules/@comis/agent/dist/executor/pi-executor.d.ts +1 -4
  2. package/node_modules/@comis/agent/package.json +1 -1
  3. package/node_modules/@comis/channels/package.json +1 -1
  4. package/node_modules/@comis/cli/package.json +1 -1
  5. package/node_modules/@comis/core/package.json +1 -1
  6. package/node_modules/@comis/daemon/package.json +1 -1
  7. package/node_modules/@comis/gateway/package.json +1 -1
  8. package/node_modules/@comis/infra/package.json +1 -1
  9. package/node_modules/@comis/memory/package.json +1 -1
  10. package/node_modules/@comis/scheduler/package.json +1 -1
  11. package/node_modules/@comis/shared/package.json +1 -1
  12. package/node_modules/@comis/skills/package.json +1 -1
  13. package/package.json +18 -18
  14. package/node_modules/@comis/core/dist/approval/approval-gate.test.d.ts +0 -1
  15. package/node_modules/@comis/core/dist/approval/approval-gate.test.js +0 -1003
  16. package/node_modules/@comis/core/dist/bootstrap.test.d.ts +0 -1
  17. package/node_modules/@comis/core/dist/bootstrap.test.js +0 -150
  18. package/node_modules/@comis/core/dist/config/backup.test.d.ts +0 -1
  19. package/node_modules/@comis/core/dist/config/backup.test.js +0 -160
  20. package/node_modules/@comis/core/dist/config/env-substitution.test.d.ts +0 -1
  21. package/node_modules/@comis/core/dist/config/env-substitution.test.js +0 -259
  22. package/node_modules/@comis/core/dist/config/field-metadata.test.d.ts +0 -1
  23. package/node_modules/@comis/core/dist/config/field-metadata.test.js +0 -72
  24. package/node_modules/@comis/core/dist/config/git-manager.test.d.ts +0 -1
  25. package/node_modules/@comis/core/dist/config/git-manager.test.js +0 -1042
  26. package/node_modules/@comis/core/dist/config/immutable-keys.test.d.ts +0 -1
  27. package/node_modules/@comis/core/dist/config/immutable-keys.test.js +0 -283
  28. package/node_modules/@comis/core/dist/config/include-resolver.test.d.ts +0 -1
  29. package/node_modules/@comis/core/dist/config/include-resolver.test.js +0 -292
  30. package/node_modules/@comis/core/dist/config/layered.test.d.ts +0 -1
  31. package/node_modules/@comis/core/dist/config/layered.test.js +0 -211
  32. package/node_modules/@comis/core/dist/config/loader.test.d.ts +0 -1
  33. package/node_modules/@comis/core/dist/config/loader.test.js +0 -300
  34. package/node_modules/@comis/core/dist/config/migrate.test.d.ts +0 -1
  35. package/node_modules/@comis/core/dist/config/migrate.test.js +0 -244
  36. package/node_modules/@comis/core/dist/config/partial-validator.test.d.ts +0 -1
  37. package/node_modules/@comis/core/dist/config/partial-validator.test.js +0 -98
  38. package/node_modules/@comis/core/dist/config/schema-agent-model.test.d.ts +0 -1
  39. package/node_modules/@comis/core/dist/config/schema-agent-model.test.js +0 -279
  40. package/node_modules/@comis/core/dist/config/schema-agent-session.test.d.ts +0 -1
  41. package/node_modules/@comis/core/dist/config/schema-agent-session.test.js +0 -242
  42. package/node_modules/@comis/core/dist/config/schema-agent.test.d.ts +0 -1
  43. package/node_modules/@comis/core/dist/config/schema-agent.test.js +0 -645
  44. package/node_modules/@comis/core/dist/config/schema-channel.test.d.ts +0 -1
  45. package/node_modules/@comis/core/dist/config/schema-channel.test.js +0 -341
  46. package/node_modules/@comis/core/dist/config/schema-coalescer.test.d.ts +0 -1
  47. package/node_modules/@comis/core/dist/config/schema-coalescer.test.js +0 -51
  48. package/node_modules/@comis/core/dist/config/schema-context-engine.test.d.ts +0 -1
  49. package/node_modules/@comis/core/dist/config/schema-context-engine.test.js +0 -797
  50. package/node_modules/@comis/core/dist/config/schema-context-guard.test.d.ts +0 -1
  51. package/node_modules/@comis/core/dist/config/schema-context-guard.test.js +0 -111
  52. package/node_modules/@comis/core/dist/config/schema-daemon.test.d.ts +0 -1
  53. package/node_modules/@comis/core/dist/config/schema-daemon.test.js +0 -107
  54. package/node_modules/@comis/core/dist/config/schema-delivery-timing.test.d.ts +0 -1
  55. package/node_modules/@comis/core/dist/config/schema-delivery-timing.test.js +0 -51
  56. package/node_modules/@comis/core/dist/config/schema-documentation.test.d.ts +0 -1
  57. package/node_modules/@comis/core/dist/config/schema-documentation.test.js +0 -63
  58. package/node_modules/@comis/core/dist/config/schema-gateway.test.d.ts +0 -1
  59. package/node_modules/@comis/core/dist/config/schema-gateway.test.js +0 -219
  60. package/node_modules/@comis/core/dist/config/schema-gemini-cache.test.d.ts +0 -1
  61. package/node_modules/@comis/core/dist/config/schema-gemini-cache.test.js +0 -41
  62. package/node_modules/@comis/core/dist/config/schema-integrations.test.d.ts +0 -1
  63. package/node_modules/@comis/core/dist/config/schema-integrations.test.js +0 -92
  64. package/node_modules/@comis/core/dist/config/schema-lifecycle-reactions.test.d.ts +0 -1
  65. package/node_modules/@comis/core/dist/config/schema-lifecycle-reactions.test.js +0 -61
  66. package/node_modules/@comis/core/dist/config/schema-misc.test.d.ts +0 -1
  67. package/node_modules/@comis/core/dist/config/schema-misc.test.js +0 -394
  68. package/node_modules/@comis/core/dist/config/schema-observability.test.d.ts +0 -1
  69. package/node_modules/@comis/core/dist/config/schema-observability.test.js +0 -76
  70. package/node_modules/@comis/core/dist/config/schema-providers.test.d.ts +0 -1
  71. package/node_modules/@comis/core/dist/config/schema-providers.test.js +0 -294
  72. package/node_modules/@comis/core/dist/config/schema-queue.test.d.ts +0 -1
  73. package/node_modules/@comis/core/dist/config/schema-queue.test.js +0 -234
  74. package/node_modules/@comis/core/dist/config/schema-response-prefix.test.d.ts +0 -1
  75. package/node_modules/@comis/core/dist/config/schema-response-prefix.test.js +0 -36
  76. package/node_modules/@comis/core/dist/config/schema-security.test.d.ts +0 -1
  77. package/node_modules/@comis/core/dist/config/schema-security.test.js +0 -54
  78. package/node_modules/@comis/core/dist/config/schema-sender-trust-display.test.d.ts +0 -1
  79. package/node_modules/@comis/core/dist/config/schema-sender-trust-display.test.js +0 -60
  80. package/node_modules/@comis/core/dist/config/schema-serializer.test.d.ts +0 -1
  81. package/node_modules/@comis/core/dist/config/schema-serializer.test.js +0 -73
  82. package/node_modules/@comis/core/dist/config/schema-telegram-file-guard.test.d.ts +0 -1
  83. package/node_modules/@comis/core/dist/config/schema-telegram-file-guard.test.js +0 -39
  84. package/node_modules/@comis/core/dist/context/context.test.d.ts +0 -1
  85. package/node_modules/@comis/core/dist/context/context.test.js +0 -276
  86. package/node_modules/@comis/core/dist/domain/agent-response.test.d.ts +0 -1
  87. package/node_modules/@comis/core/dist/domain/agent-response.test.js +0 -159
  88. package/node_modules/@comis/core/dist/domain/credential-mapping.test.d.ts +0 -1
  89. package/node_modules/@comis/core/dist/domain/credential-mapping.test.js +0 -135
  90. package/node_modules/@comis/core/dist/domain/delivery-origin.test.d.ts +0 -1
  91. package/node_modules/@comis/core/dist/domain/delivery-origin.test.js +0 -68
  92. package/node_modules/@comis/core/dist/domain/execution-graph.test.d.ts +0 -1
  93. package/node_modules/@comis/core/dist/domain/execution-graph.test.js +0 -441
  94. package/node_modules/@comis/core/dist/domain/memory-entry.test.d.ts +0 -1
  95. package/node_modules/@comis/core/dist/domain/memory-entry.test.js +0 -193
  96. package/node_modules/@comis/core/dist/domain/normalized-message.test.d.ts +0 -1
  97. package/node_modules/@comis/core/dist/domain/normalized-message.test.js +0 -255
  98. package/node_modules/@comis/core/dist/domain/session-key.test.d.ts +0 -1
  99. package/node_modules/@comis/core/dist/domain/session-key.test.js +0 -291
  100. package/node_modules/@comis/core/dist/domain/subagent-context-config.test.d.ts +0 -1
  101. package/node_modules/@comis/core/dist/domain/subagent-context-config.test.js +0 -131
  102. package/node_modules/@comis/core/dist/domain/subagent-context-types.test.d.ts +0 -1
  103. package/node_modules/@comis/core/dist/domain/subagent-context-types.test.js +0 -182
  104. package/node_modules/@comis/core/dist/event-bus/bus.test.d.ts +0 -1
  105. package/node_modules/@comis/core/dist/event-bus/bus.test.js +0 -152
  106. package/node_modules/@comis/core/dist/event-bus/events-agent.test.d.ts +0 -1
  107. package/node_modules/@comis/core/dist/event-bus/events-agent.test.js +0 -409
  108. package/node_modules/@comis/core/dist/event-bus/events-channel.test.d.ts +0 -1
  109. package/node_modules/@comis/core/dist/event-bus/events-channel.test.js +0 -240
  110. package/node_modules/@comis/core/dist/event-bus/events-infra.test.d.ts +0 -1
  111. package/node_modules/@comis/core/dist/event-bus/events-infra.test.js +0 -416
  112. package/node_modules/@comis/core/dist/event-bus/events-messaging.test.d.ts +0 -1
  113. package/node_modules/@comis/core/dist/event-bus/events-messaging.test.js +0 -433
  114. package/node_modules/@comis/core/dist/event-bus/events.test.d.ts +0 -1
  115. package/node_modules/@comis/core/dist/event-bus/events.test.js +0 -93
  116. package/node_modules/@comis/core/dist/hooks/hook-runner-global.test.d.ts +0 -1
  117. package/node_modules/@comis/core/dist/hooks/hook-runner-global.test.js +0 -29
  118. package/node_modules/@comis/core/dist/hooks/hook-runner.test.d.ts +0 -1
  119. package/node_modules/@comis/core/dist/hooks/hook-runner.test.js +0 -490
  120. package/node_modules/@comis/core/dist/hooks/hook-strategies.test.d.ts +0 -1
  121. package/node_modules/@comis/core/dist/hooks/hook-strategies.test.js +0 -209
  122. package/node_modules/@comis/core/dist/hooks/integration.test.d.ts +0 -1
  123. package/node_modules/@comis/core/dist/hooks/integration.test.js +0 -235
  124. package/node_modules/@comis/core/dist/hooks/plugin-registry.test.d.ts +0 -1
  125. package/node_modules/@comis/core/dist/hooks/plugin-registry.test.js +0 -470
  126. package/node_modules/@comis/core/dist/load-env.test.d.ts +0 -1
  127. package/node_modules/@comis/core/dist/load-env.test.js +0 -21
  128. package/node_modules/@comis/core/dist/security/action-classifier.test.d.ts +0 -1
  129. package/node_modules/@comis/core/dist/security/action-classifier.test.js +0 -298
  130. package/node_modules/@comis/core/dist/security/audit-aggregator.test.d.ts +0 -1
  131. package/node_modules/@comis/core/dist/security/audit-aggregator.test.js +0 -128
  132. package/node_modules/@comis/core/dist/security/audit.test.d.ts +0 -1
  133. package/node_modules/@comis/core/dist/security/audit.test.js +0 -154
  134. package/node_modules/@comis/core/dist/security/canary-token.test.d.ts +0 -1
  135. package/node_modules/@comis/core/dist/security/canary-token.test.js +0 -45
  136. package/node_modules/@comis/core/dist/security/config-redaction.test.d.ts +0 -1
  137. package/node_modules/@comis/core/dist/security/config-redaction.test.js +0 -107
  138. package/node_modules/@comis/core/dist/security/external-content.test.d.ts +0 -1
  139. package/node_modules/@comis/core/dist/security/external-content.test.js +0 -210
  140. package/node_modules/@comis/core/dist/security/injection-patterns.test.d.ts +0 -1
  141. package/node_modules/@comis/core/dist/security/injection-patterns.test.js +0 -213
  142. package/node_modules/@comis/core/dist/security/injection-rate-limiter.test.d.ts +0 -1
  143. package/node_modules/@comis/core/dist/security/injection-rate-limiter.test.js +0 -194
  144. package/node_modules/@comis/core/dist/security/input-guard.test.d.ts +0 -1
  145. package/node_modules/@comis/core/dist/security/input-guard.test.js +0 -215
  146. package/node_modules/@comis/core/dist/security/input-security-guard.test.d.ts +0 -1
  147. package/node_modules/@comis/core/dist/security/input-security-guard.test.js +0 -215
  148. package/node_modules/@comis/core/dist/security/input-validator.test.d.ts +0 -1
  149. package/node_modules/@comis/core/dist/security/input-validator.test.js +0 -133
  150. package/node_modules/@comis/core/dist/security/log-sanitizer.test.d.ts +0 -1
  151. package/node_modules/@comis/core/dist/security/log-sanitizer.test.js +0 -260
  152. package/node_modules/@comis/core/dist/security/memory-write-validator.test.d.ts +0 -1
  153. package/node_modules/@comis/core/dist/security/memory-write-validator.test.js +0 -62
  154. package/node_modules/@comis/core/dist/security/output-guard.test.d.ts +0 -1
  155. package/node_modules/@comis/core/dist/security/output-guard.test.js +0 -397
  156. package/node_modules/@comis/core/dist/security/safe-path.test.d.ts +0 -1
  157. package/node_modules/@comis/core/dist/security/safe-path.test.js +0 -95
  158. package/node_modules/@comis/core/dist/security/scoped-secret-manager.test.d.ts +0 -1
  159. package/node_modules/@comis/core/dist/security/scoped-secret-manager.test.js +0 -231
  160. package/node_modules/@comis/core/dist/security/secret-access.test.d.ts +0 -1
  161. package/node_modules/@comis/core/dist/security/secret-access.test.js +0 -41
  162. package/node_modules/@comis/core/dist/security/secret-crypto.test.d.ts +0 -1
  163. package/node_modules/@comis/core/dist/security/secret-crypto.test.js +0 -113
  164. package/node_modules/@comis/core/dist/security/secret-manager.test.d.ts +0 -1
  165. package/node_modules/@comis/core/dist/security/secret-manager.test.js +0 -394
  166. package/node_modules/@comis/core/dist/security/secret-ref-resolver.test.d.ts +0 -1
  167. package/node_modules/@comis/core/dist/security/secret-ref-resolver.test.js +0 -268
  168. package/node_modules/@comis/core/dist/security/secrets-audit.test.d.ts +0 -10
  169. package/node_modules/@comis/core/dist/security/secrets-audit.test.js +0 -295
  170. package/node_modules/@comis/core/dist/security/ssrf-guard.test.d.ts +0 -1
  171. package/node_modules/@comis/core/dist/security/ssrf-guard.test.js +0 -127
  172. package/node_modules/@comis/core/dist/security/token-generator.test.d.ts +0 -1
  173. package/node_modules/@comis/core/dist/security/token-generator.test.js +0 -35
  174. package/node_modules/@comis/core/dist/tool-metadata.test.d.ts +0 -1
  175. package/node_modules/@comis/core/dist/tool-metadata.test.js +0 -112
  176. package/node_modules/@comis/memory/dist/compaction.test.d.ts +0 -1
  177. package/node_modules/@comis/memory/dist/compaction.test.js +0 -298
  178. package/node_modules/@comis/memory/dist/context-schema.test.d.ts +0 -1
  179. package/node_modules/@comis/memory/dist/context-schema.test.js +0 -246
  180. package/node_modules/@comis/memory/dist/context-store.test.d.ts +0 -1
  181. package/node_modules/@comis/memory/dist/context-store.test.js +0 -708
  182. package/node_modules/@comis/memory/dist/credential-mapping-schema.test.d.ts +0 -7
  183. package/node_modules/@comis/memory/dist/credential-mapping-schema.test.js +0 -73
  184. package/node_modules/@comis/memory/dist/credential-mapping-store.test.d.ts +0 -8
  185. package/node_modules/@comis/memory/dist/credential-mapping-store.test.js +0 -340
  186. package/node_modules/@comis/memory/dist/delivery-mirror-adapter.test.d.ts +0 -1
  187. package/node_modules/@comis/memory/dist/delivery-mirror-adapter.test.js +0 -165
  188. package/node_modules/@comis/memory/dist/delivery-queue-adapter.test.d.ts +0 -1
  189. package/node_modules/@comis/memory/dist/delivery-queue-adapter.test.js +0 -263
  190. package/node_modules/@comis/memory/dist/embedding-batch-indexer.test.d.ts +0 -1
  191. package/node_modules/@comis/memory/dist/embedding-batch-indexer.test.js +0 -202
  192. package/node_modules/@comis/memory/dist/embedding-cache-lru.test.d.ts +0 -1
  193. package/node_modules/@comis/memory/dist/embedding-cache-lru.test.js +0 -241
  194. package/node_modules/@comis/memory/dist/embedding-cache-sqlite.test.d.ts +0 -8
  195. package/node_modules/@comis/memory/dist/embedding-cache-sqlite.test.js +0 -678
  196. package/node_modules/@comis/memory/dist/embedding-cache.test.d.ts +0 -1
  197. package/node_modules/@comis/memory/dist/embedding-cache.test.js +0 -213
  198. package/node_modules/@comis/memory/dist/embedding-fingerprint.test.d.ts +0 -1
  199. package/node_modules/@comis/memory/dist/embedding-fingerprint.test.js +0 -87
  200. package/node_modules/@comis/memory/dist/embedding-hash.test.d.ts +0 -1
  201. package/node_modules/@comis/memory/dist/embedding-hash.test.js +0 -31
  202. package/node_modules/@comis/memory/dist/embedding-integration.test.d.ts +0 -8
  203. package/node_modules/@comis/memory/dist/embedding-integration.test.js +0 -232
  204. package/node_modules/@comis/memory/dist/embedding-provider-factory.test.d.ts +0 -1
  205. package/node_modules/@comis/memory/dist/embedding-provider-factory.test.js +0 -176
  206. package/node_modules/@comis/memory/dist/embedding-provider-local.test.d.ts +0 -8
  207. package/node_modules/@comis/memory/dist/embedding-provider-local.test.js +0 -76
  208. package/node_modules/@comis/memory/dist/embedding-provider-openai.test.d.ts +0 -8
  209. package/node_modules/@comis/memory/dist/embedding-provider-openai.test.js +0 -100
  210. package/node_modules/@comis/memory/dist/embedding-queue.test.d.ts +0 -1
  211. package/node_modules/@comis/memory/dist/embedding-queue.test.js +0 -236
  212. package/node_modules/@comis/memory/dist/hybrid-search.test.d.ts +0 -1
  213. package/node_modules/@comis/memory/dist/hybrid-search.test.js +0 -476
  214. package/node_modules/@comis/memory/dist/identity-link-store.test.d.ts +0 -1
  215. package/node_modules/@comis/memory/dist/identity-link-store.test.js +0 -88
  216. package/node_modules/@comis/memory/dist/memory-api.test.d.ts +0 -1
  217. package/node_modules/@comis/memory/dist/memory-api.test.js +0 -495
  218. package/node_modules/@comis/memory/dist/memory-concurrency.test.d.ts +0 -20
  219. package/node_modules/@comis/memory/dist/memory-concurrency.test.js +0 -222
  220. package/node_modules/@comis/memory/dist/named-graph-store.test.d.ts +0 -1
  221. package/node_modules/@comis/memory/dist/named-graph-store.test.js +0 -227
  222. package/node_modules/@comis/memory/dist/observability-store.test.d.ts +0 -1
  223. package/node_modules/@comis/memory/dist/observability-store.test.js +0 -704
  224. package/node_modules/@comis/memory/dist/row-mapper.test.d.ts +0 -1
  225. package/node_modules/@comis/memory/dist/row-mapper.test.js +0 -409
  226. package/node_modules/@comis/memory/dist/schema.test.d.ts +0 -1
  227. package/node_modules/@comis/memory/dist/schema.test.js +0 -240
  228. package/node_modules/@comis/memory/dist/secret-store-schema.test.d.ts +0 -7
  229. package/node_modules/@comis/memory/dist/secret-store-schema.test.js +0 -90
  230. package/node_modules/@comis/memory/dist/session-store.test.d.ts +0 -1
  231. package/node_modules/@comis/memory/dist/session-store.test.js +0 -262
  232. package/node_modules/@comis/memory/dist/setup-secrets.test.d.ts +0 -1
  233. package/node_modules/@comis/memory/dist/setup-secrets.test.js +0 -140
  234. package/node_modules/@comis/memory/dist/sqlite-memory-adapter.test.d.ts +0 -1
  235. package/node_modules/@comis/memory/dist/sqlite-memory-adapter.test.js +0 -888
  236. package/node_modules/@comis/memory/dist/sqlite-secret-store.test.d.ts +0 -8
  237. package/node_modules/@comis/memory/dist/sqlite-secret-store.test.js +0 -245
  238. package/node_modules/@comis/shared/dist/abort.test.d.ts +0 -1
  239. package/node_modules/@comis/shared/dist/abort.test.js +0 -71
  240. package/node_modules/@comis/shared/dist/result.test.d.ts +0 -1
  241. package/node_modules/@comis/shared/dist/result.test.js +0 -178
  242. package/node_modules/@comis/shared/dist/suppress-error.test.d.ts +0 -1
  243. package/node_modules/@comis/shared/dist/suppress-error.test.js +0 -50
  244. package/node_modules/@comis/shared/dist/timeout.test.d.ts +0 -1
  245. package/node_modules/@comis/shared/dist/timeout.test.js +0 -75
  246. package/node_modules/@comis/shared/dist/ttl-cache.test.d.ts +0 -1
  247. package/node_modules/@comis/shared/dist/ttl-cache.test.js +0 -81
@@ -1,476 +0,0 @@
1
- import Database from "better-sqlite3";
2
- import { describe, it, expect, beforeEach } from "vitest";
3
- import { buildFtsQuery, searchByText, searchByVector, computeRRF, hybridSearch, } from "./hybrid-search.js";
4
- import { initSchema, isVecAvailable } from "./schema.js";
5
- /** Helper to insert a memory row with minimal boilerplate. */
6
- function insertMemory(db, id, content, opts) {
7
- db.prepare(`INSERT INTO memories (id, tenant_id, user_id, content, trust_level, memory_type, source_who, tags, created_at)
8
- VALUES (?, ?, ?, ?, ?, ?, 'agent', '[]', ?)`).run(id, opts?.tenantId ?? "default", opts?.userId ?? "u1", content, opts?.trustLevel ?? "learned", opts?.memoryType ?? "semantic", Date.now());
9
- }
10
- /** Helper to insert a vector embedding for a memory. */
11
- function insertEmbedding(db, memoryId, embedding) {
12
- const float32 = new Float32Array(embedding);
13
- db.prepare("INSERT INTO vec_memories(memory_id, embedding) VALUES (?, ?)").run(memoryId, float32);
14
- db.prepare("UPDATE memories SET has_embedding = 1 WHERE id = ?").run(memoryId);
15
- }
16
- // ── buildFtsQuery ────────────────────────────────────────────────────
17
- describe("buildFtsQuery", () => {
18
- it("produces quoted OR-joined query from normal text", () => {
19
- const result = buildFtsQuery("dentist appointment tomorrow");
20
- expect(result).toBe('"dentist" OR "appointment" OR "tomorrow"');
21
- });
22
- it("returns null for empty string", () => {
23
- expect(buildFtsQuery("")).toBeNull();
24
- });
25
- it("returns null for whitespace-only input", () => {
26
- expect(buildFtsQuery(" ")).toBeNull();
27
- });
28
- it("strips special characters, keeps only alphanumeric tokens", () => {
29
- const result = buildFtsQuery("hello! @world #2024");
30
- expect(result).toBe('"hello" OR "world" OR "2024"');
31
- });
32
- it("removes double quotes to prevent FTS5 injection", () => {
33
- const result = buildFtsQuery('test "injected phrase" query');
34
- // Double quotes removed, words tokenized individually
35
- expect(result).toBe('"test" OR "injected" OR "phrase" OR "query"');
36
- });
37
- it("returns null for input with only special characters", () => {
38
- expect(buildFtsQuery("!@#$%^&*()")).toBeNull();
39
- });
40
- it("handles single word input", () => {
41
- expect(buildFtsQuery("hello")).toBe('"hello"');
42
- });
43
- it("filters out English stop words", () => {
44
- const result = buildFtsQuery("what have we built together");
45
- expect(result).toBe('"built" OR "together"');
46
- });
47
- it("returns null when all tokens are stop words", () => {
48
- expect(buildFtsQuery("what is the")).toBeNull();
49
- });
50
- it("stop word filtering is case-insensitive", () => {
51
- const result = buildFtsQuery("The Quick Fox");
52
- expect(result).toBe('"Quick" OR "Fox"');
53
- });
54
- it("preserves non-Latin tokens even if they match stop words", () => {
55
- // Cyrillic "и" means "and" but should NOT be filtered as English stop word
56
- const result = buildFtsQuery("Привет и мир");
57
- expect(result).toBe('"Привет" OR "и" OR "мир"');
58
- });
59
- it("filters Latin stop words while preserving CJK in mixed query", () => {
60
- const result = buildFtsQuery("the 日本語 is great");
61
- expect(result).toBe('"日本語" OR "great"');
62
- });
63
- it("preserves Hebrew tokens (non-Latin script)", () => {
64
- const result = buildFtsQuery("מה בנינו ביחד");
65
- expect(result).toBe('"מה" OR "בנינו" OR "ביחד"');
66
- });
67
- it("preserves Arabic tokens (non-Latin script)", () => {
68
- const result = buildFtsQuery("ماذا بنينا معا");
69
- expect(result).toBe('"ماذا" OR "بنينا" OR "معا"');
70
- });
71
- it("filters Latin stop words while preserving Hebrew in mixed query", () => {
72
- const result = buildFtsQuery("the משחק is built");
73
- expect(result).toBe('"משחק" OR "built"');
74
- });
75
- // ── DATA-04: Unicode word character support ──────────────────────
76
- it("preserves Cyrillic characters", () => {
77
- const result = buildFtsQuery("\u041F\u0440\u0438\u0432\u0435\u0442 \u043C\u0438\u0440");
78
- expect(result).toBe('"\u041F\u0440\u0438\u0432\u0435\u0442" OR "\u043C\u0438\u0440"');
79
- });
80
- it("preserves CJK characters", () => {
81
- const result = buildFtsQuery("\u65E5\u672C\u8A9E");
82
- expect(result).toBe('"\u65E5\u672C\u8A9E"');
83
- });
84
- it("handles mixed Latin and CJK", () => {
85
- const result = buildFtsQuery("cafe \u65E5\u672C\u8A9E");
86
- expect(result).toBe('"cafe" OR "\u65E5\u672C\u8A9E"');
87
- });
88
- it("preserves Arabic characters", () => {
89
- const result = buildFtsQuery("\u0645\u0631\u062D\u0628\u0627");
90
- expect(result).toBe('"\u0645\u0631\u062D\u0628\u0627"');
91
- });
92
- it("still returns null for only symbols (no Unicode letters or numbers)", () => {
93
- expect(buildFtsQuery("***")).toBeNull();
94
- });
95
- it("handles mixed alpha-numeric as before", () => {
96
- const result = buildFtsQuery("test 123");
97
- expect(result).toBe('"test" OR "123"');
98
- });
99
- });
100
- // ── searchByText ─────────────────────────────────────────────────────
101
- describe("searchByText", () => {
102
- let db;
103
- beforeEach(() => {
104
- db = new Database(":memory:");
105
- initSchema(db, 4); // Small dimension for tests
106
- });
107
- it("finds exact keyword match", () => {
108
- insertMemory(db, "m1", "the quick brown fox jumps over the lazy dog");
109
- insertMemory(db, "m2", "a cat sleeps on the couch");
110
- const results = searchByText(db, "fox", 10);
111
- expect(results).toHaveLength(1);
112
- expect(results[0].id).toBe("m1");
113
- });
114
- it("FTS5 porter stemmer matches 'running' for query 'run'", () => {
115
- insertMemory(db, "m1", "she is running through the park");
116
- insertMemory(db, "m2", "he enjoys swimming");
117
- const results = searchByText(db, "run", 10);
118
- expect(results).toHaveLength(1);
119
- expect(results[0].id).toBe("m1");
120
- });
121
- it("returns results ordered by BM25 rank", () => {
122
- // Memory with more keyword density should rank higher
123
- insertMemory(db, "m1", "the cat sits");
124
- insertMemory(db, "m2", "the cat chases the cat toy and the cat plays");
125
- const results = searchByText(db, "cat", 10);
126
- expect(results.length).toBeGreaterThanOrEqual(2);
127
- // BM25 rank: lower is better match. m2 should have better (lower) rank
128
- expect(results[0].id).toBe("m2");
129
- });
130
- it("returns empty for no-match query", () => {
131
- insertMemory(db, "m1", "the quick brown fox");
132
- const results = searchByText(db, "elephant", 10);
133
- expect(results).toHaveLength(0);
134
- });
135
- it("limit parameter is respected", () => {
136
- insertMemory(db, "m1", "the cat sits on the mat");
137
- insertMemory(db, "m2", "the cat plays with yarn");
138
- insertMemory(db, "m3", "the cat sleeps in the sun");
139
- const results = searchByText(db, "cat", 2);
140
- expect(results).toHaveLength(2);
141
- });
142
- it("returns empty when query has no valid tokens", () => {
143
- insertMemory(db, "m1", "the quick brown fox");
144
- const results = searchByText(db, "!@#$%", 10);
145
- expect(results).toHaveLength(0);
146
- });
147
- });
148
- // ── searchByVector ───────────────────────────────────────────────────
149
- describe("searchByVector", () => {
150
- let db;
151
- beforeEach(() => {
152
- db = new Database(":memory:");
153
- initSchema(db, 4); // 4-dimensional vectors for testing
154
- });
155
- it("finds nearest neighbor by cosine distance", () => {
156
- if (!isVecAvailable())
157
- return;
158
- insertMemory(db, "m1", "entry one");
159
- insertMemory(db, "m2", "entry two");
160
- insertEmbedding(db, "m1", [1, 0, 0, 0]);
161
- insertEmbedding(db, "m2", [0, 1, 0, 0]);
162
- // Query embedding close to m1
163
- const results = searchByVector(db, [0.9, 0.1, 0, 0], 10);
164
- expect(results.length).toBeGreaterThanOrEqual(1);
165
- expect(results[0].id).toBe("m1");
166
- });
167
- it("k parameter controls result count", () => {
168
- if (!isVecAvailable())
169
- return;
170
- insertMemory(db, "m1", "entry one");
171
- insertMemory(db, "m2", "entry two");
172
- insertMemory(db, "m3", "entry three");
173
- insertEmbedding(db, "m1", [1, 0, 0, 0]);
174
- insertEmbedding(db, "m2", [0, 1, 0, 0]);
175
- insertEmbedding(db, "m3", [0, 0, 1, 0]);
176
- const results = searchByVector(db, [1, 0, 0, 0], 2);
177
- expect(results).toHaveLength(2);
178
- });
179
- it("Float32Array conversion works (same embedding returns distance near 0)", () => {
180
- if (!isVecAvailable())
181
- return;
182
- insertMemory(db, "m1", "exact match");
183
- insertEmbedding(db, "m1", [0.5, 0.5, 0.5, 0.5]);
184
- const results = searchByVector(db, [0.5, 0.5, 0.5, 0.5], 1);
185
- expect(results).toHaveLength(1);
186
- expect(results[0].id).toBe("m1");
187
- // Cosine distance for identical vectors should be ~0
188
- expect(results[0].distance).toBeCloseTo(0, 3);
189
- });
190
- it("returns empty when no embeddings are stored", () => {
191
- if (!isVecAvailable())
192
- return;
193
- // No memories inserted at all
194
- const results = searchByVector(db, [1, 0, 0, 0], 10);
195
- expect(results).toHaveLength(0);
196
- });
197
- });
198
- // ── computeRRF ───────────────────────────────────────────────────────
199
- describe("computeRRF", () => {
200
- it("result from both FTS and vec gets higher score than single-source", () => {
201
- const fts = [
202
- { id: "a", rank: 1 },
203
- { id: "b", rank: 2 },
204
- ];
205
- const vec = [
206
- { id: "a", rank: 1 },
207
- { id: "c", rank: 2 },
208
- ];
209
- const results = computeRRF(fts, vec);
210
- // "a" appears in both sources, should have highest score
211
- expect(results[0].id).toBe("a");
212
- // "a" score > "b" score and "a" score > "c" score
213
- const aScore = results.find((r) => r.id === "a").rrfScore;
214
- const bScore = results.find((r) => r.id === "b").rrfScore;
215
- const cScore = results.find((r) => r.id === "c").rrfScore;
216
- expect(aScore).toBeGreaterThan(bScore);
217
- expect(aScore).toBeGreaterThan(cScore);
218
- });
219
- it("empty FTS results -- vec results still ranked", () => {
220
- const fts = [];
221
- const vec = [
222
- { id: "a", rank: 1 },
223
- { id: "b", rank: 2 },
224
- ];
225
- const results = computeRRF(fts, vec);
226
- expect(results).toHaveLength(2);
227
- expect(results[0].id).toBe("a");
228
- expect(results[0].ftsRank).toBeNull();
229
- expect(results[0].vecRank).toBe(1);
230
- });
231
- it("empty vec results -- FTS results still ranked", () => {
232
- const fts = [
233
- { id: "a", rank: 1 },
234
- { id: "b", rank: 2 },
235
- ];
236
- const vec = [];
237
- const results = computeRRF(fts, vec);
238
- expect(results).toHaveLength(2);
239
- expect(results[0].id).toBe("a");
240
- expect(results[0].vecRank).toBeNull();
241
- expect(results[0].ftsRank).toBe(1);
242
- });
243
- it("both empty -- returns empty", () => {
244
- const results = computeRRF([], []);
245
- expect(results).toHaveLength(0);
246
- });
247
- it("weight parameters affect ranking", () => {
248
- // "a" is FTS-only, "b" is vec-only, both at rank 1
249
- const fts = [{ id: "a", rank: 1 }];
250
- const vec = [{ id: "b", rank: 1 }];
251
- // With higher vec weight, "b" should score higher
252
- const results = computeRRF(fts, vec, 1.0, 3.0);
253
- expect(results[0].id).toBe("b");
254
- // With higher FTS weight, "a" should score higher
255
- const results2 = computeRRF(fts, vec, 3.0, 1.0);
256
- expect(results2[0].id).toBe("a");
257
- });
258
- it("tracks source ranks correctly for dual-source results", () => {
259
- const fts = [{ id: "x", rank: 2 }];
260
- const vec = [{ id: "x", rank: 3 }];
261
- const results = computeRRF(fts, vec);
262
- expect(results).toHaveLength(1);
263
- expect(results[0].id).toBe("x");
264
- expect(results[0].ftsRank).toBe(2);
265
- expect(results[0].vecRank).toBe(3);
266
- });
267
- });
268
- // ── hybridSearch (integration) ───────────────────────────────────────
269
- describe("hybridSearch", () => {
270
- let db;
271
- beforeEach(() => {
272
- db = new Database(":memory:");
273
- initSchema(db, 4);
274
- });
275
- it("combines FTS and vector results for same query", () => {
276
- if (!isVecAvailable())
277
- return;
278
- // m1: matches text "dentist" and has embedding close to query
279
- insertMemory(db, "m1", "visit the dentist for a checkup");
280
- insertEmbedding(db, "m1", [1, 0, 0, 0]);
281
- // m2: matches text "dentist" but different embedding
282
- insertMemory(db, "m2", "the dentist said everything is fine");
283
- insertEmbedding(db, "m2", [0, 1, 0, 0]);
284
- // m3: no text match but embedding is close to query
285
- insertMemory(db, "m3", "oral health appointment scheduled");
286
- insertEmbedding(db, "m3", [0.9, 0.1, 0, 0]);
287
- const results = hybridSearch(db, "dentist", [0.95, 0.05, 0, 0], {
288
- limit: 10,
289
- });
290
- expect(results.length).toBeGreaterThanOrEqual(2);
291
- // m1 should rank highest: matches both text AND vector
292
- expect(results[0].id).toBe("m1");
293
- });
294
- it("falls back to FTS-only when no embedding provided", () => {
295
- insertMemory(db, "m1", "the quick brown fox");
296
- insertMemory(db, "m2", "lazy dog sleeps");
297
- const results = hybridSearch(db, "fox", undefined, { limit: 10 });
298
- expect(results).toHaveLength(1);
299
- expect(results[0].id).toBe("m1");
300
- });
301
- it("falls back to FTS-only when zero-length embedding provided (EMBED-01)", () => {
302
- insertMemory(db, "m1", "the quick brown fox");
303
- insertMemory(db, "m2", "lazy dog sleeps");
304
- // Empty array simulates embedding provider returning [] for short/emoji input
305
- const results = hybridSearch(db, "fox", [], { limit: 10 });
306
- // Should NOT crash -- zero-length guard skips vector search path
307
- expect(results).toHaveLength(1);
308
- expect(results[0].id).toBe("m1");
309
- });
310
- it("zero-length embedding produces same results as undefined embedding", () => {
311
- insertMemory(db, "m1", "cat plays with yarn ball");
312
- insertMemory(db, "m2", "cat naps in the sun");
313
- insertMemory(db, "m3", "dog barks at the mailman");
314
- const undefinedResults = hybridSearch(db, "cat", undefined, { limit: 10 });
315
- const emptyResults = hybridSearch(db, "cat", [], { limit: 10 });
316
- // Both should produce identical FTS-only results
317
- expect(emptyResults).toHaveLength(undefinedResults.length);
318
- expect(emptyResults.map((r) => r.id)).toEqual(undefinedResults.map((r) => r.id));
319
- });
320
- it("respects limit parameter on final output", () => {
321
- insertMemory(db, "m1", "cat plays with yarn");
322
- insertMemory(db, "m2", "cat naps in the sun");
323
- insertMemory(db, "m3", "cat chases a mouse");
324
- const results = hybridSearch(db, "cat", undefined, { limit: 2 });
325
- expect(results).toHaveLength(2);
326
- });
327
- it("filters by trustLevel", () => {
328
- insertMemory(db, "m1", "trusted cat memory", { trustLevel: "system" });
329
- insertMemory(db, "m2", "external cat data", { trustLevel: "external" });
330
- const results = hybridSearch(db, "cat", undefined, {
331
- limit: 10,
332
- trustLevel: "system",
333
- });
334
- expect(results).toHaveLength(1);
335
- expect(results[0].id).toBe("m1");
336
- });
337
- it("filters by memoryType", () => {
338
- insertMemory(db, "m1", "episodic cat event", { memoryType: "episodic" });
339
- insertMemory(db, "m2", "semantic cat fact", { memoryType: "semantic" });
340
- const results = hybridSearch(db, "cat", undefined, {
341
- limit: 10,
342
- memoryType: "episodic",
343
- });
344
- expect(results).toHaveLength(1);
345
- expect(results[0].id).toBe("m1");
346
- });
347
- it("filters by tenantId", () => {
348
- insertMemory(db, "m1", "tenant A cat", { tenantId: "tenantA" });
349
- insertMemory(db, "m2", "tenant B cat", { tenantId: "tenantB" });
350
- const results = hybridSearch(db, "cat", undefined, {
351
- limit: 10,
352
- tenantId: "tenantA",
353
- });
354
- expect(results).toHaveLength(1);
355
- expect(results[0].id).toBe("m1");
356
- });
357
- it("returns empty when no memories match query", () => {
358
- insertMemory(db, "m1", "the quick brown fox");
359
- const results = hybridSearch(db, "elephant", undefined, { limit: 10 });
360
- expect(results).toHaveLength(0);
361
- });
362
- it("returns empty when query produces no valid tokens", () => {
363
- insertMemory(db, "m1", "the quick brown fox");
364
- const results = hybridSearch(db, "!@#$%", undefined, { limit: 10 });
365
- expect(results).toHaveLength(0);
366
- });
367
- it("returns vector-only results when FTS5 has no keyword matches", () => {
368
- if (!isVecAvailable())
369
- return;
370
- insertMemory(db, "m1", "space shooter with asteroids");
371
- insertEmbedding(db, "m1", [1, 0, 0, 0]);
372
- insertMemory(db, "m2", "underwater exploration game");
373
- insertEmbedding(db, "m2", [0, 1, 0, 0]);
374
- // Query has no keyword overlap but embedding is close to m1
375
- const results = hybridSearch(db, "cosmic blaster", [0.9, 0.1, 0, 0], { limit: 10 });
376
- expect(results.length).toBeGreaterThanOrEqual(1);
377
- expect(results[0].id).toBe("m1");
378
- });
379
- it("vector weight boost ranks semantic matches higher", () => {
380
- if (!isVecAvailable())
381
- return;
382
- // m1: weak FTS match (rank 2), strong vector match (rank 1)
383
- insertMemory(db, "m1", "space shooter void breaker game with depth");
384
- insertEmbedding(db, "m1", [1, 0, 0, 0]);
385
- // m2: strong FTS match (rank 1), weak vector match (rank 2)
386
- insertMemory(db, "m2", "built a game together recently");
387
- insertEmbedding(db, "m2", [0, 1, 0, 0]);
388
- // Query embedding close to m1, text matches "game" in both
389
- const results = hybridSearch(db, "game", [0.95, 0.05, 0, 0], { limit: 10 });
390
- expect(results.length).toBeGreaterThanOrEqual(2);
391
- // m1 should rank higher due to vector weight boost (1.5x)
392
- expect(results[0].id).toBe("m1");
393
- });
394
- });
395
- // ── RRF score normalization ─────────────────────────────────────────
396
- describe("RRF score normalization", () => {
397
- let db;
398
- beforeEach(() => {
399
- db = new Database(":memory:");
400
- initSchema(db, 4);
401
- });
402
- it("hybridSearch scores are normalized to 0-1 range", () => {
403
- if (!isVecAvailable())
404
- return;
405
- insertMemory(db, "m1", "the dentist appointment next week");
406
- insertEmbedding(db, "m1", [1, 0, 0, 0]);
407
- insertMemory(db, "m2", "dentist cleaning scheduled for monday");
408
- insertEmbedding(db, "m2", [0.8, 0.2, 0, 0]);
409
- insertMemory(db, "m3", "visit dentist for cavity filling");
410
- insertEmbedding(db, "m3", [0.6, 0.4, 0, 0]);
411
- const results = hybridSearch(db, "dentist", [0.95, 0.05, 0, 0], {
412
- limit: 10,
413
- });
414
- expect(results.length).toBeGreaterThanOrEqual(1);
415
- for (const r of results) {
416
- expect(r.score).toBeGreaterThanOrEqual(0);
417
- expect(r.score).toBeLessThanOrEqual(1);
418
- }
419
- });
420
- it("highest-ranked result has score near 1.0", () => {
421
- if (!isVecAvailable())
422
- return;
423
- // m1 matches both FTS and vector at rank 1
424
- insertMemory(db, "m1", "dentist checkup tomorrow morning");
425
- insertEmbedding(db, "m1", [1, 0, 0, 0]);
426
- insertMemory(db, "m2", "dentist said teeth are healthy");
427
- insertEmbedding(db, "m2", [0, 1, 0, 0]);
428
- const results = hybridSearch(db, "dentist", [0.99, 0.01, 0, 0], {
429
- limit: 10,
430
- });
431
- expect(results.length).toBeGreaterThanOrEqual(1);
432
- // After normalization, top result should score well above 0.5
433
- // (raw RRF would be ~0.041, proving normalization happened)
434
- expect(results[0].score).toBeGreaterThan(0.5);
435
- });
436
- it("FTS-only fallback scores are normalized to 0-1 range", () => {
437
- insertMemory(db, "m1", "cat plays with yarn ball");
438
- insertMemory(db, "m2", "cat naps in the sunny window");
439
- insertMemory(db, "m3", "cat chases a laser pointer");
440
- // No embedding provided -- FTS-only fallback
441
- const results = hybridSearch(db, "cat", undefined, { limit: 10 });
442
- expect(results.length).toBeGreaterThanOrEqual(1);
443
- for (const r of results) {
444
- expect(r.score).toBeGreaterThanOrEqual(0);
445
- expect(r.score).toBeLessThanOrEqual(1);
446
- }
447
- });
448
- it("score ordering preserved after normalization", () => {
449
- if (!isVecAvailable())
450
- return;
451
- // Varied relevance: m1 matches both strongly, m2 matches FTS only, m3 matches vec weakly
452
- insertMemory(db, "m1", "cat plays with yarn ball daily");
453
- insertEmbedding(db, "m1", [1, 0, 0, 0]);
454
- insertMemory(db, "m2", "cat naps in the sunny window all day");
455
- insertEmbedding(db, "m2", [0, 0, 1, 0]);
456
- insertMemory(db, "m3", "dog barks at the mailman");
457
- insertEmbedding(db, "m3", [0.9, 0.1, 0, 0]);
458
- const results = hybridSearch(db, "cat", [0.95, 0.05, 0, 0], {
459
- limit: 10,
460
- });
461
- // Scores must be in strictly descending order
462
- for (let i = 1; i < results.length; i++) {
463
- expect(results[i - 1].score).toBeGreaterThanOrEqual(results[i].score);
464
- }
465
- });
466
- it("computeRRF raw scores are NOT normalized", () => {
467
- // computeRRF is the raw engine -- normalization happens in hybridSearch
468
- const fts = [{ id: "a", rank: 1 }];
469
- const vec = [{ id: "a", rank: 1 }];
470
- const results = computeRRF(fts, vec, 1.0, 1.5);
471
- expect(results).toHaveLength(1);
472
- // Raw RRF score: (1.0 / 61) + (1.5 / 61) = 2.5 / 61 = ~0.04098
473
- expect(results[0].rrfScore).toBeLessThan(0.05);
474
- expect(results[0].rrfScore).toBeGreaterThan(0);
475
- });
476
- });
@@ -1,88 +0,0 @@
1
- import Database from "better-sqlite3";
2
- import { describe, it, expect, beforeEach } from "vitest";
3
- import { initSchema } from "./schema.js";
4
- import { createIdentityLinkStore, } from "./identity-link-store.js";
5
- describe("createIdentityLinkStore", () => {
6
- let db;
7
- let store;
8
- beforeEach(() => {
9
- db = new Database(":memory:");
10
- initSchema(db, 128);
11
- store = createIdentityLinkStore(db);
12
- });
13
- it("link creates a new identity link, resolve returns the canonical ID", () => {
14
- store.link("canonical-1", "discord", "discord-user-42");
15
- const result = store.resolve("discord", "discord-user-42");
16
- expect(result).toBe("canonical-1");
17
- });
18
- it("link with same provider+providerUserId but different canonicalId updates (upsert)", () => {
19
- store.link("canonical-1", "telegram", "tg-user-7");
20
- store.link("canonical-2", "telegram", "tg-user-7");
21
- const result = store.resolve("telegram", "tg-user-7");
22
- expect(result).toBe("canonical-2");
23
- });
24
- it("unlink removes existing link and returns true", () => {
25
- store.link("canonical-1", "discord", "discord-user-42");
26
- const removed = store.unlink("discord", "discord-user-42");
27
- expect(removed).toBe(true);
28
- const result = store.resolve("discord", "discord-user-42");
29
- expect(result).toBeUndefined();
30
- });
31
- it("unlink returns false for non-existent link", () => {
32
- const removed = store.unlink("discord", "nonexistent-user");
33
- expect(removed).toBe(false);
34
- });
35
- it("resolve returns undefined for unlinked provider identity", () => {
36
- const result = store.resolve("slack", "unknown-user");
37
- expect(result).toBeUndefined();
38
- });
39
- it("listByCanonical returns all links for a canonical ID", () => {
40
- store.link("canonical-1", "discord", "discord-user-42");
41
- store.link("canonical-1", "telegram", "tg-user-7");
42
- store.link("canonical-2", "slack", "slack-user-99");
43
- const links = store.listByCanonical("canonical-1");
44
- expect(links).toHaveLength(2);
45
- expect(links.map((l) => l.provider).sort()).toEqual(["discord", "telegram"]);
46
- expect(links.every((l) => l.canonicalId === "canonical-1")).toBe(true);
47
- });
48
- it("listAll returns all links sorted by canonical_id, provider", () => {
49
- store.link("canonical-2", "slack", "slack-user-99");
50
- store.link("canonical-1", "telegram", "tg-user-7");
51
- store.link("canonical-1", "discord", "discord-user-42");
52
- const links = store.listAll();
53
- expect(links).toHaveLength(3);
54
- // Sorted by canonical_id then provider
55
- expect(links[0].canonicalId).toBe("canonical-1");
56
- expect(links[0].provider).toBe("discord");
57
- expect(links[1].canonicalId).toBe("canonical-1");
58
- expect(links[1].provider).toBe("telegram");
59
- expect(links[2].canonicalId).toBe("canonical-2");
60
- expect(links[2].provider).toBe("slack");
61
- });
62
- it("link with displayName stores and retrieves it", () => {
63
- store.link("canonical-1", "discord", "discord-user-42", "CoolUser#1234");
64
- const links = store.listByCanonical("canonical-1");
65
- expect(links).toHaveLength(1);
66
- expect(links[0].displayName).toBe("CoolUser#1234");
67
- });
68
- it("link without displayName stores undefined", () => {
69
- store.link("canonical-1", "discord", "discord-user-42");
70
- const links = store.listByCanonical("canonical-1");
71
- expect(links).toHaveLength(1);
72
- expect(links[0].displayName).toBeUndefined();
73
- });
74
- it("listByCanonical returns links ordered by linked_at DESC", () => {
75
- // Insert with explicit timestamps for deterministic ordering
76
- db.prepare(`INSERT INTO identity_links (canonical_id, provider, provider_user_id, display_name, linked_at)
77
- VALUES ('c1', 'discord', 'u1', NULL, 1000)`).run();
78
- db.prepare(`INSERT INTO identity_links (canonical_id, provider, provider_user_id, display_name, linked_at)
79
- VALUES ('c1', 'telegram', 'u2', NULL, 3000)`).run();
80
- db.prepare(`INSERT INTO identity_links (canonical_id, provider, provider_user_id, display_name, linked_at)
81
- VALUES ('c1', 'slack', 'u3', NULL, 2000)`).run();
82
- const links = store.listByCanonical("c1");
83
- expect(links).toHaveLength(3);
84
- expect(links[0].provider).toBe("telegram"); // linked_at 3000
85
- expect(links[1].provider).toBe("slack"); // linked_at 2000
86
- expect(links[2].provider).toBe("discord"); // linked_at 1000
87
- });
88
- });
@@ -1 +0,0 @@
1
- export {};