@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.
- package/CHANGELOG.md +10 -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.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.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/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/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/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/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/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/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/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/iteration/phases/advisory.test.ts +412 -0
- package/src/test-setup.ts +24 -0
- package/src/utils/logger.ts +6 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Current-code invariants asserted (2026-04-21, ses_006 Phase 2):
|
|
3
|
+
*
|
|
4
|
+
* - `MCPConnectorBridge` wraps a `ConnectorManager` and exposes
|
|
5
|
+
* connector methods as MCP tools with a name pattern
|
|
6
|
+
* `<prefix>_<connectorId>_<methodName>`; prefix defaults to 'namzu'.
|
|
7
|
+
* - `listTools()` with no instanceId iterates
|
|
8
|
+
* `manager.listConnectedInstances()`; `listTools(instanceId)` uses
|
|
9
|
+
* `manager.getInstance(instanceId)` and filters Boolean (missing
|
|
10
|
+
* instance → empty output).
|
|
11
|
+
* - `listTools()` rebuilds `mappings` from scratch on each call.
|
|
12
|
+
* - Schema conversion via `zodToJsonSchema` is best-effort; on
|
|
13
|
+
* conversion failure the schema falls back to `{type: 'object'}`.
|
|
14
|
+
* - `getMappings()` returns a COPY (mutation of the returned array
|
|
15
|
+
* does not affect internal state).
|
|
16
|
+
* - `callTool(name, args)`:
|
|
17
|
+
* - Returns `{content: [text], isError: true}` for unknown tools.
|
|
18
|
+
* - On manager success, returns `{content: [text], isError: false}`
|
|
19
|
+
* with the output stringified if non-string.
|
|
20
|
+
* - On manager failure, returns `{content: [text with error],
|
|
21
|
+
* isError: true}`; falls back to 'Unknown error' when the
|
|
22
|
+
* manager error is undefined.
|
|
23
|
+
* - Method descriptions include the connector DEFINITION name
|
|
24
|
+
* bracketed (not the instance name): `[<def.name>] <method.description>`.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
28
|
+
import { z } from 'zod'
|
|
29
|
+
|
|
30
|
+
import type { ConnectorManager } from '../../../manager/connector/lifecycle.js'
|
|
31
|
+
import type { ConnectorRegistry } from '../../../registry/connector/definitions.js'
|
|
32
|
+
import type {
|
|
33
|
+
ConnectorDefinition,
|
|
34
|
+
ConnectorExecuteResult,
|
|
35
|
+
ConnectorInstance,
|
|
36
|
+
} from '../../../types/connector/index.js'
|
|
37
|
+
import type { ConnectorId, ConnectorInstanceId } from '../../../types/ids/index.js'
|
|
38
|
+
|
|
39
|
+
import { MCPConnectorBridge } from './adapter.js'
|
|
40
|
+
|
|
41
|
+
const CID = 'conn_http' as ConnectorId
|
|
42
|
+
const IID = 'ci_abc' as ConnectorInstanceId
|
|
43
|
+
|
|
44
|
+
function makeDefinition(overrides: Partial<ConnectorDefinition> = {}): ConnectorDefinition {
|
|
45
|
+
return {
|
|
46
|
+
id: CID,
|
|
47
|
+
name: 'HTTP',
|
|
48
|
+
description: 'HTTP connector',
|
|
49
|
+
connectionType: 'http',
|
|
50
|
+
configSchema: z.object({}),
|
|
51
|
+
methods: [
|
|
52
|
+
{
|
|
53
|
+
name: 'request',
|
|
54
|
+
description: 'make http request',
|
|
55
|
+
inputSchema: z.object({ url: z.string() }),
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
...overrides,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function makeInstance(): ConnectorInstance {
|
|
63
|
+
return {
|
|
64
|
+
id: IID,
|
|
65
|
+
connectorId: CID,
|
|
66
|
+
config: { connectorId: CID, name: 'svc' },
|
|
67
|
+
status: 'connected',
|
|
68
|
+
createdAt: Date.now(),
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function makeManager(overrides: Partial<ConnectorManager> = {}): ConnectorManager {
|
|
73
|
+
const def = makeDefinition()
|
|
74
|
+
const registry = {
|
|
75
|
+
get: vi.fn(() => def),
|
|
76
|
+
} as unknown as ConnectorRegistry
|
|
77
|
+
const instance = makeInstance()
|
|
78
|
+
return {
|
|
79
|
+
getInstance: vi.fn(() => instance),
|
|
80
|
+
getRegistry: vi.fn(() => registry),
|
|
81
|
+
listConnectedInstances: vi.fn(() => [instance]),
|
|
82
|
+
execute: vi.fn<() => Promise<ConnectorExecuteResult>>(),
|
|
83
|
+
...overrides,
|
|
84
|
+
} as unknown as ConnectorManager
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe('MCPConnectorBridge.listTools', () => {
|
|
88
|
+
it('builds one MCP tool per method per connected instance with default prefix namzu', () => {
|
|
89
|
+
const bridge = new MCPConnectorBridge({ manager: makeManager() })
|
|
90
|
+
const tools = bridge.listTools()
|
|
91
|
+
expect(tools).toHaveLength(1)
|
|
92
|
+
expect(tools[0]?.name).toBe(`namzu_${CID}_request`)
|
|
93
|
+
expect(tools[0]?.description).toBe('[HTTP] make http request')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('honors a custom prefix', () => {
|
|
97
|
+
const bridge = new MCPConnectorBridge({ manager: makeManager(), prefix: 'acme' })
|
|
98
|
+
expect(bridge.listTools()[0]?.name).toBe(`acme_${CID}_request`)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('single-instance lookup via listTools(instanceId) uses getInstance; missing instance → empty', () => {
|
|
102
|
+
const manager = makeManager({
|
|
103
|
+
getInstance: vi.fn(() => undefined),
|
|
104
|
+
} as unknown as Partial<ConnectorManager>)
|
|
105
|
+
const bridge = new MCPConnectorBridge({ manager })
|
|
106
|
+
expect(bridge.listTools(IID)).toEqual([])
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('mappings are rebuilt on every listTools() call (not appended)', () => {
|
|
110
|
+
const bridge = new MCPConnectorBridge({ manager: makeManager() })
|
|
111
|
+
bridge.listTools()
|
|
112
|
+
bridge.listTools()
|
|
113
|
+
expect(bridge.getMappings()).toHaveLength(1)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('skips instances whose connectorId is missing in the registry', () => {
|
|
117
|
+
const manager = makeManager({
|
|
118
|
+
getRegistry: vi.fn(() => ({ get: vi.fn(() => undefined) }) as unknown as ConnectorRegistry),
|
|
119
|
+
} as unknown as Partial<ConnectorManager>)
|
|
120
|
+
const bridge = new MCPConnectorBridge({ manager })
|
|
121
|
+
expect(bridge.listTools()).toEqual([])
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('getMappings returns a copy — external mutation does not leak', () => {
|
|
125
|
+
const bridge = new MCPConnectorBridge({ manager: makeManager() })
|
|
126
|
+
bridge.listTools()
|
|
127
|
+
const copy = bridge.getMappings()
|
|
128
|
+
copy.length = 0
|
|
129
|
+
expect(bridge.getMappings()).toHaveLength(1)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('schema conversion falls back to {type: object} when zodToJsonSchema throws', () => {
|
|
133
|
+
const bad = makeDefinition({
|
|
134
|
+
methods: [
|
|
135
|
+
{
|
|
136
|
+
name: 'broken',
|
|
137
|
+
description: 'd',
|
|
138
|
+
// Not a real zod type — intentionally misleads zodToJsonSchema.
|
|
139
|
+
inputSchema: null as unknown as z.ZodType,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
})
|
|
143
|
+
const manager = makeManager({
|
|
144
|
+
getRegistry: vi.fn(() => ({ get: vi.fn(() => bad) }) as unknown as ConnectorRegistry),
|
|
145
|
+
} as unknown as Partial<ConnectorManager>)
|
|
146
|
+
const bridge = new MCPConnectorBridge({ manager })
|
|
147
|
+
const tools = bridge.listTools()
|
|
148
|
+
expect(tools[0]?.inputSchema).toEqual({ type: 'object' })
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('MCPConnectorBridge.callTool', () => {
|
|
153
|
+
it('unknown tool → isError true + explanatory text', async () => {
|
|
154
|
+
const bridge = new MCPConnectorBridge({ manager: makeManager() })
|
|
155
|
+
const result = await bridge.callTool('nope')
|
|
156
|
+
expect(result.isError).toBe(true)
|
|
157
|
+
expect(result.content[0]).toMatchObject({ type: 'text', text: expect.stringContaining('nope') })
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('happy path — string output passes through as text', async () => {
|
|
161
|
+
const manager = makeManager()
|
|
162
|
+
vi.mocked(manager.execute).mockResolvedValueOnce({
|
|
163
|
+
success: true,
|
|
164
|
+
output: 'hello',
|
|
165
|
+
durationMs: 5,
|
|
166
|
+
})
|
|
167
|
+
const bridge = new MCPConnectorBridge({ manager })
|
|
168
|
+
bridge.listTools()
|
|
169
|
+
const result = await bridge.callTool(`namzu_${CID}_request`, { url: 'x' })
|
|
170
|
+
expect(result.isError).toBe(false)
|
|
171
|
+
expect(result.content[0]).toEqual({ type: 'text', text: 'hello' })
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('non-string success output is JSON-stringified', async () => {
|
|
175
|
+
const manager = makeManager()
|
|
176
|
+
vi.mocked(manager.execute).mockResolvedValueOnce({
|
|
177
|
+
success: true,
|
|
178
|
+
output: { status: 200 },
|
|
179
|
+
durationMs: 1,
|
|
180
|
+
})
|
|
181
|
+
const bridge = new MCPConnectorBridge({ manager })
|
|
182
|
+
bridge.listTools()
|
|
183
|
+
const result = await bridge.callTool(`namzu_${CID}_request`)
|
|
184
|
+
expect(result.content[0]).toEqual({
|
|
185
|
+
type: 'text',
|
|
186
|
+
text: JSON.stringify({ status: 200 }, null, 2),
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('manager failure → isError true with error text; falls back to "Unknown error" when error is undefined', async () => {
|
|
191
|
+
const manager = makeManager()
|
|
192
|
+
vi.mocked(manager.execute).mockResolvedValueOnce({
|
|
193
|
+
success: false,
|
|
194
|
+
output: undefined,
|
|
195
|
+
durationMs: 1,
|
|
196
|
+
})
|
|
197
|
+
const bridge = new MCPConnectorBridge({ manager })
|
|
198
|
+
bridge.listTools()
|
|
199
|
+
const result = await bridge.callTool(`namzu_${CID}_request`)
|
|
200
|
+
expect(result.isError).toBe(true)
|
|
201
|
+
expect(result.content[0]).toEqual({ type: 'text', text: 'Unknown error' })
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('carries the explicit error message when manager provides one', async () => {
|
|
205
|
+
const manager = makeManager()
|
|
206
|
+
vi.mocked(manager.execute).mockResolvedValueOnce({
|
|
207
|
+
success: false,
|
|
208
|
+
output: undefined,
|
|
209
|
+
durationMs: 1,
|
|
210
|
+
error: 'timeout',
|
|
211
|
+
})
|
|
212
|
+
const bridge = new MCPConnectorBridge({ manager })
|
|
213
|
+
bridge.listTools()
|
|
214
|
+
const result = await bridge.callTool(`namzu_${CID}_request`)
|
|
215
|
+
expect(result.content[0]).toEqual({ type: 'text', text: 'timeout' })
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('passes empty args through as {} when invoked without a second arg', async () => {
|
|
219
|
+
const manager = makeManager()
|
|
220
|
+
vi.mocked(manager.execute).mockResolvedValueOnce({
|
|
221
|
+
success: true,
|
|
222
|
+
output: 'ok',
|
|
223
|
+
durationMs: 1,
|
|
224
|
+
})
|
|
225
|
+
const bridge = new MCPConnectorBridge({ manager })
|
|
226
|
+
bridge.listTools()
|
|
227
|
+
await bridge.callTool(`namzu_${CID}_request`)
|
|
228
|
+
expect(manager.execute).toHaveBeenCalledWith(expect.objectContaining({ input: {} }))
|
|
229
|
+
})
|
|
230
|
+
})
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Current-code invariants asserted (2026-04-21, ses_006 Phase 2):
|
|
3
|
+
*
|
|
4
|
+
* - `mapRunToStreamEvent(event, runId)` returns `{wire, data}` or null.
|
|
5
|
+
* - Wire names match a fixed mapping (one per RunEvent.type):
|
|
6
|
+
* run.started, iteration.started, iteration.completed, message.delta,
|
|
7
|
+
* tool.executing, tool.completed, review.requested, review.completed,
|
|
8
|
+
* checkpoint.created, run.paused, run.resuming, token.usage,
|
|
9
|
+
* activity.created, activity.updated, plan.ready, plan.approved,
|
|
10
|
+
* plan.rejected, plan.step_updated, agent.pending, agent.completed,
|
|
11
|
+
* agent.failed, agent.canceled, task.created, task.updated,
|
|
12
|
+
* plugin.hook_executing, plugin.hook_completed, sandbox.created,
|
|
13
|
+
* sandbox.exec, sandbox.destroyed.
|
|
14
|
+
* - `run_completed` and `run_failed` produce null (final state is
|
|
15
|
+
* delivered by the task.* path, not the SSE delta).
|
|
16
|
+
* - Sub-session lifecycle events (spawned / messaged / idled) produce
|
|
17
|
+
* null — the SSE wire surface does not carry them today.
|
|
18
|
+
* - `data.run_id` is always set from the second arg.
|
|
19
|
+
* - `llm_response` data: `content` falls back to null when empty;
|
|
20
|
+
* `has_tool_calls` is a boolean.
|
|
21
|
+
* - If the event carries `sourceAgentId` or `parentTaskId` fields,
|
|
22
|
+
* they are mirrored onto `data.source_agent_id` / `data.parent_task_id`
|
|
23
|
+
* (snake-cased).
|
|
24
|
+
* - `mapSessionToStreamEvent` is a deprecated alias.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { describe, expect, it } from 'vitest'
|
|
28
|
+
|
|
29
|
+
import type {
|
|
30
|
+
ActivityId,
|
|
31
|
+
CheckpointId,
|
|
32
|
+
PlanId,
|
|
33
|
+
PluginId,
|
|
34
|
+
RunId,
|
|
35
|
+
SandboxId,
|
|
36
|
+
TaskId,
|
|
37
|
+
} from '../../types/ids/index.js'
|
|
38
|
+
import type { RunEvent } from '../../types/run/events.js'
|
|
39
|
+
|
|
40
|
+
import { mapRunToStreamEvent, mapSessionToStreamEvent } from './mapper.js'
|
|
41
|
+
|
|
42
|
+
const RID = 'run_1' as RunId
|
|
43
|
+
|
|
44
|
+
describe('mapRunToStreamEvent — mapped variants', () => {
|
|
45
|
+
it('run_started → run.started', () => {
|
|
46
|
+
const r = mapRunToStreamEvent(
|
|
47
|
+
{ type: 'run_started', runId: RID, systemPrompt: 'be terse' },
|
|
48
|
+
RID,
|
|
49
|
+
)
|
|
50
|
+
expect(r?.wire).toBe('run.started')
|
|
51
|
+
expect(r?.data).toMatchObject({ run_id: RID, system_prompt: 'be terse' })
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('run_started with no systemPrompt → system_prompt: null', () => {
|
|
55
|
+
const r = mapRunToStreamEvent({ type: 'run_started', runId: RID }, RID)
|
|
56
|
+
expect(r?.data).toMatchObject({ system_prompt: null })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('iteration_started / iteration_completed carry iteration number', () => {
|
|
60
|
+
const a = mapRunToStreamEvent({ type: 'iteration_started', runId: RID, iteration: 2 }, RID)
|
|
61
|
+
expect(a).toEqual({ wire: 'iteration.started', data: { run_id: RID, iteration: 2 } })
|
|
62
|
+
|
|
63
|
+
const b = mapRunToStreamEvent(
|
|
64
|
+
{ type: 'iteration_completed', runId: RID, iteration: 2, hasToolCalls: false },
|
|
65
|
+
RID,
|
|
66
|
+
)
|
|
67
|
+
expect(b).toEqual({ wire: 'iteration.completed', data: { run_id: RID, iteration: 2 } })
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('llm_response → message.delta with content + has_tool_calls', () => {
|
|
71
|
+
const r = mapRunToStreamEvent(
|
|
72
|
+
{ type: 'llm_response', runId: RID, content: 'hi', hasToolCalls: false },
|
|
73
|
+
RID,
|
|
74
|
+
)
|
|
75
|
+
expect(r).toEqual({
|
|
76
|
+
wire: 'message.delta',
|
|
77
|
+
data: { run_id: RID, content: 'hi', has_tool_calls: false },
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('llm_response with null content → content: null', () => {
|
|
82
|
+
const r = mapRunToStreamEvent(
|
|
83
|
+
{ type: 'llm_response', runId: RID, content: null, hasToolCalls: true },
|
|
84
|
+
RID,
|
|
85
|
+
)
|
|
86
|
+
expect(r?.data).toMatchObject({ content: null, has_tool_calls: true })
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('tool_executing / tool_completed carry tool_name + input/result', () => {
|
|
90
|
+
const exec = mapRunToStreamEvent(
|
|
91
|
+
{ type: 'tool_executing', runId: RID, toolName: 'read_file', input: { path: '/a' } },
|
|
92
|
+
RID,
|
|
93
|
+
)
|
|
94
|
+
expect(exec?.wire).toBe('tool.executing')
|
|
95
|
+
expect(exec?.data).toMatchObject({ tool_name: 'read_file', input: { path: '/a' } })
|
|
96
|
+
|
|
97
|
+
const done = mapRunToStreamEvent(
|
|
98
|
+
{ type: 'tool_completed', runId: RID, toolName: 'read_file', result: 'ok' },
|
|
99
|
+
RID,
|
|
100
|
+
)
|
|
101
|
+
expect(done?.wire).toBe('tool.completed')
|
|
102
|
+
expect(done?.data).toMatchObject({ tool_name: 'read_file', result: 'ok' })
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('tool_review_requested / tool_review_completed carry review fields', () => {
|
|
106
|
+
const a = mapRunToStreamEvent(
|
|
107
|
+
{
|
|
108
|
+
type: 'tool_review_requested',
|
|
109
|
+
runId: RID,
|
|
110
|
+
iteration: 1,
|
|
111
|
+
toolCalls: [{ id: 'tc1', name: 'write_file', input: {}, isDestructive: true }],
|
|
112
|
+
},
|
|
113
|
+
RID,
|
|
114
|
+
)
|
|
115
|
+
expect(a?.wire).toBe('review.requested')
|
|
116
|
+
expect(a?.data.iteration).toBe(1)
|
|
117
|
+
|
|
118
|
+
const b = mapRunToStreamEvent(
|
|
119
|
+
{ type: 'tool_review_completed', runId: RID, decision: 'modified' },
|
|
120
|
+
RID,
|
|
121
|
+
)
|
|
122
|
+
expect(b).toEqual({ wire: 'review.completed', data: { run_id: RID, decision: 'modified' } })
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('checkpoint_created → checkpoint.created', () => {
|
|
126
|
+
const r = mapRunToStreamEvent(
|
|
127
|
+
{
|
|
128
|
+
type: 'checkpoint_created',
|
|
129
|
+
runId: RID,
|
|
130
|
+
checkpointId: 'ckpt_1' as CheckpointId,
|
|
131
|
+
iteration: 1,
|
|
132
|
+
},
|
|
133
|
+
RID,
|
|
134
|
+
)
|
|
135
|
+
expect(r?.wire).toBe('checkpoint.created')
|
|
136
|
+
expect(r?.data).toMatchObject({ checkpoint_id: 'ckpt_1', iteration: 1 })
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('run_paused / run_resuming carry checkpoint fields', () => {
|
|
140
|
+
const p = mapRunToStreamEvent(
|
|
141
|
+
{
|
|
142
|
+
type: 'run_paused',
|
|
143
|
+
runId: RID,
|
|
144
|
+
checkpointId: 'ckpt_2' as CheckpointId,
|
|
145
|
+
reason: 'input required',
|
|
146
|
+
},
|
|
147
|
+
RID,
|
|
148
|
+
)
|
|
149
|
+
expect(p?.wire).toBe('run.paused')
|
|
150
|
+
expect(p?.data).toMatchObject({ checkpoint_id: 'ckpt_2', reason: 'input required' })
|
|
151
|
+
|
|
152
|
+
const r = mapRunToStreamEvent(
|
|
153
|
+
{ type: 'run_resuming', runId: RID, fromCheckpointId: 'ckpt_2' as CheckpointId },
|
|
154
|
+
RID,
|
|
155
|
+
)
|
|
156
|
+
expect(r).toEqual({
|
|
157
|
+
wire: 'run.resuming',
|
|
158
|
+
data: { run_id: RID, from_checkpoint_id: 'ckpt_2' },
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('plan_* events carry plan_id', () => {
|
|
163
|
+
const ready = mapRunToStreamEvent(
|
|
164
|
+
{
|
|
165
|
+
type: 'plan_ready',
|
|
166
|
+
runId: RID,
|
|
167
|
+
planId: 'plan_1' as PlanId,
|
|
168
|
+
title: 't',
|
|
169
|
+
summary: 's',
|
|
170
|
+
steps: [],
|
|
171
|
+
},
|
|
172
|
+
RID,
|
|
173
|
+
)
|
|
174
|
+
expect(ready?.wire).toBe('plan.ready')
|
|
175
|
+
|
|
176
|
+
expect(
|
|
177
|
+
mapRunToStreamEvent({ type: 'plan_approved', runId: RID, planId: 'plan_1' as PlanId }, RID)
|
|
178
|
+
?.wire,
|
|
179
|
+
).toBe('plan.approved')
|
|
180
|
+
|
|
181
|
+
expect(
|
|
182
|
+
mapRunToStreamEvent(
|
|
183
|
+
{ type: 'plan_rejected', runId: RID, planId: 'plan_1' as PlanId, reason: 'nope' },
|
|
184
|
+
RID,
|
|
185
|
+
)?.wire,
|
|
186
|
+
).toBe('plan.rejected')
|
|
187
|
+
|
|
188
|
+
expect(
|
|
189
|
+
mapRunToStreamEvent(
|
|
190
|
+
{
|
|
191
|
+
type: 'plan_step_updated',
|
|
192
|
+
runId: RID,
|
|
193
|
+
planId: 'plan_1' as PlanId,
|
|
194
|
+
stepId: 's1',
|
|
195
|
+
status: 'completed',
|
|
196
|
+
},
|
|
197
|
+
RID,
|
|
198
|
+
)?.wire,
|
|
199
|
+
).toBe('plan.step_updated')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('agent_* events carry task_id', () => {
|
|
203
|
+
const pending = mapRunToStreamEvent(
|
|
204
|
+
{
|
|
205
|
+
type: 'agent_pending',
|
|
206
|
+
runId: RID,
|
|
207
|
+
taskId: 'task_1' as TaskId,
|
|
208
|
+
parentAgentId: 'a',
|
|
209
|
+
childAgentId: 'b',
|
|
210
|
+
depth: 1,
|
|
211
|
+
},
|
|
212
|
+
RID,
|
|
213
|
+
)
|
|
214
|
+
expect(pending?.wire).toBe('agent.pending')
|
|
215
|
+
expect(pending?.data).toMatchObject({ task_id: 'task_1', depth: 1 })
|
|
216
|
+
|
|
217
|
+
expect(
|
|
218
|
+
mapRunToStreamEvent(
|
|
219
|
+
{
|
|
220
|
+
type: 'agent_completed',
|
|
221
|
+
runId: RID,
|
|
222
|
+
taskId: 'task_1' as TaskId,
|
|
223
|
+
result: {
|
|
224
|
+
runId: RID,
|
|
225
|
+
status: 'completed',
|
|
226
|
+
iterations: 1,
|
|
227
|
+
durationMs: 1,
|
|
228
|
+
messages: [],
|
|
229
|
+
usage: {
|
|
230
|
+
promptTokens: 0,
|
|
231
|
+
completionTokens: 0,
|
|
232
|
+
totalTokens: 0,
|
|
233
|
+
cachedTokens: 0,
|
|
234
|
+
cacheWriteTokens: 0,
|
|
235
|
+
},
|
|
236
|
+
cost: {
|
|
237
|
+
inputCostPer1M: 0,
|
|
238
|
+
outputCostPer1M: 0,
|
|
239
|
+
totalCost: 0,
|
|
240
|
+
cacheDiscount: 0,
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
RID,
|
|
245
|
+
)?.wire,
|
|
246
|
+
).toBe('agent.completed')
|
|
247
|
+
|
|
248
|
+
expect(
|
|
249
|
+
mapRunToStreamEvent(
|
|
250
|
+
{ type: 'agent_failed', runId: RID, taskId: 'task_1' as TaskId, error: 'e' },
|
|
251
|
+
RID,
|
|
252
|
+
)?.wire,
|
|
253
|
+
).toBe('agent.failed')
|
|
254
|
+
|
|
255
|
+
expect(
|
|
256
|
+
mapRunToStreamEvent({ type: 'agent_canceled', runId: RID, taskId: 'task_1' as TaskId }, RID)
|
|
257
|
+
?.wire,
|
|
258
|
+
).toBe('agent.canceled')
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('task_created / task_updated map cleanly', () => {
|
|
262
|
+
const a = mapRunToStreamEvent(
|
|
263
|
+
{
|
|
264
|
+
type: 'task_created',
|
|
265
|
+
runId: RID,
|
|
266
|
+
taskId: 'task_1' as TaskId,
|
|
267
|
+
subject: 's',
|
|
268
|
+
status: 'pending',
|
|
269
|
+
},
|
|
270
|
+
RID,
|
|
271
|
+
)
|
|
272
|
+
expect(a?.wire).toBe('task.created')
|
|
273
|
+
|
|
274
|
+
const b = mapRunToStreamEvent(
|
|
275
|
+
{
|
|
276
|
+
type: 'task_updated',
|
|
277
|
+
runId: RID,
|
|
278
|
+
taskId: 'task_1' as TaskId,
|
|
279
|
+
subject: 's',
|
|
280
|
+
status: 'completed',
|
|
281
|
+
},
|
|
282
|
+
RID,
|
|
283
|
+
)
|
|
284
|
+
expect(b?.wire).toBe('task.updated')
|
|
285
|
+
expect(b?.data.owner).toBe(null) // undefined owner → null
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('plugin_hook_* + sandbox_* + activity_* events map cleanly', () => {
|
|
289
|
+
expect(
|
|
290
|
+
mapRunToStreamEvent(
|
|
291
|
+
{
|
|
292
|
+
type: 'plugin_hook_executing',
|
|
293
|
+
runId: RID,
|
|
294
|
+
pluginId: 'plugin_x' as PluginId,
|
|
295
|
+
hookEvent: 'pre_tool_use',
|
|
296
|
+
},
|
|
297
|
+
RID,
|
|
298
|
+
)?.wire,
|
|
299
|
+
).toBe('plugin.hook_executing')
|
|
300
|
+
|
|
301
|
+
expect(
|
|
302
|
+
mapRunToStreamEvent(
|
|
303
|
+
{
|
|
304
|
+
type: 'plugin_hook_completed',
|
|
305
|
+
runId: RID,
|
|
306
|
+
pluginId: 'plugin_x' as PluginId,
|
|
307
|
+
hookEvent: 'pre_tool_use',
|
|
308
|
+
result: { action: 'continue' },
|
|
309
|
+
},
|
|
310
|
+
RID,
|
|
311
|
+
)?.wire,
|
|
312
|
+
).toBe('plugin.hook_completed')
|
|
313
|
+
|
|
314
|
+
expect(
|
|
315
|
+
mapRunToStreamEvent(
|
|
316
|
+
{
|
|
317
|
+
type: 'sandbox_created',
|
|
318
|
+
runId: RID,
|
|
319
|
+
sandboxId: 'sbx_1' as SandboxId,
|
|
320
|
+
environment: 'basic',
|
|
321
|
+
},
|
|
322
|
+
RID,
|
|
323
|
+
)?.wire,
|
|
324
|
+
).toBe('sandbox.created')
|
|
325
|
+
|
|
326
|
+
expect(
|
|
327
|
+
mapRunToStreamEvent(
|
|
328
|
+
{
|
|
329
|
+
type: 'sandbox_exec',
|
|
330
|
+
runId: RID,
|
|
331
|
+
sandboxId: 'sbx_1' as SandboxId,
|
|
332
|
+
command: 'ls',
|
|
333
|
+
exitCode: 0,
|
|
334
|
+
durationMs: 10,
|
|
335
|
+
},
|
|
336
|
+
RID,
|
|
337
|
+
)?.wire,
|
|
338
|
+
).toBe('sandbox.exec')
|
|
339
|
+
|
|
340
|
+
expect(
|
|
341
|
+
mapRunToStreamEvent(
|
|
342
|
+
{ type: 'sandbox_destroyed', runId: RID, sandboxId: 'sbx_1' as SandboxId },
|
|
343
|
+
RID,
|
|
344
|
+
)?.wire,
|
|
345
|
+
).toBe('sandbox.destroyed')
|
|
346
|
+
|
|
347
|
+
expect(
|
|
348
|
+
mapRunToStreamEvent(
|
|
349
|
+
{
|
|
350
|
+
type: 'activity_created',
|
|
351
|
+
runId: RID,
|
|
352
|
+
activityId: 'act_1' as ActivityId,
|
|
353
|
+
activityType: 'tool_call',
|
|
354
|
+
description: 'd',
|
|
355
|
+
},
|
|
356
|
+
RID,
|
|
357
|
+
)?.wire,
|
|
358
|
+
).toBe('activity.created')
|
|
359
|
+
|
|
360
|
+
expect(
|
|
361
|
+
mapRunToStreamEvent(
|
|
362
|
+
{
|
|
363
|
+
type: 'activity_updated',
|
|
364
|
+
runId: RID,
|
|
365
|
+
activityId: 'act_1' as ActivityId,
|
|
366
|
+
status: 'completed',
|
|
367
|
+
},
|
|
368
|
+
RID,
|
|
369
|
+
)?.wire,
|
|
370
|
+
).toBe('activity.updated')
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('token_usage_updated → token.usage with usage + cost passed through', () => {
|
|
374
|
+
const usage = {
|
|
375
|
+
promptTokens: 10,
|
|
376
|
+
completionTokens: 20,
|
|
377
|
+
totalTokens: 30,
|
|
378
|
+
cachedTokens: 0,
|
|
379
|
+
cacheWriteTokens: 0,
|
|
380
|
+
}
|
|
381
|
+
const cost = {
|
|
382
|
+
inputCostPer1M: 1,
|
|
383
|
+
outputCostPer1M: 2,
|
|
384
|
+
totalCost: 0.01,
|
|
385
|
+
cacheDiscount: 0,
|
|
386
|
+
}
|
|
387
|
+
const r = mapRunToStreamEvent({ type: 'token_usage_updated', runId: RID, usage, cost }, RID)
|
|
388
|
+
expect(r?.wire).toBe('token.usage')
|
|
389
|
+
expect(r?.data).toMatchObject({ usage, cost })
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('source_agent_id + parent_task_id are mirrored when present on the event', () => {
|
|
393
|
+
const event = {
|
|
394
|
+
type: 'run_started',
|
|
395
|
+
runId: RID,
|
|
396
|
+
sourceAgentId: 'sub_agent_1',
|
|
397
|
+
parentTaskId: 'task_42',
|
|
398
|
+
} as unknown as RunEvent
|
|
399
|
+
const r = mapRunToStreamEvent(event, RID)
|
|
400
|
+
expect(r?.data).toMatchObject({ source_agent_id: 'sub_agent_1', parent_task_id: 'task_42' })
|
|
401
|
+
})
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
describe('mapRunToStreamEvent — explicit null set', () => {
|
|
405
|
+
it.each([
|
|
406
|
+
[{ type: 'run_completed' as const, runId: RID, result: 'ok' }],
|
|
407
|
+
[{ type: 'run_failed' as const, runId: RID, error: 'boom' }],
|
|
408
|
+
])('%o returns null', (event) => {
|
|
409
|
+
expect(mapRunToStreamEvent(event, RID)).toBeNull()
|
|
410
|
+
})
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
describe('mapSessionToStreamEvent (deprecated alias)', () => {
|
|
414
|
+
it('is the same function reference as mapRunToStreamEvent', () => {
|
|
415
|
+
// Identity check is deterministic. toEqual on paired calls
|
|
416
|
+
// would work here (SSE mapper doesn't touch the clock), but
|
|
417
|
+
// we mirror the a2a mapper test pattern for consistency —
|
|
418
|
+
// the deprecation shim is literal assignment, so identity is
|
|
419
|
+
// the strictest possible assertion.
|
|
420
|
+
expect(mapSessionToStreamEvent).toBe(mapRunToStreamEvent)
|
|
421
|
+
})
|
|
422
|
+
})
|