@namzu/sdk 0.4.2 → 0.4.3

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 (207) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/advisory/context.test.d.ts +16 -0
  3. package/dist/advisory/context.test.d.ts.map +1 -0
  4. package/dist/advisory/context.test.js +92 -0
  5. package/dist/advisory/context.test.js.map +1 -0
  6. package/dist/advisory/evaluator.test.d.ts +34 -0
  7. package/dist/advisory/evaluator.test.d.ts.map +1 -0
  8. package/dist/advisory/evaluator.test.js +172 -0
  9. package/dist/advisory/evaluator.test.js.map +1 -0
  10. package/dist/advisory/executor.test.d.ts +35 -0
  11. package/dist/advisory/executor.test.d.ts.map +1 -0
  12. package/dist/advisory/executor.test.js +233 -0
  13. package/dist/advisory/executor.test.js.map +1 -0
  14. package/dist/advisory/registry.test.d.ts +16 -0
  15. package/dist/advisory/registry.test.d.ts.map +1 -0
  16. package/dist/advisory/registry.test.js +62 -0
  17. package/dist/advisory/registry.test.js.map +1 -0
  18. package/dist/bridge/a2a/agent-card.test.d.ts +24 -0
  19. package/dist/bridge/a2a/agent-card.test.d.ts.map +1 -0
  20. package/dist/bridge/a2a/agent-card.test.js +118 -0
  21. package/dist/bridge/a2a/agent-card.test.js.map +1 -0
  22. package/dist/bridge/a2a/mapper.test.d.ts +29 -0
  23. package/dist/bridge/a2a/mapper.test.d.ts.map +1 -0
  24. package/dist/bridge/a2a/mapper.test.js +265 -0
  25. package/dist/bridge/a2a/mapper.test.js.map +1 -0
  26. package/dist/bridge/a2a/message.test.d.ts +20 -0
  27. package/dist/bridge/a2a/message.test.d.ts.map +1 -0
  28. package/dist/bridge/a2a/message.test.js +116 -0
  29. package/dist/bridge/a2a/message.test.js.map +1 -0
  30. package/dist/bridge/a2a/task.test.d.ts +29 -0
  31. package/dist/bridge/a2a/task.test.d.ts.map +1 -0
  32. package/dist/bridge/a2a/task.test.js +198 -0
  33. package/dist/bridge/a2a/task.test.js.map +1 -0
  34. package/dist/bridge/mcp/connector/adapter.test.d.ts +27 -0
  35. package/dist/bridge/mcp/connector/adapter.test.d.ts.map +1 -0
  36. package/dist/bridge/mcp/connector/adapter.test.js +203 -0
  37. package/dist/bridge/mcp/connector/adapter.test.js.map +1 -0
  38. package/dist/bridge/sse/mapper.test.d.ts +27 -0
  39. package/dist/bridge/sse/mapper.test.d.ts.map +1 -0
  40. package/dist/bridge/sse/mapper.test.js +271 -0
  41. package/dist/bridge/sse/mapper.test.js.map +1 -0
  42. package/dist/bridge/tools/connector/adapter.test.d.ts +28 -0
  43. package/dist/bridge/tools/connector/adapter.test.d.ts.map +1 -0
  44. package/dist/bridge/tools/connector/adapter.test.js +182 -0
  45. package/dist/bridge/tools/connector/adapter.test.js.map +1 -0
  46. package/dist/bridge/tools/connector/definitions.test.d.ts +23 -0
  47. package/dist/bridge/tools/connector/definitions.test.d.ts.map +1 -0
  48. package/dist/bridge/tools/connector/definitions.test.js +158 -0
  49. package/dist/bridge/tools/connector/definitions.test.js.map +1 -0
  50. package/dist/bridge/tools/connector/router.test.d.ts +21 -0
  51. package/dist/bridge/tools/connector/router.test.d.ts.map +1 -0
  52. package/dist/bridge/tools/connector/router.test.js +139 -0
  53. package/dist/bridge/tools/connector/router.test.js.map +1 -0
  54. package/dist/bus/breaker.test.d.ts +41 -0
  55. package/dist/bus/breaker.test.d.ts.map +1 -0
  56. package/dist/bus/breaker.test.js +242 -0
  57. package/dist/bus/breaker.test.js.map +1 -0
  58. package/dist/bus/index.test.d.ts +25 -0
  59. package/dist/bus/index.test.d.ts.map +1 -0
  60. package/dist/bus/index.test.js +151 -0
  61. package/dist/bus/index.test.js.map +1 -0
  62. package/dist/bus/lock.test.d.ts +44 -0
  63. package/dist/bus/lock.test.d.ts.map +1 -0
  64. package/dist/bus/lock.test.js +226 -0
  65. package/dist/bus/lock.test.js.map +1 -0
  66. package/dist/bus/ownership.test.d.ts +26 -0
  67. package/dist/bus/ownership.test.d.ts.map +1 -0
  68. package/dist/bus/ownership.test.js +205 -0
  69. package/dist/bus/ownership.test.js.map +1 -0
  70. package/dist/connector/BaseConnector.test.d.ts +21 -0
  71. package/dist/connector/BaseConnector.test.d.ts.map +1 -0
  72. package/dist/connector/BaseConnector.test.js +108 -0
  73. package/dist/connector/BaseConnector.test.js.map +1 -0
  74. package/dist/connector/builtins/http.test.d.ts +30 -0
  75. package/dist/connector/builtins/http.test.d.ts.map +1 -0
  76. package/dist/connector/builtins/http.test.js +232 -0
  77. package/dist/connector/builtins/http.test.js.map +1 -0
  78. package/dist/connector/builtins/webhook.test.d.ts +20 -0
  79. package/dist/connector/builtins/webhook.test.d.ts.map +1 -0
  80. package/dist/connector/builtins/webhook.test.js +113 -0
  81. package/dist/connector/builtins/webhook.test.js.map +1 -0
  82. package/dist/connector/execution/factory.test.d.ts +16 -0
  83. package/dist/connector/execution/factory.test.d.ts.map +1 -0
  84. package/dist/connector/execution/factory.test.js +64 -0
  85. package/dist/connector/execution/factory.test.js.map +1 -0
  86. package/dist/connector/execution/remote.test.d.ts +16 -0
  87. package/dist/connector/execution/remote.test.d.ts.map +1 -0
  88. package/dist/connector/execution/remote.test.js +53 -0
  89. package/dist/connector/execution/remote.test.js.map +1 -0
  90. package/dist/connector/mcp/adapter.test.d.ts +34 -0
  91. package/dist/connector/mcp/adapter.test.d.ts.map +1 -0
  92. package/dist/connector/mcp/adapter.test.js +199 -0
  93. package/dist/connector/mcp/adapter.test.js.map +1 -0
  94. package/dist/rag/chunking.test.d.ts +20 -0
  95. package/dist/rag/chunking.test.d.ts.map +1 -0
  96. package/dist/rag/chunking.test.js +92 -0
  97. package/dist/rag/chunking.test.js.map +1 -0
  98. package/dist/rag/context-assembler.test.d.ts +19 -0
  99. package/dist/rag/context-assembler.test.d.ts.map +1 -0
  100. package/dist/rag/context-assembler.test.js +98 -0
  101. package/dist/rag/context-assembler.test.js.map +1 -0
  102. package/dist/rag/embedding.test.d.ts +19 -0
  103. package/dist/rag/embedding.test.d.ts.map +1 -0
  104. package/dist/rag/embedding.test.js +115 -0
  105. package/dist/rag/embedding.test.js.map +1 -0
  106. package/dist/rag/ingestion.test.d.ts +22 -0
  107. package/dist/rag/ingestion.test.d.ts.map +1 -0
  108. package/dist/rag/ingestion.test.js +99 -0
  109. package/dist/rag/ingestion.test.js.map +1 -0
  110. package/dist/rag/knowledge-base.test.d.ts +17 -0
  111. package/dist/rag/knowledge-base.test.d.ts.map +1 -0
  112. package/dist/rag/knowledge-base.test.js +77 -0
  113. package/dist/rag/knowledge-base.test.js.map +1 -0
  114. package/dist/rag/rag-tool.test.d.ts +21 -0
  115. package/dist/rag/rag-tool.test.d.ts.map +1 -0
  116. package/dist/rag/rag-tool.test.js +149 -0
  117. package/dist/rag/rag-tool.test.js.map +1 -0
  118. package/dist/rag/retriever.test.d.ts +26 -0
  119. package/dist/rag/retriever.test.d.ts.map +1 -0
  120. package/dist/rag/retriever.test.js +180 -0
  121. package/dist/rag/retriever.test.js.map +1 -0
  122. package/dist/rag/vector-store.test.d.ts +38 -0
  123. package/dist/rag/vector-store.test.d.ts.map +1 -0
  124. package/dist/rag/vector-store.test.js +175 -0
  125. package/dist/rag/vector-store.test.js.map +1 -0
  126. package/dist/registry/ManagedRegistry.test.d.ts +21 -0
  127. package/dist/registry/ManagedRegistry.test.d.ts.map +1 -0
  128. package/dist/registry/ManagedRegistry.test.js +98 -0
  129. package/dist/registry/ManagedRegistry.test.js.map +1 -0
  130. package/dist/registry/Registry.test.d.ts +18 -0
  131. package/dist/registry/Registry.test.d.ts.map +1 -0
  132. package/dist/registry/Registry.test.js +79 -0
  133. package/dist/registry/Registry.test.js.map +1 -0
  134. package/dist/registry/agent/definitions.test.d.ts +15 -0
  135. package/dist/registry/agent/definitions.test.d.ts.map +1 -0
  136. package/dist/registry/agent/definitions.test.js +84 -0
  137. package/dist/registry/agent/definitions.test.js.map +1 -0
  138. package/dist/registry/connector/definitions.test.d.ts +13 -0
  139. package/dist/registry/connector/definitions.test.d.ts.map +1 -0
  140. package/dist/registry/connector/definitions.test.js +41 -0
  141. package/dist/registry/connector/definitions.test.js.map +1 -0
  142. package/dist/registry/connector/scoped.test.d.ts +21 -0
  143. package/dist/registry/connector/scoped.test.d.ts.map +1 -0
  144. package/dist/registry/connector/scoped.test.js +115 -0
  145. package/dist/registry/connector/scoped.test.js.map +1 -0
  146. package/dist/registry/plugin/index.test.d.ts +12 -0
  147. package/dist/registry/plugin/index.test.d.ts.map +1 -0
  148. package/dist/registry/plugin/index.test.js +69 -0
  149. package/dist/registry/plugin/index.test.js.map +1 -0
  150. package/dist/registry/tool/execute.test.d.ts +42 -0
  151. package/dist/registry/tool/execute.test.d.ts.map +1 -0
  152. package/dist/registry/tool/execute.test.js +281 -0
  153. package/dist/registry/tool/execute.test.js.map +1 -0
  154. package/dist/runtime/query/iteration/phases/advisory.test.d.ts +42 -0
  155. package/dist/runtime/query/iteration/phases/advisory.test.d.ts.map +1 -0
  156. package/dist/runtime/query/iteration/phases/advisory.test.js +334 -0
  157. package/dist/runtime/query/iteration/phases/advisory.test.js.map +1 -0
  158. package/dist/test-setup.d.ts +22 -0
  159. package/dist/test-setup.d.ts.map +1 -0
  160. package/dist/test-setup.js +23 -0
  161. package/dist/test-setup.js.map +1 -0
  162. package/dist/utils/logger.d.ts +1 -1
  163. package/dist/utils/logger.d.ts.map +1 -1
  164. package/dist/utils/logger.js +5 -0
  165. package/dist/utils/logger.js.map +1 -1
  166. package/package.json +4 -1
  167. package/src/advisory/context.test.ts +109 -0
  168. package/src/advisory/evaluator.test.ts +192 -0
  169. package/src/advisory/executor.test.ts +272 -0
  170. package/src/advisory/registry.test.ts +75 -0
  171. package/src/bridge/a2a/agent-card.test.ts +140 -0
  172. package/src/bridge/a2a/mapper.test.ts +293 -0
  173. package/src/bridge/a2a/message.test.ts +138 -0
  174. package/src/bridge/a2a/task.test.ts +235 -0
  175. package/src/bridge/mcp/connector/adapter.test.ts +230 -0
  176. package/src/bridge/sse/mapper.test.ts +422 -0
  177. package/src/bridge/tools/connector/adapter.test.ts +224 -0
  178. package/src/bridge/tools/connector/definitions.test.ts +183 -0
  179. package/src/bridge/tools/connector/router.test.ts +159 -0
  180. package/src/bus/breaker.test.ts +274 -0
  181. package/src/bus/index.test.ts +183 -0
  182. package/src/bus/lock.test.ts +265 -0
  183. package/src/bus/ownership.test.ts +243 -0
  184. package/src/connector/BaseConnector.test.ts +130 -0
  185. package/src/connector/builtins/http.test.ts +290 -0
  186. package/src/connector/builtins/webhook.test.ts +138 -0
  187. package/src/connector/execution/factory.test.ts +75 -0
  188. package/src/connector/execution/remote.test.ts +63 -0
  189. package/src/connector/mcp/adapter.test.ts +249 -0
  190. package/src/rag/chunking.test.ts +107 -0
  191. package/src/rag/context-assembler.test.ts +114 -0
  192. package/src/rag/embedding.test.ts +130 -0
  193. package/src/rag/ingestion.test.ts +114 -0
  194. package/src/rag/knowledge-base.test.ts +106 -0
  195. package/src/rag/rag-tool.test.ts +167 -0
  196. package/src/rag/retriever.test.ts +210 -0
  197. package/src/rag/vector-store.test.ts +196 -0
  198. package/src/registry/ManagedRegistry.test.ts +118 -0
  199. package/src/registry/Registry.test.ts +91 -0
  200. package/src/registry/agent/definitions.test.ts +100 -0
  201. package/src/registry/connector/definitions.test.ts +51 -0
  202. package/src/registry/connector/scoped.test.ts +129 -0
  203. package/src/registry/plugin/index.test.ts +85 -0
  204. package/src/registry/tool/execute.test.ts +330 -0
  205. package/src/runtime/query/iteration/phases/advisory.test.ts +412 -0
  206. package/src/test-setup.ts +24 -0
  207. package/src/utils/logger.ts +6 -1
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Current-code invariants asserted (2026-04-21, ses_006 Phase 4):
3
+ *
4
+ * - `DefaultIngestionPipeline.ingest(content, metadata, scope, kbId)`:
5
+ * - Generates a fresh `documentId` per call.
6
+ * - Chunks via `TextChunker.chunk(content, chunkingConfig)`.
7
+ * - Returns zero-chunk result when the chunker emits nothing.
8
+ * - Calls `embeddingProvider.embed([...chunkTexts])` exactly once
9
+ * with every chunk in order.
10
+ * - Each resulting `Chunk` carries:
11
+ * - a fresh `id`,
12
+ * - the generated `documentId`,
13
+ * - the passed `knowledgeBaseId`,
14
+ * - `scope.tenantId`,
15
+ * - the original `chunkContent` + `chunkIndex` metadata,
16
+ * - `tokenCount = Math.ceil(content.length / 4)`.
17
+ * - Calls `vectorStore.upsert(chunks)` exactly once.
18
+ * - Totals `tokenCount` across chunks and reports `durationMs`.
19
+ * - `remove(documentId)` delegates to `vectorStore.deleteByDocument`.
20
+ */
21
+
22
+ import { describe, expect, it, vi } from 'vitest'
23
+ import { z } from 'zod'
24
+
25
+ import type { DocumentId, KnowledgeBaseId, TenantId } from '../types/ids/index.js'
26
+ import type { Chunk, EmbeddingProvider, TenantScope, VectorStore } from '../types/rag/index.js'
27
+
28
+ import { DefaultIngestionPipeline } from './ingestion.js'
29
+
30
+ const KB = 'kb_1' as KnowledgeBaseId
31
+ const TENANT = 't_1' as TenantId
32
+ const scope: TenantScope = { tenantId: TENANT }
33
+
34
+ function makeVectorStore(): VectorStore {
35
+ return {
36
+ upsert: vi.fn<(chunks: Chunk[]) => Promise<void>>(),
37
+ search: vi.fn(),
38
+ delete: vi.fn(),
39
+ deleteByDocument: vi.fn(),
40
+ deleteByKnowledgeBase: vi.fn(),
41
+ } as unknown as VectorStore
42
+ }
43
+
44
+ function makeEmbedder(): EmbeddingProvider {
45
+ return {
46
+ id: 'mock',
47
+ model: 'x',
48
+ dimensions: 3,
49
+ embed: vi.fn(async (texts: string[]) => texts.map(() => [1, 0, 0])),
50
+ embedQuery: vi.fn(async () => [1, 0, 0]),
51
+ }
52
+ }
53
+
54
+ describe('DefaultIngestionPipeline — ingest', () => {
55
+ it('returns zero-chunk result when the chunker emits nothing', async () => {
56
+ const vs = makeVectorStore()
57
+ const emb = makeEmbedder()
58
+ // chunkSize 10, no content at all → chunker returns []
59
+ const pipeline = new DefaultIngestionPipeline(vs, emb, {
60
+ strategy: 'fixed',
61
+ chunkSize: 10,
62
+ chunkOverlap: 0,
63
+ })
64
+ const result = await pipeline.ingest(' ', {}, scope, KB)
65
+ expect(result.chunkCount).toBe(0)
66
+ expect(result.totalTokens).toBe(0)
67
+ expect(vs.upsert).not.toHaveBeenCalled()
68
+ })
69
+
70
+ it('chunks content, embeds every chunk once, upserts once', async () => {
71
+ const vs = makeVectorStore()
72
+ const emb = makeEmbedder()
73
+ const pipeline = new DefaultIngestionPipeline(vs, emb, {
74
+ strategy: 'fixed',
75
+ chunkSize: 10,
76
+ chunkOverlap: 0,
77
+ })
78
+
79
+ const result = await pipeline.ingest('a'.repeat(25), { source: 'repo' }, scope, KB)
80
+
81
+ expect(emb.embed).toHaveBeenCalledTimes(1)
82
+ expect(vs.upsert).toHaveBeenCalledTimes(1)
83
+ expect(result.chunkCount).toBeGreaterThan(1)
84
+
85
+ const upsertedChunks = vi.mocked(vs.upsert).mock.calls[0]?.[0] ?? []
86
+ for (const c of upsertedChunks) {
87
+ expect(c.documentId).toBe(result.documentId)
88
+ expect(c.knowledgeBaseId).toBe(KB)
89
+ expect(c.tenantId).toBe(TENANT)
90
+ expect(c.metadata.source).toBe('repo')
91
+ expect(c.metadata.chunkIndex).toBeDefined()
92
+ expect(c.tokenCount).toBe(Math.ceil(c.content.length / 4))
93
+ }
94
+ })
95
+
96
+ it('generates a fresh documentId per ingest call', async () => {
97
+ const pipeline = new DefaultIngestionPipeline(makeVectorStore(), makeEmbedder())
98
+ const a = await pipeline.ingest('alpha', {}, scope, KB)
99
+ const b = await pipeline.ingest('beta', {}, scope, KB)
100
+ expect(a.documentId).not.toBe(b.documentId)
101
+ })
102
+ })
103
+
104
+ describe('DefaultIngestionPipeline — remove', () => {
105
+ it('delegates to vectorStore.deleteByDocument', async () => {
106
+ const vs = makeVectorStore()
107
+ const pipeline = new DefaultIngestionPipeline(vs, makeEmbedder())
108
+ await pipeline.remove('doc_9' as DocumentId)
109
+ expect(vs.deleteByDocument).toHaveBeenCalledWith('doc_9')
110
+ })
111
+ })
112
+
113
+ // Avoid the unused zod import — used in type coverage indirectly.
114
+ void z
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Current-code invariants asserted (2026-04-21, ses_006 Phase 4):
3
+ *
4
+ * - `DefaultKnowledgeBase` is a composition layer: it builds an
5
+ * ingestion pipeline + a retriever from its config and delegates
6
+ * `ingest` / `query` / `remove` / `clear` to them.
7
+ * - `id` is set from `config.id` if provided, else generated once
8
+ * via `generateKnowledgeBaseId` — never regenerated later.
9
+ * - `ingest(content, metadata)` invokes the ingestion pipeline with
10
+ * the configured scope + kb id; returns whatever the pipeline
11
+ * returns.
12
+ * - `query(query)` invokes the retriever with scope + kb id.
13
+ * - `clear()` calls `vectorStore.deleteByKnowledgeBase(id, tenantId)`.
14
+ * - `remove(documentId)` calls `vectorStore.deleteByDocument`.
15
+ */
16
+
17
+ import { describe, expect, it, vi } from 'vitest'
18
+
19
+ import type { DocumentId, KnowledgeBaseId, TenantId } from '../types/ids/index.js'
20
+ import type { EmbeddingProvider, VectorStore } from '../types/rag/index.js'
21
+
22
+ import { DefaultKnowledgeBase } from './knowledge-base.js'
23
+
24
+ const TENANT = 't_1' as TenantId
25
+
26
+ function makeVectorStore(): VectorStore {
27
+ return {
28
+ upsert: vi.fn(),
29
+ search: vi.fn(async () => []),
30
+ delete: vi.fn(),
31
+ deleteByDocument: vi.fn(),
32
+ deleteByKnowledgeBase: vi.fn(),
33
+ }
34
+ }
35
+
36
+ function makeEmbedder(): EmbeddingProvider {
37
+ return {
38
+ id: 'mock',
39
+ model: 'x',
40
+ dimensions: 3,
41
+ embed: vi.fn(async (texts: string[]) => texts.map(() => [1, 0, 0])),
42
+ embedQuery: vi.fn(async () => [1, 0, 0]),
43
+ }
44
+ }
45
+
46
+ describe('DefaultKnowledgeBase', () => {
47
+ it('uses the id provided in config when set', () => {
48
+ const kb = new DefaultKnowledgeBase(
49
+ { id: 'kb_fixed' as KnowledgeBaseId, name: 'kb', tenantId: TENANT },
50
+ makeVectorStore(),
51
+ makeEmbedder(),
52
+ )
53
+ expect(kb.id).toBe('kb_fixed')
54
+ expect(kb.config.id).toBe('kb_fixed')
55
+ })
56
+
57
+ it('generates an id when none is provided', () => {
58
+ const kb = new DefaultKnowledgeBase(
59
+ { name: 'kb', tenantId: TENANT },
60
+ makeVectorStore(),
61
+ makeEmbedder(),
62
+ )
63
+ expect(kb.id).toMatch(/^kb_/)
64
+ })
65
+
66
+ it('ingest delegates to the ingestion pipeline and carries metadata through', async () => {
67
+ const vs = makeVectorStore()
68
+ const kb = new DefaultKnowledgeBase(
69
+ { id: 'kb_fixed' as KnowledgeBaseId, name: 'kb', tenantId: TENANT },
70
+ vs,
71
+ makeEmbedder(),
72
+ )
73
+ const result = await kb.ingest('hello world', { source: 'readme' })
74
+ expect(result.documentId).toMatch(/^doc_/)
75
+ expect(vs.upsert).toHaveBeenCalled()
76
+ const chunks = vi.mocked(vs.upsert).mock.calls[0]?.[0] ?? []
77
+ expect(chunks[0]?.knowledgeBaseId).toBe('kb_fixed')
78
+ expect(chunks[0]?.tenantId).toBe(TENANT)
79
+ })
80
+
81
+ it('remove delegates to vectorStore.deleteByDocument', async () => {
82
+ const vs = makeVectorStore()
83
+ const kb = new DefaultKnowledgeBase({ name: 'kb', tenantId: TENANT }, vs, makeEmbedder())
84
+ await kb.remove('doc_1' as DocumentId)
85
+ expect(vs.deleteByDocument).toHaveBeenCalledWith('doc_1')
86
+ })
87
+
88
+ it('clear delegates to vectorStore.deleteByKnowledgeBase with id + tenantId', async () => {
89
+ const vs = makeVectorStore()
90
+ const kb = new DefaultKnowledgeBase(
91
+ { id: 'kb_fixed' as KnowledgeBaseId, name: 'kb', tenantId: TENANT },
92
+ vs,
93
+ makeEmbedder(),
94
+ )
95
+ await kb.clear()
96
+ expect(vs.deleteByKnowledgeBase).toHaveBeenCalledWith('kb_fixed', TENANT)
97
+ })
98
+
99
+ it('query delegates to retriever', async () => {
100
+ const vs = makeVectorStore()
101
+ const kb = new DefaultKnowledgeBase({ name: 'kb', tenantId: TENANT }, vs, makeEmbedder())
102
+ const out = await kb.query({ text: 'hi' })
103
+ expect(out.mode).toBeDefined()
104
+ expect(vs.search).toHaveBeenCalled()
105
+ })
106
+ })
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Current-code invariants asserted (2026-04-21, ses_006 Phase 4):
3
+ *
4
+ * - `createRAGTool(config)` returns a `defineTool`-wrapped
5
+ * `knowledge_search` tool with `category: 'analysis'`,
6
+ * `permissions: ['network_access']`, read-only.
7
+ * - Knowledge base selection:
8
+ * - If `input.knowledge_base_id` is set, looks up by it.
9
+ * - Else uses `config.defaultKnowledgeBaseId`.
10
+ * - Else picks the first value from
11
+ * `config.knowledgeBases.values().next()`.
12
+ * - Returns `{success: false, error: ...}` if no knowledge base is
13
+ * resolved.
14
+ * - When the query returns no chunks, returns the literal
15
+ * "No relevant information found..." message.
16
+ * - When chunks exist, assembles them via `assembleRAGContext` and
17
+ * returns `{success: true, output: content, data: {sources, mode, durationMs}}`.
18
+ * - `top_k` override precedence: input > config.topK > 5.
19
+ */
20
+
21
+ import { describe, expect, it, vi } from 'vitest'
22
+
23
+ import type { ChunkId, DocumentId, KnowledgeBaseId, TenantId } from '../types/ids/index.js'
24
+ import type { KnowledgeBase, RetrievalResult, VectorSearchResult } from '../types/rag/index.js'
25
+ import type { ToolContext } from '../types/tool/index.js'
26
+
27
+ import { createRAGTool } from './rag-tool.js'
28
+
29
+ const TENANT = 't_1' as TenantId
30
+ const KB_A = 'kb_a' as KnowledgeBaseId
31
+ const KB_B = 'kb_b' as KnowledgeBaseId
32
+
33
+ function makeKB(retrievalResult: RetrievalResult): KnowledgeBase {
34
+ const kb = {
35
+ id: KB_A,
36
+ config: { id: KB_A, tenantId: TENANT },
37
+ ingest: vi.fn(),
38
+ remove: vi.fn(),
39
+ query: vi.fn(async () => retrievalResult),
40
+ clear: vi.fn(),
41
+ } as unknown as KnowledgeBase
42
+ return kb
43
+ }
44
+
45
+ function searchResult(content: string): VectorSearchResult {
46
+ return {
47
+ chunk: {
48
+ id: 'c_1' as ChunkId,
49
+ documentId: 'd_1' as DocumentId,
50
+ knowledgeBaseId: KB_A,
51
+ tenantId: TENANT,
52
+ content,
53
+ index: 0,
54
+ tokenCount: 0,
55
+ metadata: {},
56
+ createdAt: 0,
57
+ },
58
+ score: 0.9,
59
+ }
60
+ }
61
+
62
+ const ctx: ToolContext = {} as ToolContext
63
+
64
+ describe('createRAGTool', () => {
65
+ it('is named knowledge_search + read-only', () => {
66
+ const tool = createRAGTool({ knowledgeBases: new Map() })
67
+ expect(tool.name).toBe('knowledge_search')
68
+ expect(tool.isReadOnly?.({ query: 'x' })).toBe(true)
69
+ })
70
+
71
+ it('returns an error when no knowledge base is resolved', async () => {
72
+ const tool = createRAGTool({ knowledgeBases: new Map() })
73
+ const result = await tool.execute({ query: 'hi' }, ctx)
74
+ expect(result.success).toBe(false)
75
+ expect(result.error).toMatch(/Knowledge base not found/)
76
+ })
77
+
78
+ it('picks KB by explicit input.knowledge_base_id when set', async () => {
79
+ const kbA = makeKB({
80
+ chunks: [searchResult('from A')],
81
+ query: 'hi',
82
+ mode: 'vector',
83
+ durationMs: 1,
84
+ })
85
+ const kbB = makeKB({
86
+ chunks: [searchResult('from B')],
87
+ query: 'hi',
88
+ mode: 'vector',
89
+ durationMs: 1,
90
+ })
91
+ const tool = createRAGTool({
92
+ knowledgeBases: new Map([
93
+ [KB_A, kbA],
94
+ [KB_B, kbB],
95
+ ]),
96
+ })
97
+ const out = await tool.execute({ query: 'hi', knowledge_base_id: KB_B }, ctx)
98
+ expect(out.success).toBe(true)
99
+ expect(kbB.query).toHaveBeenCalled()
100
+ expect(kbA.query).not.toHaveBeenCalled()
101
+ })
102
+
103
+ it('falls back to defaultKnowledgeBaseId when input.knowledge_base_id is absent', async () => {
104
+ const kbA = makeKB({
105
+ chunks: [searchResult('from A')],
106
+ query: 'hi',
107
+ mode: 'vector',
108
+ durationMs: 1,
109
+ })
110
+ const kbB = makeKB({
111
+ chunks: [],
112
+ query: 'hi',
113
+ mode: 'vector',
114
+ durationMs: 1,
115
+ })
116
+ const tool = createRAGTool({
117
+ knowledgeBases: new Map([
118
+ [KB_A, kbA],
119
+ [KB_B, kbB],
120
+ ]),
121
+ defaultKnowledgeBaseId: KB_B,
122
+ })
123
+ await tool.execute({ query: 'hi' }, ctx)
124
+ expect(kbB.query).toHaveBeenCalled()
125
+ expect(kbA.query).not.toHaveBeenCalled()
126
+ })
127
+
128
+ it('returns "No relevant information..." when the query returns zero chunks', async () => {
129
+ const kb = makeKB({ chunks: [], query: 'hi', mode: 'vector', durationMs: 1 })
130
+ const tool = createRAGTool({ knowledgeBases: new Map([[KB_A, kb]]) })
131
+ const result = await tool.execute({ query: 'hi' }, ctx)
132
+ expect(result.success).toBe(true)
133
+ expect(result.output).toBe(
134
+ 'No relevant information found in the knowledge base for this query.',
135
+ )
136
+ })
137
+
138
+ it('returns assembled content + metadata on hit', async () => {
139
+ const kb = makeKB({
140
+ chunks: [searchResult('answer text')],
141
+ query: 'hi',
142
+ mode: 'vector',
143
+ durationMs: 42,
144
+ })
145
+ const tool = createRAGTool({ knowledgeBases: new Map([[KB_A, kb]]) })
146
+ const result = await tool.execute({ query: 'hi' }, ctx)
147
+ expect(result.success).toBe(true)
148
+ expect(result.output).toContain('answer text')
149
+ expect((result.data as { mode?: string })?.mode).toBe('vector')
150
+ expect((result.data as { durationMs?: number })?.durationMs).toBe(42)
151
+ })
152
+
153
+ it('top_k override: input > config.topK > 5 default', async () => {
154
+ const kb = makeKB({
155
+ chunks: [searchResult('x')],
156
+ query: 'hi',
157
+ mode: 'vector',
158
+ durationMs: 1,
159
+ })
160
+ const tool = createRAGTool({
161
+ knowledgeBases: new Map([[KB_A, kb]]),
162
+ topK: 8,
163
+ })
164
+ await tool.execute({ query: 'hi', top_k: 3 }, ctx)
165
+ expect(kb.query).toHaveBeenCalledWith(expect.objectContaining({ config: { topK: 3 } }))
166
+ })
167
+ })
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Current-code invariants asserted (2026-04-21, ses_006 Phase 4):
3
+ *
4
+ * - `DefaultRetriever.retrieve(query, scope, kbId)` dispatches by
5
+ * `effectiveConfig.mode` (query.config overrides instance config).
6
+ * - Query expansion: if `recentMessages` is present and non-empty,
7
+ * appends the last 3 messages joined with spaces after a
8
+ * `\n\nContext: ` marker. Otherwise passes `query.text` verbatim.
9
+ * - `vector` mode: embeds the (expanded) query once, forwards
10
+ * `{embedding, topK, tenantId, knowledgeBaseId, minScore}` to the
11
+ * store, returns store results unchanged.
12
+ * - `keyword` mode: fetches `topK * 2` results from vector search with
13
+ * `minScore: 0`, then rescores via BM25 against the query's
14
+ * tokenized form. Filters score > 0, sorts desc, slices to topK.
15
+ * Tokenization: lowercase, strip non-`\w\s`, split by whitespace,
16
+ * drop tokens length ≤ 1.
17
+ * - `hybrid` mode: runs vector + keyword in parallel, merges by
18
+ * `chunk.id`, weights vector by `alpha` and keyword by `1 - alpha`
19
+ * (default alpha = 0.7). Sorts desc, slices to topK.
20
+ * - Result shape: `{chunks, query, expandedQuery?, mode, durationMs}`.
21
+ * `expandedQuery` is omitted when equal to original `query.text`.
22
+ * - Unknown mode would hit the exhaustive throw (unreachable via
23
+ * types; not asserted).
24
+ */
25
+
26
+ import { describe, expect, it, vi } from 'vitest'
27
+
28
+ import type { ChunkId, DocumentId, KnowledgeBaseId, TenantId } from '../types/ids/index.js'
29
+ import type {
30
+ Chunk,
31
+ EmbeddingProvider,
32
+ TenantScope,
33
+ VectorSearchResult,
34
+ VectorStore,
35
+ } from '../types/rag/index.js'
36
+
37
+ import { DefaultRetriever } from './retriever.js'
38
+
39
+ const TENANT = 't_1' as TenantId
40
+ const KB = 'kb_1' as KnowledgeBaseId
41
+ const scope: TenantScope = { tenantId: TENANT }
42
+
43
+ function chunk(id: string, content: string): Chunk {
44
+ return {
45
+ id: id as ChunkId,
46
+ documentId: 'doc_1' as DocumentId,
47
+ knowledgeBaseId: KB,
48
+ tenantId: TENANT,
49
+ content,
50
+ index: 0,
51
+ tokenCount: 0,
52
+ embedding: [1, 0, 0],
53
+ metadata: {},
54
+ createdAt: 0,
55
+ }
56
+ }
57
+
58
+ function r(id: string, content: string, score: number): VectorSearchResult {
59
+ return { chunk: chunk(id, content), score }
60
+ }
61
+
62
+ function makeStore(searchResults: VectorSearchResult[]): VectorStore {
63
+ return {
64
+ search: vi.fn(async () => searchResults),
65
+ upsert: vi.fn(),
66
+ delete: vi.fn(),
67
+ deleteByDocument: vi.fn(),
68
+ deleteByKnowledgeBase: vi.fn(),
69
+ }
70
+ }
71
+
72
+ function makeEmbedder(): EmbeddingProvider {
73
+ return {
74
+ id: 'mock',
75
+ model: 'x',
76
+ dimensions: 3,
77
+ embed: vi.fn(async (texts: string[]) => texts.map(() => [1, 0, 0])),
78
+ embedQuery: vi.fn(async () => [1, 0, 0]),
79
+ }
80
+ }
81
+
82
+ describe('DefaultRetriever — vector mode', () => {
83
+ it('forwards embedded query + topK + tenant + minScore to store', async () => {
84
+ const store = makeStore([r('a', 'x', 0.9)])
85
+ const emb = makeEmbedder()
86
+ const retriever = new DefaultRetriever(store, emb, {
87
+ mode: 'vector',
88
+ topK: 3,
89
+ minScore: 0.2,
90
+ })
91
+ await retriever.retrieve({ text: 'hi' }, scope, KB)
92
+ expect(store.search).toHaveBeenCalledWith(
93
+ expect.objectContaining({
94
+ embedding: [1, 0, 0],
95
+ topK: 3,
96
+ tenantId: TENANT,
97
+ knowledgeBaseId: KB,
98
+ minScore: 0.2,
99
+ }),
100
+ )
101
+ })
102
+
103
+ it('returns mode + durationMs + chunks without mutation', async () => {
104
+ const results = [r('a', 'x', 0.9)]
105
+ const store = makeStore(results)
106
+ const retriever = new DefaultRetriever(store, makeEmbedder(), {
107
+ mode: 'vector',
108
+ topK: 5,
109
+ })
110
+ const out = await retriever.retrieve({ text: 'hi' }, scope)
111
+ expect(out.mode).toBe('vector')
112
+ expect(out.chunks).toEqual(results)
113
+ expect(typeof out.durationMs).toBe('number')
114
+ })
115
+ })
116
+
117
+ describe('DefaultRetriever — query expansion', () => {
118
+ it('when recentMessages exists, appends "Context: " + last 3 joined', async () => {
119
+ const store = makeStore([])
120
+ const emb = makeEmbedder()
121
+ const retriever = new DefaultRetriever(store, emb, { mode: 'vector', topK: 3 })
122
+
123
+ const out = await retriever.retrieve(
124
+ { text: 'query', recentMessages: ['m1', 'm2', 'm3', 'm4'] },
125
+ scope,
126
+ )
127
+ expect(out.expandedQuery).toBe('query\n\nContext: m2 m3 m4')
128
+ expect(emb.embedQuery).toHaveBeenCalledWith('query\n\nContext: m2 m3 m4')
129
+ })
130
+
131
+ it('when no recentMessages, expandedQuery is undefined (original == expanded)', async () => {
132
+ const store = makeStore([])
133
+ const retriever = new DefaultRetriever(store, makeEmbedder(), {
134
+ mode: 'vector',
135
+ topK: 3,
136
+ })
137
+ const out = await retriever.retrieve({ text: 'plain' }, scope)
138
+ expect(out.expandedQuery).toBeUndefined()
139
+ })
140
+ })
141
+
142
+ describe('DefaultRetriever — keyword mode', () => {
143
+ it('fetches 2*topK from store, rescores via BM25, slices to topK', async () => {
144
+ // 6 candidate chunks (topK * 2 with topK=3) — retriever requests 2*topK with minScore 0.
145
+ const store = makeStore([
146
+ r('a', 'apple orange', 0.1),
147
+ r('b', 'banana apple apple', 0.1),
148
+ r('c', 'unrelated text', 0.1),
149
+ r('d', 'apple apple apple', 0.1),
150
+ r('e', 'nothing here', 0.1),
151
+ r('f', 'apple', 0.1),
152
+ ])
153
+ const retriever = new DefaultRetriever(store, makeEmbedder(), {
154
+ mode: 'keyword',
155
+ topK: 3,
156
+ })
157
+ const out = await retriever.retrieve({ text: 'apple' }, scope)
158
+ expect(out.chunks.length).toBeLessThanOrEqual(3)
159
+ expect(out.chunks.every((c) => c.score > 0)).toBe(true)
160
+ // Tokenization: 'nothing' + 'here' — doesn't contain 'apple' → score 0 → filtered.
161
+ expect(out.chunks.every((c) => c.chunk.id !== ('e' as ChunkId))).toBe(true)
162
+ })
163
+
164
+ it('query store is called with minScore: 0 + topK*2', async () => {
165
+ const store = makeStore([r('a', 'apple', 0.5)])
166
+ const retriever = new DefaultRetriever(store, makeEmbedder(), {
167
+ mode: 'keyword',
168
+ topK: 2,
169
+ })
170
+ await retriever.retrieve({ text: 'apple' }, scope)
171
+ expect(store.search).toHaveBeenCalledWith(expect.objectContaining({ topK: 4, minScore: 0 }))
172
+ })
173
+ })
174
+
175
+ describe('DefaultRetriever — hybrid mode', () => {
176
+ it('merges by chunk.id, weighted by alpha (default 0.7)', async () => {
177
+ const store = makeStore([r('a', 'apple', 1), r('b', 'banana', 0.5)])
178
+ const retriever = new DefaultRetriever(store, makeEmbedder(), {
179
+ mode: 'hybrid',
180
+ topK: 5,
181
+ })
182
+ const out = await retriever.retrieve({ text: 'apple' }, scope)
183
+ // Vector side: a=0.7, b=0.35. Keyword side (BM25): a matches → >0; b=0.
184
+ // Hybrid: a stays high; b gets only vector weight.
185
+ expect(out.chunks[0]?.chunk.id).toBe('a')
186
+ })
187
+
188
+ it('chunks in only one side still appear with partial weight', async () => {
189
+ const store = makeStore([r('a', 'apple', 1)])
190
+ const retriever = new DefaultRetriever(store, makeEmbedder(), {
191
+ mode: 'hybrid',
192
+ topK: 5,
193
+ })
194
+ const out = await retriever.retrieve({ text: 'apple' }, scope)
195
+ expect(out.chunks.length).toBe(1)
196
+ expect(out.chunks[0]?.score).toBeGreaterThan(0)
197
+ })
198
+ })
199
+
200
+ describe('DefaultRetriever — config merge', () => {
201
+ it('query.config overrides instance config per-call', async () => {
202
+ const store = makeStore([r('a', 'x', 0.9)])
203
+ const retriever = new DefaultRetriever(store, makeEmbedder(), {
204
+ mode: 'vector',
205
+ topK: 5,
206
+ })
207
+ await retriever.retrieve({ text: 'hi', config: { topK: 1 } }, scope)
208
+ expect(store.search).toHaveBeenCalledWith(expect.objectContaining({ topK: 1 }))
209
+ })
210
+ })