comisai 1.0.10 → 1.0.12

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