@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,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Current-code invariants asserted (2026-04-21, ses_006 Phase 2):
|
|
3
|
+
*
|
|
4
|
+
* - `mapRunToA2AEvent(event, contextId?)` is a one-way mapper: RunEvent →
|
|
5
|
+
* A2AStreamEvent | null. There is no reverse mapper (§2.7).
|
|
6
|
+
* - For events in MAPPING, the returned object is either a
|
|
7
|
+
* TaskStatusUpdateEvent (with a `status` field) or a
|
|
8
|
+
* TaskArtifactUpdateEvent (with an `artifact` field).
|
|
9
|
+
* - For events explicitly mapped to null (iteration_completed,
|
|
10
|
+
* tool_executing, sub-session lifecycle, etc.), the mapper returns
|
|
11
|
+
* null — this is NOT a bug, it is the "bridge does not surface this"
|
|
12
|
+
* contract. Asserting the null-set here pins the contract.
|
|
13
|
+
* - `run_started` / `run_completed` / `run_failed` / `iteration_started`
|
|
14
|
+
* / `run_paused` produce TaskStatusUpdateEvent with stable states:
|
|
15
|
+
* running / completed / failed / running / input-required.
|
|
16
|
+
* - `run_completed.final` is true; every non-terminal status event has
|
|
17
|
+
* `final: false`.
|
|
18
|
+
* - `llm_response` with null/empty `content` returns null; with content
|
|
19
|
+
* returns a running status event.
|
|
20
|
+
* - `tool_completed` produces an artifact event with `artifactId`
|
|
21
|
+
* containing a timestamp and `metadata.toolName`.
|
|
22
|
+
* - `tool_review_requested` and `plan_ready` attach data parts with
|
|
23
|
+
* domain-specific mime types: `application/x-namzu-review-request`
|
|
24
|
+
* and `application/x-namzu-plan`.
|
|
25
|
+
* - `contextId` threads through into the returned event when provided.
|
|
26
|
+
* - `mapSessionToA2AEvent` is a deprecated alias — identical behavior.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { describe, expect, it } from 'vitest'
|
|
30
|
+
|
|
31
|
+
import type { CheckpointId, PlanId, RunId, TaskId } from '../../types/ids/index.js'
|
|
32
|
+
import type { RunEvent } from '../../types/run/events.js'
|
|
33
|
+
|
|
34
|
+
import { mapRunToA2AEvent, mapSessionToA2AEvent } from './mapper.js'
|
|
35
|
+
|
|
36
|
+
const RID = 'run_1' as RunId
|
|
37
|
+
|
|
38
|
+
function isStatusEvent(
|
|
39
|
+
e: ReturnType<typeof mapRunToA2AEvent>,
|
|
40
|
+
): e is Extract<NonNullable<ReturnType<typeof mapRunToA2AEvent>>, { status: unknown }> {
|
|
41
|
+
return !!e && 'status' in e
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isArtifactEvent(
|
|
45
|
+
e: ReturnType<typeof mapRunToA2AEvent>,
|
|
46
|
+
): e is Extract<NonNullable<ReturnType<typeof mapRunToA2AEvent>>, { artifact: unknown }> {
|
|
47
|
+
return !!e && 'artifact' in e
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('mapRunToA2AEvent — mapped variants', () => {
|
|
51
|
+
it('run_started → running / not final / contextId threaded', () => {
|
|
52
|
+
const event: RunEvent = { type: 'run_started', runId: RID }
|
|
53
|
+
const a2a = mapRunToA2AEvent(event, 'ctx_42')
|
|
54
|
+
expect(isStatusEvent(a2a)).toBe(true)
|
|
55
|
+
if (isStatusEvent(a2a)) {
|
|
56
|
+
expect(a2a.status.state).toBe('running')
|
|
57
|
+
expect(a2a.final).toBe(false)
|
|
58
|
+
expect(a2a.contextId).toBe('ctx_42')
|
|
59
|
+
expect(a2a.taskId).toBe(RID)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('run_completed → completed / final / message carries the result', () => {
|
|
64
|
+
const event: RunEvent = { type: 'run_completed', runId: RID, result: 'done' }
|
|
65
|
+
const a2a = mapRunToA2AEvent(event)
|
|
66
|
+
expect(isStatusEvent(a2a)).toBe(true)
|
|
67
|
+
if (isStatusEvent(a2a)) {
|
|
68
|
+
expect(a2a.status.state).toBe('completed')
|
|
69
|
+
expect(a2a.final).toBe(true)
|
|
70
|
+
expect(a2a.status.message?.parts).toEqual([{ kind: 'text', text: 'done' }])
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('run_failed → failed / final / message carries the error', () => {
|
|
75
|
+
const event: RunEvent = { type: 'run_failed', runId: RID, error: 'boom' }
|
|
76
|
+
const a2a = mapRunToA2AEvent(event)
|
|
77
|
+
expect(isStatusEvent(a2a)).toBe(true)
|
|
78
|
+
if (isStatusEvent(a2a)) {
|
|
79
|
+
expect(a2a.status.state).toBe('failed')
|
|
80
|
+
expect(a2a.final).toBe(true)
|
|
81
|
+
expect(a2a.status.message?.parts).toEqual([{ kind: 'text', text: 'boom' }])
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('iteration_started → running / not final / message names the iteration', () => {
|
|
86
|
+
const event: RunEvent = { type: 'iteration_started', runId: RID, iteration: 3 }
|
|
87
|
+
const a2a = mapRunToA2AEvent(event)
|
|
88
|
+
expect(isStatusEvent(a2a)).toBe(true)
|
|
89
|
+
if (isStatusEvent(a2a)) {
|
|
90
|
+
expect(a2a.status.state).toBe('running')
|
|
91
|
+
expect(a2a.final).toBe(false)
|
|
92
|
+
expect(a2a.status.message?.parts[0]).toMatchObject({
|
|
93
|
+
kind: 'text',
|
|
94
|
+
text: expect.stringContaining('Iteration 3'),
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('llm_response with content → running + text part', () => {
|
|
100
|
+
const event: RunEvent = { type: 'llm_response', runId: RID, content: 'hi', hasToolCalls: false }
|
|
101
|
+
const a2a = mapRunToA2AEvent(event)
|
|
102
|
+
expect(isStatusEvent(a2a)).toBe(true)
|
|
103
|
+
if (isStatusEvent(a2a)) {
|
|
104
|
+
expect(a2a.status.state).toBe('running')
|
|
105
|
+
expect(a2a.status.message?.parts).toEqual([{ kind: 'text', text: 'hi' }])
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('llm_response with null content → null', () => {
|
|
110
|
+
const event: RunEvent = { type: 'llm_response', runId: RID, content: null, hasToolCalls: true }
|
|
111
|
+
expect(mapRunToA2AEvent(event)).toBeNull()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('llm_response with empty-string content → null (falsy)', () => {
|
|
115
|
+
const event: RunEvent = { type: 'llm_response', runId: RID, content: '', hasToolCalls: false }
|
|
116
|
+
expect(mapRunToA2AEvent(event)).toBeNull()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('tool_completed → artifact with toolName metadata + timestamped id', () => {
|
|
120
|
+
const event: RunEvent = {
|
|
121
|
+
type: 'tool_completed',
|
|
122
|
+
runId: RID,
|
|
123
|
+
toolName: 'read_file',
|
|
124
|
+
result: 'ok',
|
|
125
|
+
}
|
|
126
|
+
const a2a = mapRunToA2AEvent(event)
|
|
127
|
+
expect(isArtifactEvent(a2a)).toBe(true)
|
|
128
|
+
if (isArtifactEvent(a2a)) {
|
|
129
|
+
expect(a2a.artifact.artifactId).toMatch(/^tool-read_file-\d+$/)
|
|
130
|
+
expect(a2a.artifact.name).toBe('read_file result')
|
|
131
|
+
expect(a2a.artifact.parts).toEqual([{ kind: 'text', text: 'ok' }])
|
|
132
|
+
expect(a2a.artifact.metadata).toEqual({ toolName: 'read_file' })
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('tool_review_requested → input-required + data part with review mime type', () => {
|
|
137
|
+
const event: RunEvent = {
|
|
138
|
+
type: 'tool_review_requested',
|
|
139
|
+
runId: RID,
|
|
140
|
+
iteration: 2,
|
|
141
|
+
toolCalls: [{ id: 'tc1', name: 'write_file', input: {}, isDestructive: true }],
|
|
142
|
+
}
|
|
143
|
+
const a2a = mapRunToA2AEvent(event)
|
|
144
|
+
expect(isStatusEvent(a2a)).toBe(true)
|
|
145
|
+
if (isStatusEvent(a2a)) {
|
|
146
|
+
expect(a2a.status.state).toBe('input-required')
|
|
147
|
+
const dataPart = a2a.status.message?.parts.find((p) => p.kind === 'data')
|
|
148
|
+
expect(dataPart).toBeDefined()
|
|
149
|
+
if (dataPart && dataPart.kind === 'data') {
|
|
150
|
+
expect(dataPart.mimeType).toBe('application/x-namzu-review-request')
|
|
151
|
+
expect(dataPart.data).toEqual({
|
|
152
|
+
toolCalls: [{ id: 'tc1', name: 'write_file', isDestructive: true }],
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('plan_ready → input-required + data part with plan mime type', () => {
|
|
159
|
+
const event: RunEvent = {
|
|
160
|
+
type: 'plan_ready',
|
|
161
|
+
runId: RID,
|
|
162
|
+
planId: 'plan_1' as PlanId,
|
|
163
|
+
title: 'Migrate tables',
|
|
164
|
+
summary: 'Three steps',
|
|
165
|
+
steps: [
|
|
166
|
+
{
|
|
167
|
+
id: 's1',
|
|
168
|
+
description: 'create schema',
|
|
169
|
+
toolName: 'bash',
|
|
170
|
+
dependsOn: [],
|
|
171
|
+
order: 0,
|
|
172
|
+
status: 'pending',
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
}
|
|
176
|
+
const a2a = mapRunToA2AEvent(event)
|
|
177
|
+
expect(isStatusEvent(a2a)).toBe(true)
|
|
178
|
+
if (isStatusEvent(a2a)) {
|
|
179
|
+
expect(a2a.status.state).toBe('input-required')
|
|
180
|
+
const dataPart = a2a.status.message?.parts.find((p) => p.kind === 'data')
|
|
181
|
+
expect(dataPart).toBeDefined()
|
|
182
|
+
if (dataPart && dataPart.kind === 'data') {
|
|
183
|
+
expect(dataPart.mimeType).toBe('application/x-namzu-plan')
|
|
184
|
+
expect(dataPart.data).toMatchObject({
|
|
185
|
+
planId: 'plan_1',
|
|
186
|
+
title: 'Migrate tables',
|
|
187
|
+
summary: 'Three steps',
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('run_paused → input-required + reason in text part', () => {
|
|
194
|
+
const event: RunEvent = {
|
|
195
|
+
type: 'run_paused',
|
|
196
|
+
runId: RID,
|
|
197
|
+
checkpointId: 'ckpt_1' as CheckpointId,
|
|
198
|
+
reason: 'waiting for review',
|
|
199
|
+
}
|
|
200
|
+
const a2a = mapRunToA2AEvent(event)
|
|
201
|
+
expect(isStatusEvent(a2a)).toBe(true)
|
|
202
|
+
if (isStatusEvent(a2a)) {
|
|
203
|
+
expect(a2a.status.state).toBe('input-required')
|
|
204
|
+
expect(a2a.status.message?.parts[0]).toMatchObject({
|
|
205
|
+
kind: 'text',
|
|
206
|
+
text: expect.stringContaining('waiting for review'),
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
describe('mapRunToA2AEvent — explicit null set', () => {
|
|
213
|
+
const nullEvents: RunEvent[] = [
|
|
214
|
+
{ type: 'iteration_completed', runId: RID, iteration: 1, hasToolCalls: false },
|
|
215
|
+
{ type: 'tool_executing', runId: RID, toolName: 'x', input: {} },
|
|
216
|
+
{ type: 'tool_review_completed', runId: RID, decision: 'approved' },
|
|
217
|
+
{
|
|
218
|
+
type: 'checkpoint_created',
|
|
219
|
+
runId: RID,
|
|
220
|
+
checkpointId: 'ckpt_1' as CheckpointId,
|
|
221
|
+
iteration: 1,
|
|
222
|
+
},
|
|
223
|
+
{ type: 'run_resuming', runId: RID, fromCheckpointId: 'ckpt_1' as CheckpointId },
|
|
224
|
+
{
|
|
225
|
+
type: 'token_usage_updated',
|
|
226
|
+
runId: RID,
|
|
227
|
+
usage: {
|
|
228
|
+
promptTokens: 0,
|
|
229
|
+
completionTokens: 0,
|
|
230
|
+
totalTokens: 0,
|
|
231
|
+
cachedTokens: 0,
|
|
232
|
+
cacheWriteTokens: 0,
|
|
233
|
+
},
|
|
234
|
+
cost: { inputCostPer1M: 0, outputCostPer1M: 0, totalCost: 0, cacheDiscount: 0 },
|
|
235
|
+
},
|
|
236
|
+
{ type: 'plan_approved', runId: RID, planId: 'plan_1' as PlanId },
|
|
237
|
+
{ type: 'plan_rejected', runId: RID, planId: 'plan_1' as PlanId },
|
|
238
|
+
{
|
|
239
|
+
type: 'agent_pending',
|
|
240
|
+
runId: RID,
|
|
241
|
+
taskId: 'task_1' as TaskId,
|
|
242
|
+
parentAgentId: 'a',
|
|
243
|
+
childAgentId: 'b',
|
|
244
|
+
depth: 1,
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
type: 'agent_completed',
|
|
248
|
+
runId: RID,
|
|
249
|
+
taskId: 'task_1' as TaskId,
|
|
250
|
+
result: {
|
|
251
|
+
runId: RID,
|
|
252
|
+
status: 'completed',
|
|
253
|
+
iterations: 1,
|
|
254
|
+
durationMs: 1,
|
|
255
|
+
messages: [],
|
|
256
|
+
usage: {
|
|
257
|
+
promptTokens: 0,
|
|
258
|
+
completionTokens: 0,
|
|
259
|
+
totalTokens: 0,
|
|
260
|
+
cachedTokens: 0,
|
|
261
|
+
cacheWriteTokens: 0,
|
|
262
|
+
},
|
|
263
|
+
cost: {
|
|
264
|
+
inputCostPer1M: 0,
|
|
265
|
+
outputCostPer1M: 0,
|
|
266
|
+
totalCost: 0,
|
|
267
|
+
cacheDiscount: 0,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
{ type: 'agent_failed', runId: RID, taskId: 'task_1' as TaskId, error: 'e' },
|
|
272
|
+
{ type: 'agent_canceled', runId: RID, taskId: 'task_1' as TaskId },
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
it.each(nullEvents.map((e) => [e.type, e] as const))(
|
|
276
|
+
'%s returns null (bridge does not surface)',
|
|
277
|
+
(_name, event) => {
|
|
278
|
+
expect(mapRunToA2AEvent(event)).toBeNull()
|
|
279
|
+
},
|
|
280
|
+
)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
describe('mapSessionToA2AEvent (deprecated alias)', () => {
|
|
284
|
+
it('is the same function reference as mapRunToA2AEvent', () => {
|
|
285
|
+
// toEqual against paired invocations races the ISO timestamp
|
|
286
|
+
// inside `statusEvent()` across a millisecond boundary; CI
|
|
287
|
+
// flaked once with 1 ms drift (see PR #11 Build & Test (22)
|
|
288
|
+
// 2026-04-22T11:13). Identity check is deterministic and
|
|
289
|
+
// asserts the deprecation shim strictly — not just a "similar
|
|
290
|
+
// output" check.
|
|
291
|
+
expect(mapSessionToA2AEvent).toBe(mapRunToA2AEvent)
|
|
292
|
+
})
|
|
293
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Current-code invariants asserted (2026-04-21, ses_006 Phase 2):
|
|
3
|
+
*
|
|
4
|
+
* - `messageToA2A(msg)` role mapping: user → user; assistant / system /
|
|
5
|
+
* tool → agent. Any other MessageRole triggers the exhaustive-check
|
|
6
|
+
* throw (unreachable via types).
|
|
7
|
+
* - Non-empty `content` produces one text part.
|
|
8
|
+
* - Assistant with `toolCalls`: one data part per tool call with
|
|
9
|
+
* mimeType `application/x-namzu-tool-call` and fields
|
|
10
|
+
* `{toolCallId, name, arguments}`.
|
|
11
|
+
* - If the message would produce zero parts (null content, no tool
|
|
12
|
+
* calls), a fallback empty-text part is added.
|
|
13
|
+
* - `extractTextFromA2AMessage(msg)` joins text parts with '\n' and
|
|
14
|
+
* ignores non-text parts entirely.
|
|
15
|
+
* - `a2aMessageToInput` is a pure alias for `extractTextFromA2AMessage`.
|
|
16
|
+
* - Only SDK → A2A direction is covered (design.md §2.7: no reverse
|
|
17
|
+
* mapper exists).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, expect, it } from 'vitest'
|
|
21
|
+
|
|
22
|
+
import type { A2AMessage } from '../../types/a2a/index.js'
|
|
23
|
+
import type {
|
|
24
|
+
AssistantMessage,
|
|
25
|
+
SystemMessage,
|
|
26
|
+
ToolMessage,
|
|
27
|
+
UserMessage,
|
|
28
|
+
} from '../../types/message/index.js'
|
|
29
|
+
|
|
30
|
+
import { a2aMessageToInput, extractTextFromA2AMessage, messageToA2A } from './message.js'
|
|
31
|
+
|
|
32
|
+
describe('messageToA2A', () => {
|
|
33
|
+
it('maps user role to user', () => {
|
|
34
|
+
const msg: UserMessage = { role: 'user', content: 'hi' }
|
|
35
|
+
expect(messageToA2A(msg).role).toBe('user')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it.each([['system'], ['assistant'], ['tool']])('maps %s role to agent', (role) => {
|
|
39
|
+
const msg = { role, content: 'x', ...(role === 'tool' && { toolCallId: 'c' }) } as
|
|
40
|
+
| SystemMessage
|
|
41
|
+
| AssistantMessage
|
|
42
|
+
| ToolMessage
|
|
43
|
+
expect(messageToA2A(msg).role).toBe('agent')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('emits a single text part for content-only messages', () => {
|
|
47
|
+
const msg: UserMessage = { role: 'user', content: 'hello' }
|
|
48
|
+
const a2a = messageToA2A(msg)
|
|
49
|
+
expect(a2a.parts).toEqual([{ kind: 'text', text: 'hello' }])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('emits text + data parts when the assistant has tool calls', () => {
|
|
53
|
+
const msg: AssistantMessage = {
|
|
54
|
+
role: 'assistant',
|
|
55
|
+
content: 'let me check',
|
|
56
|
+
toolCalls: [
|
|
57
|
+
{ id: 'tc1', type: 'function', function: { name: 'read_file', arguments: '{"p":"/a"}' } },
|
|
58
|
+
{ id: 'tc2', type: 'function', function: { name: 'list_dir', arguments: '{}' } },
|
|
59
|
+
],
|
|
60
|
+
}
|
|
61
|
+
const a2a = messageToA2A(msg)
|
|
62
|
+
expect(a2a.parts).toEqual([
|
|
63
|
+
{ kind: 'text', text: 'let me check' },
|
|
64
|
+
{
|
|
65
|
+
kind: 'data',
|
|
66
|
+
data: { toolCallId: 'tc1', name: 'read_file', arguments: '{"p":"/a"}' },
|
|
67
|
+
mimeType: 'application/x-namzu-tool-call',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
kind: 'data',
|
|
71
|
+
data: { toolCallId: 'tc2', name: 'list_dir', arguments: '{}' },
|
|
72
|
+
mimeType: 'application/x-namzu-tool-call',
|
|
73
|
+
},
|
|
74
|
+
])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('emits ONLY data parts (no text part) when content is null and tool calls exist', () => {
|
|
78
|
+
const msg: AssistantMessage = {
|
|
79
|
+
role: 'assistant',
|
|
80
|
+
content: null,
|
|
81
|
+
toolCalls: [{ id: 'tc1', type: 'function', function: { name: 'x', arguments: '{}' } }],
|
|
82
|
+
}
|
|
83
|
+
const a2a = messageToA2A(msg)
|
|
84
|
+
expect(a2a.parts.some((p) => p.kind === 'text')).toBe(false)
|
|
85
|
+
expect(a2a.parts.some((p) => p.kind === 'data')).toBe(true)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('fallbacks to an empty text part when content is null and no tool calls', () => {
|
|
89
|
+
const msg: AssistantMessage = { role: 'assistant', content: null }
|
|
90
|
+
expect(messageToA2A(msg).parts).toEqual([{ kind: 'text', text: '' }])
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('extractTextFromA2AMessage', () => {
|
|
95
|
+
it('joins every text part with a newline', () => {
|
|
96
|
+
const msg: A2AMessage = {
|
|
97
|
+
role: 'user',
|
|
98
|
+
parts: [
|
|
99
|
+
{ kind: 'text', text: 'line 1' },
|
|
100
|
+
{ kind: 'text', text: 'line 2' },
|
|
101
|
+
],
|
|
102
|
+
}
|
|
103
|
+
expect(extractTextFromA2AMessage(msg)).toBe('line 1\nline 2')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('ignores non-text parts', () => {
|
|
107
|
+
const msg: A2AMessage = {
|
|
108
|
+
role: 'agent',
|
|
109
|
+
parts: [
|
|
110
|
+
{ kind: 'text', text: 'hello' },
|
|
111
|
+
{ kind: 'data', data: { foo: 1 }, mimeType: 'application/json' },
|
|
112
|
+
],
|
|
113
|
+
}
|
|
114
|
+
expect(extractTextFromA2AMessage(msg)).toBe('hello')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('returns empty string when there are no text parts', () => {
|
|
118
|
+
const msg: A2AMessage = {
|
|
119
|
+
role: 'agent',
|
|
120
|
+
parts: [{ kind: 'data', data: {}, mimeType: 'x' }],
|
|
121
|
+
}
|
|
122
|
+
expect(extractTextFromA2AMessage(msg)).toBe('')
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('a2aMessageToInput', () => {
|
|
127
|
+
it('behaves identically to extractTextFromA2AMessage', () => {
|
|
128
|
+
const msg: A2AMessage = {
|
|
129
|
+
role: 'user',
|
|
130
|
+
parts: [
|
|
131
|
+
{ kind: 'text', text: 'a' },
|
|
132
|
+
{ kind: 'text', text: 'b' },
|
|
133
|
+
{ kind: 'data', data: {}, mimeType: 'x' },
|
|
134
|
+
],
|
|
135
|
+
}
|
|
136
|
+
expect(a2aMessageToInput(msg)).toBe(extractTextFromA2AMessage(msg))
|
|
137
|
+
})
|
|
138
|
+
})
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Current-code invariants asserted (2026-04-21, ses_006 Phase 2):
|
|
3
|
+
*
|
|
4
|
+
* - `isTerminalState(state)` returns true iff state ∈ {completed,
|
|
5
|
+
* failed, canceled, rejected}.
|
|
6
|
+
* - `runStatusToA2AState(status)` is a table lookup:
|
|
7
|
+
* queued → pending; running → running; completed → completed;
|
|
8
|
+
* failed → failed; cancelled → canceled; cancelling → running;
|
|
9
|
+
* expired → failed.
|
|
10
|
+
* - `runToA2ATask(run, messages?)`:
|
|
11
|
+
* - `id` comes from `run.id`; `contextId` comes from
|
|
12
|
+
* `run.project_id ?? undefined`.
|
|
13
|
+
* - `status.timestamp` picks the first defined of
|
|
14
|
+
* `completed_at`, `started_at`, `created_at` (in that order).
|
|
15
|
+
* - `status.message` is agent-text of `run.result` if present,
|
|
16
|
+
* else of `run.last_error` if present, else undefined.
|
|
17
|
+
* - `artifacts` is present iff `run.result` is present; the single
|
|
18
|
+
* artifact carries a subset of usage + timing metadata.
|
|
19
|
+
* - `history` is mapped through `messageToA2A` only when `messages`
|
|
20
|
+
* is supplied.
|
|
21
|
+
* - Top-level `metadata` carries agent_id, agent_name, stop_reason
|
|
22
|
+
* (even if undefined).
|
|
23
|
+
* - `a2aMessageToCreateRun(agentId, params)` only sets a metadata
|
|
24
|
+
* field on `config` when the source value has the expected type
|
|
25
|
+
* (string for model/systemPrompt; number for numeric fields;
|
|
26
|
+
* 'plan' | 'auto' for permissionMode). Everything else is omitted.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { describe, expect, it } from 'vitest'
|
|
30
|
+
|
|
31
|
+
import type { ISOTimestamp, RunConfig, WireRun } from '../../contracts/index.js'
|
|
32
|
+
import type { A2AMessage, A2AMessageSendParams, A2ATaskState } from '../../types/a2a/index.js'
|
|
33
|
+
import type { ProjectId, RunId } from '../../types/ids/index.js'
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
a2aMessageToCreateRun,
|
|
37
|
+
isTerminalState,
|
|
38
|
+
runStatusToA2AState,
|
|
39
|
+
runToA2ATask,
|
|
40
|
+
} from './task.js'
|
|
41
|
+
|
|
42
|
+
const baseRun: WireRun = {
|
|
43
|
+
id: 'run_1' as RunId,
|
|
44
|
+
project_id: null,
|
|
45
|
+
agent_id: 'coder',
|
|
46
|
+
status: 'running',
|
|
47
|
+
created_at: '2026-04-21T12:00:00Z' as ISOTimestamp,
|
|
48
|
+
config: {} as RunConfig,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('isTerminalState', () => {
|
|
52
|
+
const terminals: A2ATaskState[] = ['completed', 'failed', 'canceled', 'rejected']
|
|
53
|
+
const nonTerminals: A2ATaskState[] = ['input-required', 'running', 'pending']
|
|
54
|
+
|
|
55
|
+
it.each(terminals.map((s) => [s]))('%s is terminal', (state) => {
|
|
56
|
+
expect(isTerminalState(state)).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it.each(nonTerminals.map((s) => [s]))('%s is not terminal', (state) => {
|
|
60
|
+
expect(isTerminalState(state)).toBe(false)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('runStatusToA2AState', () => {
|
|
65
|
+
it.each([
|
|
66
|
+
['queued', 'pending'],
|
|
67
|
+
['running', 'running'],
|
|
68
|
+
['completed', 'completed'],
|
|
69
|
+
['failed', 'failed'],
|
|
70
|
+
['cancelled', 'canceled'],
|
|
71
|
+
['cancelling', 'running'],
|
|
72
|
+
['expired', 'failed'],
|
|
73
|
+
] as const)('%s → %s', (wire, a2a) => {
|
|
74
|
+
expect(runStatusToA2AState(wire)).toBe(a2a)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('runToA2ATask', () => {
|
|
79
|
+
it('sets id + contextId from run.id + run.project_id', () => {
|
|
80
|
+
const task = runToA2ATask({ ...baseRun, project_id: 'proj_9' as ProjectId })
|
|
81
|
+
expect(task.id).toBe('run_1')
|
|
82
|
+
expect(task.contextId).toBe('proj_9')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('contextId is undefined when project_id is null', () => {
|
|
86
|
+
const task = runToA2ATask(baseRun)
|
|
87
|
+
expect(task.contextId).toBeUndefined()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('timestamp prefers completed_at > started_at > created_at', () => {
|
|
91
|
+
const created = '2026-04-21T10:00:00Z' as ISOTimestamp
|
|
92
|
+
const started = '2026-04-21T10:05:00Z' as ISOTimestamp
|
|
93
|
+
const completed = '2026-04-21T10:10:00Z' as ISOTimestamp
|
|
94
|
+
|
|
95
|
+
expect(
|
|
96
|
+
runToA2ATask({
|
|
97
|
+
...baseRun,
|
|
98
|
+
created_at: created,
|
|
99
|
+
started_at: started,
|
|
100
|
+
completed_at: completed,
|
|
101
|
+
}).status.timestamp,
|
|
102
|
+
).toBe(completed)
|
|
103
|
+
expect(
|
|
104
|
+
runToA2ATask({ ...baseRun, created_at: created, started_at: started }).status.timestamp,
|
|
105
|
+
).toBe(started)
|
|
106
|
+
expect(runToA2ATask({ ...baseRun, created_at: created }).status.timestamp).toBe(created)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('status.message is the result text when result is present', () => {
|
|
110
|
+
const task = runToA2ATask({ ...baseRun, status: 'completed', result: 'all done' })
|
|
111
|
+
expect(task.status.message?.parts).toEqual([{ kind: 'text', text: 'all done' }])
|
|
112
|
+
expect(task.status.message?.role).toBe('agent')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('status.message falls back to last_error when result is absent', () => {
|
|
116
|
+
const task = runToA2ATask({ ...baseRun, status: 'failed', last_error: 'boom' })
|
|
117
|
+
expect(task.status.message?.parts).toEqual([{ kind: 'text', text: 'boom' }])
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('status.message is undefined when neither result nor last_error is set', () => {
|
|
121
|
+
const task = runToA2ATask(baseRun)
|
|
122
|
+
expect(task.status.message).toBeUndefined()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('attaches an artifact iff result is present', () => {
|
|
126
|
+
expect(runToA2ATask(baseRun).artifacts).toBeUndefined()
|
|
127
|
+
|
|
128
|
+
const withResult = runToA2ATask({
|
|
129
|
+
...baseRun,
|
|
130
|
+
status: 'completed',
|
|
131
|
+
result: 'done',
|
|
132
|
+
model: 'claude-opus-4-7',
|
|
133
|
+
iterations: 3,
|
|
134
|
+
duration_ms: 1200,
|
|
135
|
+
usage: { input_tokens: 10, output_tokens: 20, total_tokens: 30, total_cost_usd: 0.05 },
|
|
136
|
+
})
|
|
137
|
+
expect(withResult.artifacts).toHaveLength(1)
|
|
138
|
+
const artifact = withResult.artifacts?.[0]
|
|
139
|
+
expect(artifact?.artifactId).toBe('run_1-result')
|
|
140
|
+
expect(artifact?.name).toBe('Agent Response')
|
|
141
|
+
expect(artifact?.parts).toEqual([{ kind: 'text', text: 'done' }])
|
|
142
|
+
expect(artifact?.metadata).toMatchObject({
|
|
143
|
+
model: 'claude-opus-4-7',
|
|
144
|
+
iterations: 3,
|
|
145
|
+
duration_ms: 1200,
|
|
146
|
+
input_tokens: 10,
|
|
147
|
+
output_tokens: 20,
|
|
148
|
+
total_cost_usd: 0.05,
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('history is undefined when messages are not supplied', () => {
|
|
153
|
+
expect(runToA2ATask(baseRun).history).toBeUndefined()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('history maps through messageToA2A for every message', () => {
|
|
157
|
+
const task = runToA2ATask(baseRun, [
|
|
158
|
+
{ role: 'user', content: 'hi' },
|
|
159
|
+
{ role: 'assistant', content: 'ack' },
|
|
160
|
+
])
|
|
161
|
+
expect(task.history).toHaveLength(2)
|
|
162
|
+
expect(task.history?.[0]?.role).toBe('user')
|
|
163
|
+
expect(task.history?.[1]?.role).toBe('agent')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('top-level metadata carries agent_id + stop_reason', () => {
|
|
167
|
+
const task = runToA2ATask({ ...baseRun, agent_name: 'Coder', stop_reason: 'end_turn' })
|
|
168
|
+
expect(task.metadata).toMatchObject({
|
|
169
|
+
agent_id: 'coder',
|
|
170
|
+
agent_name: 'Coder',
|
|
171
|
+
stop_reason: 'end_turn',
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
describe('a2aMessageToCreateRun', () => {
|
|
177
|
+
const baseMsg: A2AMessage = { role: 'user', parts: [{ kind: 'text', text: 'do a thing' }] }
|
|
178
|
+
|
|
179
|
+
it('extracts input text from the message', () => {
|
|
180
|
+
const params: A2AMessageSendParams = { message: baseMsg }
|
|
181
|
+
const result = a2aMessageToCreateRun('agent_1', params)
|
|
182
|
+
expect(result.agentId).toBe('agent_1')
|
|
183
|
+
expect(result.input).toBe('do a thing')
|
|
184
|
+
expect(result.config).toEqual({})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('threads contextId from params into projectId', () => {
|
|
188
|
+
const params: A2AMessageSendParams = { message: baseMsg, contextId: 'proj_2' }
|
|
189
|
+
expect(a2aMessageToCreateRun('agent_1', params).projectId).toBe('proj_2')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('only includes typed metadata fields in config', () => {
|
|
193
|
+
const params: A2AMessageSendParams = {
|
|
194
|
+
message: baseMsg,
|
|
195
|
+
metadata: {
|
|
196
|
+
model: 'opus',
|
|
197
|
+
tokenBudget: 1000,
|
|
198
|
+
timeoutMs: 5000,
|
|
199
|
+
temperature: 0.2,
|
|
200
|
+
maxResponseTokens: 2048,
|
|
201
|
+
permissionMode: 'plan',
|
|
202
|
+
systemPrompt: 'be terse',
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
const config = a2aMessageToCreateRun('a', params).config
|
|
206
|
+
expect(config).toEqual({
|
|
207
|
+
model: 'opus',
|
|
208
|
+
tokenBudget: 1000,
|
|
209
|
+
timeoutMs: 5000,
|
|
210
|
+
temperature: 0.2,
|
|
211
|
+
maxResponseTokens: 2048,
|
|
212
|
+
permissionMode: 'plan',
|
|
213
|
+
systemPrompt: 'be terse',
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('drops metadata fields with wrong types', () => {
|
|
218
|
+
const params: A2AMessageSendParams = {
|
|
219
|
+
message: baseMsg,
|
|
220
|
+
metadata: {
|
|
221
|
+
model: 123, // wrong type → dropped
|
|
222
|
+
tokenBudget: 'big', // wrong type → dropped
|
|
223
|
+
permissionMode: 'invalid', // not 'plan'|'auto' → dropped
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
expect(a2aMessageToCreateRun('a', params).config).toEqual({})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('accepts permissionMode only for "plan" or "auto"', () => {
|
|
230
|
+
for (const mode of ['plan', 'auto'] as const) {
|
|
231
|
+
const params: A2AMessageSendParams = { message: baseMsg, metadata: { permissionMode: mode } }
|
|
232
|
+
expect(a2aMessageToCreateRun('a', params).config.permissionMode).toBe(mode)
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
})
|