@namzu/sdk 0.4.2 → 0.4.4
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.
- package/CHANGELOG.md +46 -0
- package/dist/advisory/context.test.d.ts +16 -0
- package/dist/advisory/context.test.d.ts.map +1 -0
- package/dist/advisory/context.test.js +92 -0
- package/dist/advisory/context.test.js.map +1 -0
- package/dist/advisory/evaluator.test.d.ts +34 -0
- package/dist/advisory/evaluator.test.d.ts.map +1 -0
- package/dist/advisory/evaluator.test.js +172 -0
- package/dist/advisory/evaluator.test.js.map +1 -0
- package/dist/advisory/executor.test.d.ts +35 -0
- package/dist/advisory/executor.test.d.ts.map +1 -0
- package/dist/advisory/executor.test.js +233 -0
- package/dist/advisory/executor.test.js.map +1 -0
- package/dist/advisory/registry.test.d.ts +16 -0
- package/dist/advisory/registry.test.d.ts.map +1 -0
- package/dist/advisory/registry.test.js +62 -0
- package/dist/advisory/registry.test.js.map +1 -0
- package/dist/bridge/a2a/agent-card.test.d.ts +24 -0
- package/dist/bridge/a2a/agent-card.test.d.ts.map +1 -0
- package/dist/bridge/a2a/agent-card.test.js +118 -0
- package/dist/bridge/a2a/agent-card.test.js.map +1 -0
- package/dist/bridge/a2a/mapper.test.d.ts +29 -0
- package/dist/bridge/a2a/mapper.test.d.ts.map +1 -0
- package/dist/bridge/a2a/mapper.test.js +265 -0
- package/dist/bridge/a2a/mapper.test.js.map +1 -0
- package/dist/bridge/a2a/message.test.d.ts +20 -0
- package/dist/bridge/a2a/message.test.d.ts.map +1 -0
- package/dist/bridge/a2a/message.test.js +116 -0
- package/dist/bridge/a2a/message.test.js.map +1 -0
- package/dist/bridge/a2a/task.test.d.ts +29 -0
- package/dist/bridge/a2a/task.test.d.ts.map +1 -0
- package/dist/bridge/a2a/task.test.js +198 -0
- package/dist/bridge/a2a/task.test.js.map +1 -0
- package/dist/bridge/mcp/connector/adapter.test.d.ts +27 -0
- package/dist/bridge/mcp/connector/adapter.test.d.ts.map +1 -0
- package/dist/bridge/mcp/connector/adapter.test.js +203 -0
- package/dist/bridge/mcp/connector/adapter.test.js.map +1 -0
- package/dist/bridge/sse/mapper.test.d.ts +27 -0
- package/dist/bridge/sse/mapper.test.d.ts.map +1 -0
- package/dist/bridge/sse/mapper.test.js +271 -0
- package/dist/bridge/sse/mapper.test.js.map +1 -0
- package/dist/bridge/tools/connector/adapter.d.ts +2 -2
- package/dist/bridge/tools/connector/adapter.test.d.ts +28 -0
- package/dist/bridge/tools/connector/adapter.test.d.ts.map +1 -0
- package/dist/bridge/tools/connector/adapter.test.js +182 -0
- package/dist/bridge/tools/connector/adapter.test.js.map +1 -0
- package/dist/bridge/tools/connector/definitions.test.d.ts +23 -0
- package/dist/bridge/tools/connector/definitions.test.d.ts.map +1 -0
- package/dist/bridge/tools/connector/definitions.test.js +158 -0
- package/dist/bridge/tools/connector/definitions.test.js.map +1 -0
- package/dist/bridge/tools/connector/router.test.d.ts +21 -0
- package/dist/bridge/tools/connector/router.test.d.ts.map +1 -0
- package/dist/bridge/tools/connector/router.test.js +139 -0
- package/dist/bridge/tools/connector/router.test.js.map +1 -0
- package/dist/bus/breaker.test.d.ts +41 -0
- package/dist/bus/breaker.test.d.ts.map +1 -0
- package/dist/bus/breaker.test.js +242 -0
- package/dist/bus/breaker.test.js.map +1 -0
- package/dist/bus/index.d.ts +3 -1
- package/dist/bus/index.d.ts.map +1 -1
- package/dist/bus/index.js +18 -11
- package/dist/bus/index.js.map +1 -1
- package/dist/bus/index.test.d.ts +25 -0
- package/dist/bus/index.test.d.ts.map +1 -0
- package/dist/bus/index.test.js +151 -0
- package/dist/bus/index.test.js.map +1 -0
- package/dist/bus/lock.test.d.ts +44 -0
- package/dist/bus/lock.test.d.ts.map +1 -0
- package/dist/bus/lock.test.js +226 -0
- package/dist/bus/lock.test.js.map +1 -0
- package/dist/bus/ownership.test.d.ts +26 -0
- package/dist/bus/ownership.test.d.ts.map +1 -0
- package/dist/bus/ownership.test.js +205 -0
- package/dist/bus/ownership.test.js.map +1 -0
- package/dist/config/runtime.d.ts +28 -28
- package/dist/connector/BaseConnector.test.d.ts +21 -0
- package/dist/connector/BaseConnector.test.d.ts.map +1 -0
- package/dist/connector/BaseConnector.test.js +108 -0
- package/dist/connector/BaseConnector.test.js.map +1 -0
- package/dist/connector/builtins/http.test.d.ts +30 -0
- package/dist/connector/builtins/http.test.d.ts.map +1 -0
- package/dist/connector/builtins/http.test.js +232 -0
- package/dist/connector/builtins/http.test.js.map +1 -0
- package/dist/connector/builtins/webhook.test.d.ts +20 -0
- package/dist/connector/builtins/webhook.test.d.ts.map +1 -0
- package/dist/connector/builtins/webhook.test.js +113 -0
- package/dist/connector/builtins/webhook.test.js.map +1 -0
- package/dist/connector/execution/factory.test.d.ts +16 -0
- package/dist/connector/execution/factory.test.d.ts.map +1 -0
- package/dist/connector/execution/factory.test.js +64 -0
- package/dist/connector/execution/factory.test.js.map +1 -0
- package/dist/connector/execution/remote.test.d.ts +16 -0
- package/dist/connector/execution/remote.test.d.ts.map +1 -0
- package/dist/connector/execution/remote.test.js +53 -0
- package/dist/connector/execution/remote.test.js.map +1 -0
- package/dist/connector/mcp/adapter.test.d.ts +34 -0
- package/dist/connector/mcp/adapter.test.d.ts.map +1 -0
- package/dist/connector/mcp/adapter.test.js +199 -0
- package/dist/connector/mcp/adapter.test.js.map +1 -0
- package/dist/probe/context.d.ts +8 -0
- package/dist/probe/context.d.ts.map +1 -0
- package/dist/probe/context.js +7 -0
- package/dist/probe/context.js.map +1 -0
- package/dist/probe/errors.d.ts +12 -0
- package/dist/probe/errors.d.ts.map +1 -0
- package/dist/probe/errors.js +21 -0
- package/dist/probe/errors.js.map +1 -0
- package/dist/probe/index.d.ts +5 -0
- package/dist/probe/index.d.ts.map +1 -0
- package/dist/probe/index.js +4 -0
- package/dist/probe/index.js.map +1 -0
- package/dist/probe/registry.d.ts +24 -0
- package/dist/probe/registry.d.ts.map +1 -0
- package/dist/probe/registry.js +228 -0
- package/dist/probe/registry.js.map +1 -0
- package/dist/probe/registry.test.d.ts +7 -0
- package/dist/probe/registry.test.d.ts.map +1 -0
- package/dist/probe/registry.test.js +310 -0
- package/dist/probe/registry.test.js.map +1 -0
- package/dist/provider/instrumentation.d.ts +9 -0
- package/dist/provider/instrumentation.d.ts.map +1 -0
- package/dist/provider/instrumentation.js +104 -0
- package/dist/provider/instrumentation.js.map +1 -0
- package/dist/provider/instrumentation.test.d.ts +2 -0
- package/dist/provider/instrumentation.test.d.ts.map +1 -0
- package/dist/provider/instrumentation.test.js +152 -0
- package/dist/provider/instrumentation.test.js.map +1 -0
- package/dist/public-runtime.d.ts +5 -0
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +4 -0
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +3 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/rag/chunking.test.d.ts +20 -0
- package/dist/rag/chunking.test.d.ts.map +1 -0
- package/dist/rag/chunking.test.js +92 -0
- package/dist/rag/chunking.test.js.map +1 -0
- package/dist/rag/context-assembler.test.d.ts +19 -0
- package/dist/rag/context-assembler.test.d.ts.map +1 -0
- package/dist/rag/context-assembler.test.js +98 -0
- package/dist/rag/context-assembler.test.js.map +1 -0
- package/dist/rag/embedding.test.d.ts +19 -0
- package/dist/rag/embedding.test.d.ts.map +1 -0
- package/dist/rag/embedding.test.js +115 -0
- package/dist/rag/embedding.test.js.map +1 -0
- package/dist/rag/ingestion.test.d.ts +22 -0
- package/dist/rag/ingestion.test.d.ts.map +1 -0
- package/dist/rag/ingestion.test.js +99 -0
- package/dist/rag/ingestion.test.js.map +1 -0
- package/dist/rag/knowledge-base.test.d.ts +17 -0
- package/dist/rag/knowledge-base.test.d.ts.map +1 -0
- package/dist/rag/knowledge-base.test.js +77 -0
- package/dist/rag/knowledge-base.test.js.map +1 -0
- package/dist/rag/rag-tool.test.d.ts +21 -0
- package/dist/rag/rag-tool.test.d.ts.map +1 -0
- package/dist/rag/rag-tool.test.js +149 -0
- package/dist/rag/rag-tool.test.js.map +1 -0
- package/dist/rag/retriever.test.d.ts +26 -0
- package/dist/rag/retriever.test.d.ts.map +1 -0
- package/dist/rag/retriever.test.js +180 -0
- package/dist/rag/retriever.test.js.map +1 -0
- package/dist/rag/vector-store.test.d.ts +38 -0
- package/dist/rag/vector-store.test.d.ts.map +1 -0
- package/dist/rag/vector-store.test.js +175 -0
- package/dist/rag/vector-store.test.js.map +1 -0
- package/dist/registry/ManagedRegistry.test.d.ts +21 -0
- package/dist/registry/ManagedRegistry.test.d.ts.map +1 -0
- package/dist/registry/ManagedRegistry.test.js +98 -0
- package/dist/registry/ManagedRegistry.test.js.map +1 -0
- package/dist/registry/Registry.test.d.ts +18 -0
- package/dist/registry/Registry.test.d.ts.map +1 -0
- package/dist/registry/Registry.test.js +79 -0
- package/dist/registry/Registry.test.js.map +1 -0
- package/dist/registry/agent/definitions.test.d.ts +15 -0
- package/dist/registry/agent/definitions.test.d.ts.map +1 -0
- package/dist/registry/agent/definitions.test.js +84 -0
- package/dist/registry/agent/definitions.test.js.map +1 -0
- package/dist/registry/connector/definitions.test.d.ts +13 -0
- package/dist/registry/connector/definitions.test.d.ts.map +1 -0
- package/dist/registry/connector/definitions.test.js +41 -0
- package/dist/registry/connector/definitions.test.js.map +1 -0
- package/dist/registry/connector/scoped.test.d.ts +21 -0
- package/dist/registry/connector/scoped.test.d.ts.map +1 -0
- package/dist/registry/connector/scoped.test.js +115 -0
- package/dist/registry/connector/scoped.test.js.map +1 -0
- package/dist/registry/plugin/index.test.d.ts +12 -0
- package/dist/registry/plugin/index.test.d.ts.map +1 -0
- package/dist/registry/plugin/index.test.js +69 -0
- package/dist/registry/plugin/index.test.js.map +1 -0
- package/dist/registry/tool/execute.test.d.ts +42 -0
- package/dist/registry/tool/execute.test.d.ts.map +1 -0
- package/dist/registry/tool/execute.test.js +281 -0
- package/dist/registry/tool/execute.test.js.map +1 -0
- package/dist/runtime/query/events.d.ts +3 -1
- package/dist/runtime/query/events.d.ts.map +1 -1
- package/dist/runtime/query/events.js +6 -1
- package/dist/runtime/query/events.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +3 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +30 -1
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/iteration/phases/advisory.test.d.ts +42 -0
- package/dist/runtime/query/iteration/phases/advisory.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/advisory.test.js +334 -0
- package/dist/runtime/query/iteration/phases/advisory.test.js.map +1 -0
- package/dist/test-setup.d.ts +22 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +23 -0
- package/dist/test-setup.js.map +1 -0
- package/dist/types/bus/index.d.ts +46 -2
- package/dist/types/bus/index.d.ts.map +1 -1
- package/dist/types/doctor/check.d.ts +41 -0
- package/dist/types/doctor/check.d.ts.map +1 -0
- package/dist/types/doctor/check.js +2 -0
- package/dist/types/doctor/check.js.map +1 -0
- package/dist/types/doctor/index.d.ts +2 -0
- package/dist/types/doctor/index.d.ts.map +1 -0
- package/dist/types/doctor/index.js +2 -0
- package/dist/types/doctor/index.js.map +1 -0
- package/dist/types/probe/event-kind.d.ts +6 -0
- package/dist/types/probe/event-kind.d.ts.map +1 -0
- package/dist/types/probe/event-kind.js +2 -0
- package/dist/types/probe/event-kind.js.map +1 -0
- package/dist/types/probe/event-of.d.ts +5 -0
- package/dist/types/probe/event-of.d.ts.map +1 -0
- package/dist/types/probe/event-of.js +2 -0
- package/dist/types/probe/event-of.js.map +1 -0
- package/dist/types/probe/index.d.ts +4 -0
- package/dist/types/probe/index.d.ts.map +1 -0
- package/dist/types/probe/index.js +2 -0
- package/dist/types/probe/index.js.map +1 -0
- package/dist/types/probe/registry.d.ts +27 -0
- package/dist/types/probe/registry.d.ts.map +1 -0
- package/dist/types/probe/registry.js +2 -0
- package/dist/types/probe/registry.js.map +1 -0
- package/dist/utils/logger.d.ts +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +5 -0
- package/dist/utils/logger.js.map +1 -1
- package/dist/vault/instrumentation.d.ts +11 -0
- package/dist/vault/instrumentation.d.ts.map +1 -0
- package/dist/vault/instrumentation.js +32 -0
- package/dist/vault/instrumentation.js.map +1 -0
- package/dist/vault/instrumentation.test.d.ts +2 -0
- package/dist/vault/instrumentation.test.d.ts.map +1 -0
- package/dist/vault/instrumentation.test.js +80 -0
- package/dist/vault/instrumentation.test.js.map +1 -0
- package/package.json +4 -1
- package/src/advisory/context.test.ts +109 -0
- package/src/advisory/evaluator.test.ts +192 -0
- package/src/advisory/executor.test.ts +272 -0
- package/src/advisory/registry.test.ts +75 -0
- package/src/bridge/a2a/agent-card.test.ts +140 -0
- package/src/bridge/a2a/mapper.test.ts +293 -0
- package/src/bridge/a2a/message.test.ts +138 -0
- package/src/bridge/a2a/task.test.ts +235 -0
- package/src/bridge/mcp/connector/adapter.test.ts +230 -0
- package/src/bridge/sse/mapper.test.ts +422 -0
- package/src/bridge/tools/connector/adapter.test.ts +224 -0
- package/src/bridge/tools/connector/definitions.test.ts +183 -0
- package/src/bridge/tools/connector/router.test.ts +159 -0
- package/src/bus/breaker.test.ts +274 -0
- package/src/bus/index.test.ts +183 -0
- package/src/bus/index.ts +21 -10
- package/src/bus/lock.test.ts +265 -0
- package/src/bus/ownership.test.ts +243 -0
- package/src/connector/BaseConnector.test.ts +130 -0
- package/src/connector/builtins/http.test.ts +290 -0
- package/src/connector/builtins/webhook.test.ts +138 -0
- package/src/connector/execution/factory.test.ts +75 -0
- package/src/connector/execution/remote.test.ts +63 -0
- package/src/connector/mcp/adapter.test.ts +249 -0
- package/src/probe/context.ts +14 -0
- package/src/probe/errors.ts +27 -0
- package/src/probe/index.ts +4 -0
- package/src/probe/registry.test.ts +480 -0
- package/src/probe/registry.ts +276 -0
- package/src/provider/instrumentation.test.ts +192 -0
- package/src/provider/instrumentation.ts +139 -0
- package/src/public-runtime.ts +17 -0
- package/src/public-types.ts +3 -0
- package/src/rag/chunking.test.ts +107 -0
- package/src/rag/context-assembler.test.ts +114 -0
- package/src/rag/embedding.test.ts +130 -0
- package/src/rag/ingestion.test.ts +114 -0
- package/src/rag/knowledge-base.test.ts +106 -0
- package/src/rag/rag-tool.test.ts +167 -0
- package/src/rag/retriever.test.ts +210 -0
- package/src/rag/vector-store.test.ts +196 -0
- package/src/registry/ManagedRegistry.test.ts +118 -0
- package/src/registry/Registry.test.ts +91 -0
- package/src/registry/agent/definitions.test.ts +100 -0
- package/src/registry/connector/definitions.test.ts +51 -0
- package/src/registry/connector/scoped.test.ts +129 -0
- package/src/registry/plugin/index.test.ts +85 -0
- package/src/registry/tool/execute.test.ts +330 -0
- package/src/runtime/query/events.ts +6 -1
- package/src/runtime/query/executor.ts +34 -0
- package/src/runtime/query/iteration/phases/advisory.test.ts +412 -0
- package/src/test-setup.ts +24 -0
- package/src/types/bus/index.ts +54 -2
- package/src/types/doctor/check.ts +53 -0
- package/src/types/doctor/index.ts +9 -0
- package/src/types/probe/event-kind.ts +8 -0
- package/src/types/probe/event-of.ts +3 -0
- package/src/types/probe/index.ts +11 -0
- package/src/types/probe/registry.ts +36 -0
- package/src/utils/logger.ts +6 -1
- package/src/vault/instrumentation.test.ts +98 -0
- package/src/vault/instrumentation.ts +56 -0
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Current-code invariants asserted (2026-04-21, ses_006 Phase 4):
|
|
3
|
+
*
|
|
4
|
+
* This file doubles as the VectorStore contract — the tests here run
|
|
5
|
+
* against `InMemoryVectorStore` as the reference implementation, and
|
|
6
|
+
* are the spec a future `@namzu/vector-store-conformance` package will
|
|
7
|
+
* be built around (D4 ratification + Q5 refinement: contract first,
|
|
8
|
+
* not "whatever the reference happens to do").
|
|
9
|
+
*
|
|
10
|
+
* **VectorStore contract (derived from current code, 2026-04-21):**
|
|
11
|
+
*
|
|
12
|
+
* - `upsert(chunks[])`: stores each chunk keyed by `chunk.id`. Re-upsert
|
|
13
|
+
* of a chunk with the same id REPLACES — no merge, no error. Tenant
|
|
14
|
+
* scoping is carried on each chunk, not enforced at upsert (the
|
|
15
|
+
* caller is trusted).
|
|
16
|
+
* - `search(query)`: returns `VectorSearchResult[]`:
|
|
17
|
+
* - Filters by `tenantId` (EXACT match).
|
|
18
|
+
* - If `knowledgeBaseId` is set, filters by that too.
|
|
19
|
+
* - Skips chunks with `embedding === undefined`.
|
|
20
|
+
* - Applies `filter` as metadata-equality AND across the whole
|
|
21
|
+
* filter object.
|
|
22
|
+
* - Computes similarity via `cosineSimilarity(query, chunk.embedding)`.
|
|
23
|
+
* - If `minScore` is set, drops scores below it.
|
|
24
|
+
* - Sorts descending by score and returns the top `topK` results.
|
|
25
|
+
* - `delete(chunkIds[])`: deletes by exact chunk id. Unknown ids are
|
|
26
|
+
* silently ignored.
|
|
27
|
+
* - `deleteByDocument(documentId)`: NOT tenant-scoped in the
|
|
28
|
+
* current API — deletes every chunk with matching `documentId`
|
|
29
|
+
* ACROSS tenants. This is an asymmetry with `search` /
|
|
30
|
+
* `deleteByKnowledgeBase` and is intentional to pin (Codex #9 flag).
|
|
31
|
+
* - `deleteByKnowledgeBase(kbId, tenantId)`: tenant-scoped deletion
|
|
32
|
+
* by knowledge base.
|
|
33
|
+
* - `cosineSimilarity(a, b)`: returns 0 when arrays differ in length
|
|
34
|
+
* OR when either has zero norm. Otherwise returns the standard
|
|
35
|
+
* dot-product / (‖a‖·‖b‖) result.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { describe, expect, it } from 'vitest'
|
|
39
|
+
|
|
40
|
+
import type { ChunkId, DocumentId, KnowledgeBaseId, TenantId } from '../types/ids/index.js'
|
|
41
|
+
import type { Chunk, VectorStoreQuery } from '../types/rag/index.js'
|
|
42
|
+
|
|
43
|
+
import { InMemoryVectorStore, cosineSimilarity } from './vector-store.js'
|
|
44
|
+
|
|
45
|
+
const T1 = 'tenant_1' as TenantId
|
|
46
|
+
const T2 = 'tenant_2' as TenantId
|
|
47
|
+
const KB1 = 'kb_1' as KnowledgeBaseId
|
|
48
|
+
const KB2 = 'kb_2' as KnowledgeBaseId
|
|
49
|
+
const D1 = 'doc_1' as DocumentId
|
|
50
|
+
const D2 = 'doc_2' as DocumentId
|
|
51
|
+
|
|
52
|
+
function chunk(id: string, overrides: Partial<Chunk> = {}): Chunk {
|
|
53
|
+
return {
|
|
54
|
+
id: id as ChunkId,
|
|
55
|
+
documentId: D1,
|
|
56
|
+
knowledgeBaseId: KB1,
|
|
57
|
+
tenantId: T1,
|
|
58
|
+
content: `content of ${id}`,
|
|
59
|
+
index: 0,
|
|
60
|
+
tokenCount: 10,
|
|
61
|
+
embedding: [1, 0, 0],
|
|
62
|
+
metadata: {},
|
|
63
|
+
createdAt: Date.now(),
|
|
64
|
+
...overrides,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const baseQuery: VectorStoreQuery = {
|
|
69
|
+
embedding: [1, 0, 0],
|
|
70
|
+
topK: 10,
|
|
71
|
+
tenantId: T1,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe('InMemoryVectorStore — upsert', () => {
|
|
75
|
+
it('stores chunks by id; re-upsert replaces', async () => {
|
|
76
|
+
const s = new InMemoryVectorStore()
|
|
77
|
+
await s.upsert([chunk('c1', { content: 'v1' })])
|
|
78
|
+
await s.upsert([chunk('c1', { content: 'v2' })])
|
|
79
|
+
const results = await s.search(baseQuery)
|
|
80
|
+
expect(results).toHaveLength(1)
|
|
81
|
+
expect(results[0]?.chunk.content).toBe('v2')
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('InMemoryVectorStore — search', () => {
|
|
86
|
+
it('enforces tenant isolation on the query path', async () => {
|
|
87
|
+
const s = new InMemoryVectorStore()
|
|
88
|
+
await s.upsert([
|
|
89
|
+
chunk('a', { tenantId: T1, embedding: [1, 0, 0] }),
|
|
90
|
+
chunk('b', { tenantId: T2, embedding: [1, 0, 0] }),
|
|
91
|
+
])
|
|
92
|
+
const results = await s.search({ ...baseQuery, tenantId: T1 })
|
|
93
|
+
expect(results.map((r) => r.chunk.id)).toEqual(['a'])
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('filters by knowledgeBaseId when provided', async () => {
|
|
97
|
+
const s = new InMemoryVectorStore()
|
|
98
|
+
await s.upsert([chunk('a', { knowledgeBaseId: KB1 }), chunk('b', { knowledgeBaseId: KB2 })])
|
|
99
|
+
const results = await s.search({ ...baseQuery, knowledgeBaseId: KB1 })
|
|
100
|
+
expect(results.map((r) => r.chunk.id)).toEqual(['a'])
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('skips chunks with no embedding', async () => {
|
|
104
|
+
const s = new InMemoryVectorStore()
|
|
105
|
+
await s.upsert([chunk('a', { embedding: [1, 0, 0] }), chunk('b', { embedding: undefined })])
|
|
106
|
+
const results = await s.search(baseQuery)
|
|
107
|
+
expect(results.map((r) => r.chunk.id)).toEqual(['a'])
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('applies AND-of-equalities across filter keys', async () => {
|
|
111
|
+
const s = new InMemoryVectorStore()
|
|
112
|
+
await s.upsert([
|
|
113
|
+
chunk('a', { metadata: { lang: 'en', year: 2026 } }),
|
|
114
|
+
chunk('b', { metadata: { lang: 'en', year: 2024 } }),
|
|
115
|
+
chunk('c', { metadata: { lang: 'tr', year: 2026 } }),
|
|
116
|
+
])
|
|
117
|
+
const results = await s.search({
|
|
118
|
+
...baseQuery,
|
|
119
|
+
filter: { lang: 'en', year: 2026 },
|
|
120
|
+
})
|
|
121
|
+
expect(results.map((r) => r.chunk.id)).toEqual(['a'])
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('applies minScore cutoff', async () => {
|
|
125
|
+
const s = new InMemoryVectorStore()
|
|
126
|
+
await s.upsert([
|
|
127
|
+
chunk('same', { embedding: [1, 0, 0] }),
|
|
128
|
+
chunk('orthogonal', { embedding: [0, 1, 0] }),
|
|
129
|
+
])
|
|
130
|
+
const results = await s.search({ ...baseQuery, minScore: 0.5 })
|
|
131
|
+
expect(results.map((r) => r.chunk.id)).toEqual(['same'])
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('sorts descending by score and slices to topK', async () => {
|
|
135
|
+
const s = new InMemoryVectorStore()
|
|
136
|
+
await s.upsert([
|
|
137
|
+
chunk('strong', { embedding: [1, 0, 0] }),
|
|
138
|
+
chunk('medium', { embedding: [0.5, 0.5, 0] }),
|
|
139
|
+
chunk('weak', { embedding: [0.1, 0.9, 0] }),
|
|
140
|
+
])
|
|
141
|
+
const results = await s.search({ ...baseQuery, topK: 2 })
|
|
142
|
+
expect(results.map((r) => r.chunk.id)).toEqual(['strong', 'medium'])
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
describe('InMemoryVectorStore — delete', () => {
|
|
147
|
+
it('delete(chunkIds) removes each id; unknown ids are silent', async () => {
|
|
148
|
+
const s = new InMemoryVectorStore()
|
|
149
|
+
await s.upsert([chunk('a'), chunk('b')])
|
|
150
|
+
await s.delete(['a' as ChunkId, 'missing' as ChunkId])
|
|
151
|
+
expect((await s.search(baseQuery)).map((r) => r.chunk.id)).toEqual(['b'])
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('deleteByDocument removes every chunk of the document ACROSS tenants (asymmetry per Codex #9)', async () => {
|
|
155
|
+
const s = new InMemoryVectorStore()
|
|
156
|
+
await s.upsert([
|
|
157
|
+
chunk('a', { documentId: D1, tenantId: T1 }),
|
|
158
|
+
chunk('b', { documentId: D1, tenantId: T2 }),
|
|
159
|
+
chunk('c', { documentId: D2, tenantId: T1 }),
|
|
160
|
+
])
|
|
161
|
+
await s.deleteByDocument(D1)
|
|
162
|
+
// Both D1 chunks gone even though we didn't pass a tenant.
|
|
163
|
+
expect((await s.search({ ...baseQuery, tenantId: T1 })).map((r) => r.chunk.id)).toEqual(['c'])
|
|
164
|
+
expect(await s.search({ ...baseQuery, tenantId: T2 })).toEqual([])
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('deleteByKnowledgeBase is tenant-scoped', async () => {
|
|
168
|
+
const s = new InMemoryVectorStore()
|
|
169
|
+
await s.upsert([
|
|
170
|
+
chunk('a', { knowledgeBaseId: KB1, tenantId: T1 }),
|
|
171
|
+
chunk('b', { knowledgeBaseId: KB1, tenantId: T2 }),
|
|
172
|
+
])
|
|
173
|
+
await s.deleteByKnowledgeBase(KB1, T1)
|
|
174
|
+
expect(await s.search({ ...baseQuery, tenantId: T1 })).toEqual([])
|
|
175
|
+
expect((await s.search({ ...baseQuery, tenantId: T2 })).map((r) => r.chunk.id)).toEqual(['b'])
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
describe('cosineSimilarity', () => {
|
|
180
|
+
it('is 1 for identical non-zero vectors', () => {
|
|
181
|
+
expect(cosineSimilarity([1, 2, 3], [1, 2, 3])).toBeCloseTo(1)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('is 0 for orthogonal vectors', () => {
|
|
185
|
+
expect(cosineSimilarity([1, 0], [0, 1])).toBe(0)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('is 0 when vectors differ in length', () => {
|
|
189
|
+
expect(cosineSimilarity([1, 0], [1, 0, 0])).toBe(0)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('is 0 when either vector has zero norm', () => {
|
|
193
|
+
expect(cosineSimilarity([0, 0], [1, 1])).toBe(0)
|
|
194
|
+
expect(cosineSimilarity([1, 1], [0, 0])).toBe(0)
|
|
195
|
+
})
|
|
196
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Current-code invariants asserted (2026-04-21, ses_006 Phase 3):
|
|
3
|
+
*
|
|
4
|
+
* - `ManagedRegistry` extends `Registry` with a component-named
|
|
5
|
+
* logger + two optional id-extraction strategies: `idField` or
|
|
6
|
+
* `computeId`. `computeId` takes precedence when both are set.
|
|
7
|
+
* - `register(id, item)` (2-arg): throws when `item` is missing;
|
|
8
|
+
* warn-logs + overwrites on duplicate id (no typed error).
|
|
9
|
+
* - `register(item)` (1-arg): extracts id via computeId/idField;
|
|
10
|
+
* throws when neither is configured.
|
|
11
|
+
* - `register(items[])`: batch-registers (recursively calls the
|
|
12
|
+
* single-arg path for each). Any failure in a single register
|
|
13
|
+
* throws and aborts the batch (no partial-success semantics).
|
|
14
|
+
* - `getOrThrow(id)`: returns the item; throws
|
|
15
|
+
* `new Error("Not found: <id>. Available: <csv of known ids>")`
|
|
16
|
+
* — a plain `Error`, NOT a typed `XYZNotFoundError` (Codex #1).
|
|
17
|
+
* - No start / stop lifecycle exists (design §2.3 claim was
|
|
18
|
+
* fictional).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
22
|
+
|
|
23
|
+
import type { Logger } from '../utils/logger.js'
|
|
24
|
+
|
|
25
|
+
import { ManagedRegistry } from './ManagedRegistry.js'
|
|
26
|
+
|
|
27
|
+
function makeLogger(): Logger {
|
|
28
|
+
const self = {
|
|
29
|
+
info: vi.fn(),
|
|
30
|
+
warn: vi.fn(),
|
|
31
|
+
error: vi.fn(),
|
|
32
|
+
debug: vi.fn(),
|
|
33
|
+
child: vi.fn(),
|
|
34
|
+
} as unknown as Logger
|
|
35
|
+
;(self as { child: (ctx: unknown) => Logger }).child = vi.fn(() => self)
|
|
36
|
+
return self
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Item {
|
|
40
|
+
id: string
|
|
41
|
+
info: { id: string }
|
|
42
|
+
value: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('ManagedRegistry', () => {
|
|
46
|
+
describe('register (2-arg form)', () => {
|
|
47
|
+
it('throws when called with (id) only', () => {
|
|
48
|
+
const r = new ManagedRegistry<Item>({ componentName: 't' })
|
|
49
|
+
// biome-ignore lint/suspicious/noExplicitAny: exercises the no-item-arg path
|
|
50
|
+
expect(() => (r as any).register('a')).toThrow(/requires an item argument/)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('warn-logs then overwrites on duplicate id', () => {
|
|
54
|
+
const logger = makeLogger()
|
|
55
|
+
const r = new ManagedRegistry<Item>({ componentName: 't', logger })
|
|
56
|
+
const a = { id: 'a', info: { id: 'a' }, value: 1 }
|
|
57
|
+
const b = { id: 'a', info: { id: 'a' }, value: 2 }
|
|
58
|
+
r.register('a', a)
|
|
59
|
+
r.register('a', b)
|
|
60
|
+
expect(r.get('a')?.value).toBe(2)
|
|
61
|
+
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('"a" already registered'))
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('register (single-item form)', () => {
|
|
66
|
+
it('uses idField when computeId is not set', () => {
|
|
67
|
+
const r = new ManagedRegistry<Item>({ componentName: 't', idField: 'id' })
|
|
68
|
+
r.register({ id: 'a', info: { id: 'nested' }, value: 1 })
|
|
69
|
+
expect(r.get('a')).toBeDefined()
|
|
70
|
+
expect(r.get('nested')).toBeUndefined()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('computeId takes precedence over idField when both are set', () => {
|
|
74
|
+
const r = new ManagedRegistry<Item>({
|
|
75
|
+
componentName: 't',
|
|
76
|
+
idField: 'id',
|
|
77
|
+
computeId: (item) => item.info.id,
|
|
78
|
+
})
|
|
79
|
+
r.register({ id: 'top', info: { id: 'nested' }, value: 1 })
|
|
80
|
+
expect(r.get('nested')).toBeDefined()
|
|
81
|
+
expect(r.get('top')).toBeUndefined()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('throws when neither idField nor computeId is configured', () => {
|
|
85
|
+
const r = new ManagedRegistry<Item>({ componentName: 't' })
|
|
86
|
+
expect(() => r.register({ id: 'a', info: { id: 'a' }, value: 1 })).toThrow(
|
|
87
|
+
/requires idField or computeId/,
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('register (array form)', () => {
|
|
93
|
+
it('batch-registers via the single-item path', () => {
|
|
94
|
+
const r = new ManagedRegistry<Item>({ componentName: 't', idField: 'id' })
|
|
95
|
+
r.register([
|
|
96
|
+
{ id: 'a', info: { id: 'a' }, value: 1 },
|
|
97
|
+
{ id: 'b', info: { id: 'b' }, value: 2 },
|
|
98
|
+
])
|
|
99
|
+
expect(r.listIds()).toEqual(['a', 'b'])
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('getOrThrow', () => {
|
|
104
|
+
it('returns the item when present', () => {
|
|
105
|
+
const r = new ManagedRegistry<Item>({ componentName: 't', idField: 'id' })
|
|
106
|
+
const a = { id: 'a', info: { id: 'a' }, value: 1 }
|
|
107
|
+
r.register(a)
|
|
108
|
+
expect(r.getOrThrow('a')).toBe(a)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('throws a plain Error naming the missing id + available ids', () => {
|
|
112
|
+
const r = new ManagedRegistry<Item>({ componentName: 't', idField: 'id' })
|
|
113
|
+
r.register({ id: 'a', info: { id: 'a' }, value: 1 })
|
|
114
|
+
r.register({ id: 'b', info: { id: 'b' }, value: 2 })
|
|
115
|
+
expect(() => r.getOrThrow('missing')).toThrow(/Not found: "missing"\. Available: a, b/)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
})
|