@namzu/sdk 6.2.0 → 8.0.0
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 +677 -0
- package/dist/agents/ReactiveAgent.d.ts.map +1 -1
- package/dist/agents/ReactiveAgent.js +5 -0
- package/dist/agents/ReactiveAgent.js.map +1 -1
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js +172 -158
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/agents/__tests__/supervisor-inbox-scope.test.d.ts +2 -0
- package/dist/agents/__tests__/supervisor-inbox-scope.test.d.ts.map +1 -0
- package/dist/agents/__tests__/supervisor-inbox-scope.test.js +125 -0
- package/dist/agents/__tests__/supervisor-inbox-scope.test.js.map +1 -0
- package/dist/agents/runAgent.d.ts +19 -1
- package/dist/agents/runAgent.d.ts.map +1 -1
- package/dist/agents/runAgent.js +2 -0
- package/dist/agents/runAgent.js.map +1 -1
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +4 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +24 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/connector/mcp/__tests__/positional-arrays.test.d.ts +2 -0
- package/dist/connector/mcp/__tests__/positional-arrays.test.d.ts.map +1 -0
- package/dist/connector/mcp/__tests__/positional-arrays.test.js +142 -0
- package/dist/connector/mcp/__tests__/positional-arrays.test.js.map +1 -0
- package/dist/connector/mcp/adapter.d.ts.map +1 -1
- package/dist/connector/mcp/adapter.js +123 -8
- package/dist/connector/mcp/adapter.js.map +1 -1
- package/dist/constants/agent/index.d.ts +5 -0
- package/dist/constants/agent/index.d.ts.map +1 -1
- package/dist/constants/agent/index.js +5 -0
- package/dist/constants/agent/index.js.map +1 -1
- package/dist/constants/plugin/index.d.ts +15 -0
- package/dist/constants/plugin/index.d.ts.map +1 -1
- package/dist/constants/plugin/index.js +15 -0
- package/dist/constants/plugin/index.js.map +1 -1
- package/dist/contracts/api.d.ts +1 -1
- package/dist/contracts/api.d.ts.map +1 -1
- package/dist/gateway/__tests__/completion-inbox.test.js +292 -2
- package/dist/gateway/__tests__/completion-inbox.test.js.map +1 -1
- package/dist/gateway/completion-inbox.d.ts +94 -6
- package/dist/gateway/completion-inbox.d.ts.map +1 -1
- package/dist/gateway/completion-inbox.js +235 -15
- package/dist/gateway/completion-inbox.js.map +1 -1
- package/dist/gateway/local.d.ts +11 -0
- package/dist/gateway/local.d.ts.map +1 -1
- package/dist/gateway/local.js +27 -1
- package/dist/gateway/local.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +6 -0
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/run/persistence.d.ts +8 -0
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +12 -0
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/provider/thinking-support.d.ts +2 -1
- package/dist/provider/thinking-support.d.ts.map +1 -1
- package/dist/provider/thinking-support.js +14 -0
- package/dist/provider/thinking-support.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +9 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/run/reporter.d.ts.map +1 -1
- package/dist/run/reporter.js +11 -0
- package/dist/run/reporter.js.map +1 -1
- package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.js +142 -0
- package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.js.map +1 -0
- package/dist/runtime/query/__tests__/completion-notification.test.js +414 -32
- package/dist/runtime/query/__tests__/completion-notification.test.js.map +1 -1
- package/dist/runtime/query/__tests__/context-size-on-the-wire.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/context-size-on-the-wire.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/context-size-on-the-wire.test.js +100 -0
- package/dist/runtime/query/__tests__/context-size-on-the-wire.test.js.map +1 -0
- package/dist/runtime/query/__tests__/context.test.js +18 -0
- package/dist/runtime/query/__tests__/context.test.js.map +1 -1
- package/dist/runtime/query/__tests__/effort-reaches-the-wire.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/effort-reaches-the-wire.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/effort-reaches-the-wire.test.js +118 -0
- package/dist/runtime/query/__tests__/effort-reaches-the-wire.test.js.map +1 -0
- package/dist/runtime/query/__tests__/tool-timeout.test.js +34 -0
- package/dist/runtime/query/__tests__/tool-timeout.test.js.map +1 -1
- package/dist/runtime/query/context.d.ts.map +1 -1
- package/dist/runtime/query/context.js +16 -1
- package/dist/runtime/query/context.js.map +1 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +11 -1
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/guard.d.ts +28 -0
- package/dist/runtime/query/guard.d.ts.map +1 -1
- package/dist/runtime/query/guard.js +31 -0
- package/dist/runtime/query/guard.js.map +1 -1
- package/dist/runtime/query/iteration/__tests__/settle-grace.test.d.ts +2 -0
- package/dist/runtime/query/iteration/__tests__/settle-grace.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/__tests__/settle-grace.test.js +226 -0
- package/dist/runtime/query/iteration/__tests__/settle-grace.test.js.map +1 -0
- package/dist/runtime/query/iteration/index.d.ts +92 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +818 -565
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.js +95 -0
- package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts +34 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +61 -4
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/telemetry/__tests__/model-call-span.test.js +22 -4
- package/dist/telemetry/__tests__/model-call-span.test.js.map +1 -1
- package/dist/telemetry/__tests__/span-closure.test.js +12 -5
- package/dist/telemetry/__tests__/span-closure.test.js.map +1 -1
- package/dist/tools/__tests__/untrusted-envelope.test.js +16 -0
- package/dist/tools/__tests__/untrusted-envelope.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/completion-delivery.test.js +117 -0
- package/dist/tools/coordinator/__tests__/completion-delivery.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/task-list.test.js +57 -0
- package/dist/tools/coordinator/__tests__/task-list.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/wait-with-idle-bound.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/wait-with-idle-bound.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/wait-with-idle-bound.test.js +193 -0
- package/dist/tools/coordinator/__tests__/wait-with-idle-bound.test.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts +19 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +191 -71
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/coordinator/wait-with-idle-bound.d.ts +66 -0
- package/dist/tools/coordinator/wait-with-idle-bound.d.ts.map +1 -0
- package/dist/tools/coordinator/wait-with-idle-bound.js +78 -0
- package/dist/tools/coordinator/wait-with-idle-bound.js.map +1 -0
- package/dist/tools/untrusted-envelope.d.ts.map +1 -1
- package/dist/tools/untrusted-envelope.js +9 -1
- package/dist/tools/untrusted-envelope.js.map +1 -1
- package/dist/types/agent/base.d.ts +16 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/agent/gateway.d.ts +41 -0
- package/dist/types/agent/gateway.d.ts.map +1 -1
- package/dist/types/agent/lifecycle-event.d.ts +9 -1
- package/dist/types/agent/lifecycle-event.d.ts.map +1 -1
- package/dist/types/agent/task.d.ts +5 -0
- package/dist/types/agent/task.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +10 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/probe/registry.d.ts +6 -0
- package/dist/types/probe/registry.d.ts.map +1 -1
- package/dist/types/provider/interface.d.ts +35 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/run/config.d.ts +25 -0
- package/dist/types/run/config.d.ts.map +1 -1
- package/dist/types/run/entity.d.ts +16 -0
- package/dist/types/run/entity.d.ts.map +1 -1
- package/dist/types/run/events.d.ts +75 -0
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/run/events.js.map +1 -1
- package/dist/types/run/prepare-step.d.ts +17 -2
- package/dist/types/run/prepare-step.d.ts.map +1 -1
- package/dist/types/verification/index.d.ts +98 -0
- package/dist/types/verification/index.d.ts.map +1 -1
- package/dist/types/verification/index.js +10 -0
- package/dist/types/verification/index.js.map +1 -1
- package/dist/utils/__tests__/abort-reason.test.d.ts +2 -0
- package/dist/utils/__tests__/abort-reason.test.d.ts.map +1 -0
- package/dist/utils/__tests__/abort-reason.test.js +48 -0
- package/dist/utils/__tests__/abort-reason.test.js.map +1 -0
- package/dist/utils/abort.d.ts +26 -0
- package/dist/utils/abort.d.ts.map +1 -1
- package/dist/utils/abort.js +34 -0
- package/dist/utils/abort.js.map +1 -1
- package/dist/verification/__tests__/argument-pattern.test.d.ts +2 -0
- package/dist/verification/__tests__/argument-pattern.test.d.ts.map +1 -0
- package/dist/verification/__tests__/argument-pattern.test.js +122 -0
- package/dist/verification/__tests__/argument-pattern.test.js.map +1 -0
- package/dist/verification/__tests__/rule-order-and-reason.test.d.ts +2 -0
- package/dist/verification/__tests__/rule-order-and-reason.test.d.ts.map +1 -0
- package/dist/verification/__tests__/rule-order-and-reason.test.js +126 -0
- package/dist/verification/__tests__/rule-order-and-reason.test.js.map +1 -0
- package/dist/verification/gate.d.ts +17 -1
- package/dist/verification/gate.d.ts.map +1 -1
- package/dist/verification/gate.js +102 -2
- package/dist/verification/gate.js.map +1 -1
- package/dist/verification/index.d.ts +1 -1
- package/dist/verification/index.d.ts.map +1 -1
- package/dist/verification/index.js +1 -1
- package/dist/verification/index.js.map +1 -1
- package/dist/verification/rules.d.ts.map +1 -1
- package/dist/verification/rules.js +27 -0
- package/dist/verification/rules.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/ReactiveAgent.ts +5 -0
- package/src/agents/SupervisorAgent.ts +175 -162
- package/src/agents/__tests__/supervisor-inbox-scope.test.ts +149 -0
- package/src/agents/runAgent.ts +22 -1
- package/src/bridge/a2a/mapper.ts +4 -0
- package/src/bridge/sse/mapper.ts +25 -0
- package/src/connector/mcp/__tests__/positional-arrays.test.ts +183 -0
- package/src/connector/mcp/adapter.ts +131 -7
- package/src/constants/agent/index.ts +5 -0
- package/src/constants/plugin/index.ts +15 -0
- package/src/contracts/api.ts +1 -0
- package/src/gateway/__tests__/completion-inbox.test.ts +348 -2
- package/src/gateway/completion-inbox.ts +248 -16
- package/src/gateway/local.ts +26 -1
- package/src/manager/agent/lifecycle.ts +6 -0
- package/src/manager/run/persistence.ts +12 -0
- package/src/provider/thinking-support.ts +19 -2
- package/src/public-runtime.ts +9 -0
- package/src/run/reporter.ts +12 -0
- package/src/runtime/query/__tests__/completion-does-not-erase-the-answer.test.ts +163 -0
- package/src/runtime/query/__tests__/completion-notification.test.ts +486 -34
- package/src/runtime/query/__tests__/context-size-on-the-wire.test.ts +122 -0
- package/src/runtime/query/__tests__/context.test.ts +24 -0
- package/src/runtime/query/__tests__/effort-reaches-the-wire.test.ts +135 -0
- package/src/runtime/query/__tests__/tool-timeout.test.ts +38 -0
- package/src/runtime/query/context.ts +16 -1
- package/src/runtime/query/executor.ts +11 -1
- package/src/runtime/query/guard.ts +32 -0
- package/src/runtime/query/iteration/__tests__/settle-grace.test.ts +265 -0
- package/src/runtime/query/iteration/index.ts +906 -635
- package/src/runtime/query/iteration/phases/__tests__/compaction-declined.test.ts +124 -0
- package/src/runtime/query/iteration/phases/compaction.ts +83 -10
- package/src/telemetry/__tests__/model-call-span.test.ts +22 -5
- package/src/telemetry/__tests__/span-closure.test.ts +12 -5
- package/src/tools/__tests__/untrusted-envelope.test.ts +23 -0
- package/src/tools/coordinator/__tests__/completion-delivery.test.ts +147 -0
- package/src/tools/coordinator/__tests__/task-list.test.ts +72 -0
- package/src/tools/coordinator/__tests__/wait-with-idle-bound.test.ts +247 -0
- package/src/tools/coordinator/index.ts +205 -78
- package/src/tools/coordinator/wait-with-idle-bound.ts +142 -0
- package/src/tools/untrusted-envelope.ts +9 -1
- package/src/types/agent/base.ts +17 -0
- package/src/types/agent/gateway.ts +42 -0
- package/src/types/agent/lifecycle-event.ts +7 -0
- package/src/types/agent/task.ts +5 -0
- package/src/types/hitl/index.ts +10 -0
- package/src/types/probe/registry.ts +6 -0
- package/src/types/provider/interface.ts +39 -0
- package/src/types/run/config.ts +26 -0
- package/src/types/run/entity.ts +17 -0
- package/src/types/run/events.ts +75 -0
- package/src/types/run/prepare-step.ts +17 -2
- package/src/types/verification/index.ts +61 -0
- package/src/utils/__tests__/abort-reason.test.ts +56 -0
- package/src/utils/abort.ts +34 -0
- package/src/verification/__tests__/argument-pattern.test.ts +158 -0
- package/src/verification/__tests__/rule-order-and-reason.test.ts +149 -0
- package/src/verification/gate.ts +106 -3
- package/src/verification/index.ts +1 -1
- package/src/verification/rules.ts +28 -0
|
@@ -10,7 +10,12 @@ import { defineTool } from '../../../tools/defineTool.js'
|
|
|
10
10
|
import type { TaskHandle } from '../../../types/agent/gateway.js'
|
|
11
11
|
import type { SessionId, TaskId, TenantId } from '../../../types/ids/index.js'
|
|
12
12
|
import { createUserMessage } from '../../../types/message/index.js'
|
|
13
|
-
import type {
|
|
13
|
+
import type { Message } from '../../../types/message/index.js'
|
|
14
|
+
import type {
|
|
15
|
+
ChatCompletionParams,
|
|
16
|
+
LLMProvider,
|
|
17
|
+
StreamChunk,
|
|
18
|
+
} from '../../../types/provider/index.js'
|
|
14
19
|
import type { ProjectId, ThreadId } from '../../../types/session/ids.js'
|
|
15
20
|
import { drainQuery } from '../index.js'
|
|
16
21
|
|
|
@@ -39,9 +44,19 @@ class ToolThenAnswerProvider implements LLMProvider {
|
|
|
39
44
|
readonly id = 'tool-then-answer'
|
|
40
45
|
readonly name = 'Tool Then Answer Provider'
|
|
41
46
|
calls = 0
|
|
47
|
+
/**
|
|
48
|
+
* What the model was actually SHOWN, turn by turn.
|
|
49
|
+
*
|
|
50
|
+
* `run.messages` proves a notification exists somewhere in the transcript.
|
|
51
|
+
* It does not prove the model ever saw it — a delivery that only happens on
|
|
52
|
+
* the way out satisfies the transcript and leaves the model with nothing to
|
|
53
|
+
* act on, which is the whole point of injecting mid-run.
|
|
54
|
+
*/
|
|
55
|
+
readonly requests: Message[][] = []
|
|
42
56
|
|
|
43
|
-
async *chatStream(): AsyncIterable<StreamChunk> {
|
|
57
|
+
async *chatStream(params: ChatCompletionParams): AsyncIterable<StreamChunk> {
|
|
44
58
|
this.calls += 1
|
|
59
|
+
this.requests.push([...params.messages])
|
|
45
60
|
|
|
46
61
|
if (this.calls === 1) {
|
|
47
62
|
yield {
|
|
@@ -71,6 +86,43 @@ class ToolThenAnswerProvider implements LLMProvider {
|
|
|
71
86
|
}
|
|
72
87
|
}
|
|
73
88
|
|
|
89
|
+
/** The same two turns, aimed at the tool the exit tests register. */
|
|
90
|
+
class CallsFinisherProvider implements LLMProvider {
|
|
91
|
+
readonly id = 'calls-finisher'
|
|
92
|
+
readonly name = 'Calls Finisher Provider'
|
|
93
|
+
calls = 0
|
|
94
|
+
|
|
95
|
+
async *chatStream(): AsyncIterable<StreamChunk> {
|
|
96
|
+
this.calls += 1
|
|
97
|
+
|
|
98
|
+
if (this.calls === 1) {
|
|
99
|
+
yield {
|
|
100
|
+
id: 'msg_1',
|
|
101
|
+
delta: {
|
|
102
|
+
toolCalls: [
|
|
103
|
+
{
|
|
104
|
+
index: 0,
|
|
105
|
+
id: 'toolu_finisher_1',
|
|
106
|
+
type: 'function',
|
|
107
|
+
function: { name: 'finisher', arguments: '{}' },
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
yield {
|
|
113
|
+
id: 'msg_1',
|
|
114
|
+
delta: {},
|
|
115
|
+
finishReason: 'tool_calls',
|
|
116
|
+
usage: ZERO_USAGE,
|
|
117
|
+
}
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
yield { id: 'msg_2', delta: { content: 'Done.' } }
|
|
122
|
+
yield { id: 'msg_2', delta: {}, finishReason: 'stop', usage: ZERO_USAGE }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
74
126
|
const noop = defineTool({
|
|
75
127
|
name: 'noop',
|
|
76
128
|
description: 'does nothing',
|
|
@@ -102,15 +154,18 @@ afterEach(async () => {
|
|
|
102
154
|
workdirs.length = 0
|
|
103
155
|
})
|
|
104
156
|
|
|
105
|
-
async function runWith(
|
|
157
|
+
async function runWith(
|
|
158
|
+
inbox: CompletionInbox | undefined,
|
|
159
|
+
): Promise<{ userMessages: string[]; provider: ToolThenAnswerProvider }> {
|
|
106
160
|
const workingDirectory = await mkdtemp(join(tmpdir(), 'namzu-completion-'))
|
|
107
161
|
workdirs.push(workingDirectory)
|
|
108
162
|
|
|
109
163
|
const tools = new ToolRegistry()
|
|
110
164
|
tools.register(noop)
|
|
111
165
|
|
|
166
|
+
const provider = new ToolThenAnswerProvider()
|
|
112
167
|
const run = await drainQuery({
|
|
113
|
-
provider
|
|
168
|
+
provider,
|
|
114
169
|
tools,
|
|
115
170
|
...(inbox ? { completionInbox: inbox } : {}),
|
|
116
171
|
agentId: 'agent_test',
|
|
@@ -130,24 +185,36 @@ async function runWith(inbox: CompletionInbox | undefined): Promise<string[]> {
|
|
|
130
185
|
tenantId: 'tnt_completion' as TenantId,
|
|
131
186
|
})
|
|
132
187
|
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
188
|
+
return {
|
|
189
|
+
userMessages: run.messages
|
|
190
|
+
.filter((m) => m.role === 'user')
|
|
191
|
+
.map((m) => (typeof m.content === 'string' ? m.content : JSON.stringify(m.content))),
|
|
192
|
+
provider,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Settled before the turn ended, with nothing waiting on it. */
|
|
197
|
+
function inboxHolding(taskId: string, result: string): CompletionInbox {
|
|
198
|
+
const inbox = new CompletionInbox()
|
|
199
|
+
// Said before the announcement because `create_task` says it on every
|
|
200
|
+
// launch: an inbox only hears about tasks its own run started, so a
|
|
201
|
+
// gateway shared between two supervisors cannot cross-deliver.
|
|
202
|
+
inbox.launched(taskId as TaskId)
|
|
203
|
+
inbox.attach({
|
|
204
|
+
onTaskCompleted: (cb: (h: TaskHandle) => void) => {
|
|
205
|
+
cb(completed(taskId, result))
|
|
206
|
+
return () => {}
|
|
207
|
+
},
|
|
208
|
+
getTask: () => undefined,
|
|
209
|
+
} as never)
|
|
210
|
+
return inbox
|
|
136
211
|
}
|
|
137
212
|
|
|
138
213
|
describe('an unclaimed completion reaches the transcript', () => {
|
|
139
214
|
it('injects the notification as a user message the next turn can read', async () => {
|
|
140
|
-
const inbox =
|
|
141
|
-
// Settled by a gateway before the turn ended, with nothing waiting on
|
|
142
|
-
// it — the abandoned-launch case.
|
|
143
|
-
inbox.attach({
|
|
144
|
-
onTaskCompleted: (cb: (h: TaskHandle) => void) => {
|
|
145
|
-
cb(completed('tsk_late', 'the worker finished after the wait was abandoned'))
|
|
146
|
-
return () => {}
|
|
147
|
-
},
|
|
148
|
-
} as never)
|
|
215
|
+
const inbox = inboxHolding('tsk_late', 'the worker finished after the wait was abandoned')
|
|
149
216
|
|
|
150
|
-
const userMessages = await runWith(inbox)
|
|
217
|
+
const { userMessages } = await runWith(inbox)
|
|
151
218
|
const notification = userMessages.find((m) => m.includes('task-notification'))
|
|
152
219
|
|
|
153
220
|
expect(notification).toBeDefined()
|
|
@@ -155,16 +222,36 @@ describe('an unclaimed completion reaches the transcript', () => {
|
|
|
155
222
|
expect(notification).toContain('the worker finished after the wait was abandoned')
|
|
156
223
|
})
|
|
157
224
|
|
|
225
|
+
it('shows it to the model on the very next turn, not eventually', async () => {
|
|
226
|
+
// Three sites now drain this inbox — the post-tool-batch injection, the
|
|
227
|
+
// bounded hold on the final-answer turn, and the exit-time delivery —
|
|
228
|
+
// and all three put the same text in `run.messages`. So an assertion on
|
|
229
|
+
// the transcript, or on the LAST model request, passes whichever of the
|
|
230
|
+
// three did the work, and none of them is pinned.
|
|
231
|
+
//
|
|
232
|
+
// The one that matters is the injection right after the tool batch,
|
|
233
|
+
// because it is the only one that reaches the model while there is
|
|
234
|
+
// still work to redirect. Hence the SECOND request specifically: with
|
|
235
|
+
// that injection gone the notification still arrives, one turn later,
|
|
236
|
+
// via the hold — later is a different behaviour, and this is the
|
|
237
|
+
// assertion that can tell them apart.
|
|
238
|
+
const inbox = inboxHolding('tsk_late', 'THE WORKER SAID THIS')
|
|
239
|
+
|
|
240
|
+
const { provider } = await runWith(inbox)
|
|
241
|
+
|
|
242
|
+
const turnAfterTheToolBatch = provider.requests[1] ?? []
|
|
243
|
+
expect(
|
|
244
|
+
turnAfterTheToolBatch.some(
|
|
245
|
+
(m) => typeof m.content === 'string' && m.content.includes('THE WORKER SAID THIS'),
|
|
246
|
+
),
|
|
247
|
+
'the model was not shown the completion on the turn after the tool batch',
|
|
248
|
+
).toBe(true)
|
|
249
|
+
})
|
|
250
|
+
|
|
158
251
|
it('drains it exactly once, however many turns follow', async () => {
|
|
159
|
-
const inbox =
|
|
160
|
-
inbox.attach({
|
|
161
|
-
onTaskCompleted: (cb: (h: TaskHandle) => void) => {
|
|
162
|
-
cb(completed('tsk_late', 'only once'))
|
|
163
|
-
return () => {}
|
|
164
|
-
},
|
|
165
|
-
} as never)
|
|
252
|
+
const inbox = inboxHolding('tsk_late', 'only once')
|
|
166
253
|
|
|
167
|
-
const userMessages = await runWith(inbox)
|
|
254
|
+
const { userMessages } = await runWith(inbox)
|
|
168
255
|
|
|
169
256
|
expect(userMessages.filter((m) => m.includes('task-notification'))).toHaveLength(1)
|
|
170
257
|
})
|
|
@@ -172,16 +259,10 @@ describe('an unclaimed completion reaches the transcript', () => {
|
|
|
172
259
|
it('says nothing when every completion was already delivered', async () => {
|
|
173
260
|
// The `dc16d58` regression at the loop level: a blocking `create_task`
|
|
174
261
|
// claims its own completion, so the transcript must stay clean.
|
|
175
|
-
const inbox =
|
|
176
|
-
inbox.attach({
|
|
177
|
-
onTaskCompleted: (cb: (h: TaskHandle) => void) => {
|
|
178
|
-
cb(completed('tsk_awaited', 'delivered as a tool_result'))
|
|
179
|
-
return () => {}
|
|
180
|
-
},
|
|
181
|
-
} as never)
|
|
262
|
+
const inbox = inboxHolding('tsk_awaited', 'delivered as a tool_result')
|
|
182
263
|
inbox.claim('tsk_awaited' as TaskId)
|
|
183
264
|
|
|
184
|
-
const userMessages = await runWith(inbox)
|
|
265
|
+
const { userMessages } = await runWith(inbox)
|
|
185
266
|
|
|
186
267
|
expect(userMessages.some((m) => m.includes('task-notification'))).toBe(false)
|
|
187
268
|
})
|
|
@@ -189,8 +270,379 @@ describe('an unclaimed completion reaches the transcript', () => {
|
|
|
189
270
|
it('runs unchanged with no inbox at all', async () => {
|
|
190
271
|
// The kernel must not require one: a host on the old wiring keeps
|
|
191
272
|
// working, it just never hears about abandoned completions.
|
|
192
|
-
const userMessages = await runWith(undefined)
|
|
273
|
+
const { userMessages } = await runWith(undefined)
|
|
193
274
|
|
|
194
275
|
expect(userMessages.some((m) => m.includes('task-notification'))).toBe(false)
|
|
195
276
|
})
|
|
196
277
|
})
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The exits that are not the ordinary final answer.
|
|
281
|
+
*
|
|
282
|
+
* The inbox was consulted at exactly one site, inside the no-tool-calls branch.
|
|
283
|
+
* The loop leaves by eight other routes, and three of them are ways a run
|
|
284
|
+
* legitimately ENDS: a tool the author declared terminal, a captured structured
|
|
285
|
+
* output, and the host's `stopWhen`. A worker that finished while any of those
|
|
286
|
+
* was deciding had its output dropped on the floor — the gateway held the
|
|
287
|
+
* result, the run closed, and nothing ever read it.
|
|
288
|
+
*
|
|
289
|
+
* These drive `drainQuery` rather than the helper directly, and that is the
|
|
290
|
+
* point: the delivery happens in the loop, so a unit test on the inbox proves
|
|
291
|
+
* nothing about whether the loop reaches it.
|
|
292
|
+
*/
|
|
293
|
+
describe('a run that ends some other way still hands over what finished', () => {
|
|
294
|
+
async function runEndingWith(options: {
|
|
295
|
+
terminal?: boolean
|
|
296
|
+
stopWhen?: boolean
|
|
297
|
+
}): Promise<{ userMessages: string[]; run: Awaited<ReturnType<typeof drainQuery>> }> {
|
|
298
|
+
const workingDirectory = await mkdtemp(join(tmpdir(), 'namzu-completion-exit-'))
|
|
299
|
+
workdirs.push(workingDirectory)
|
|
300
|
+
|
|
301
|
+
const inbox = new CompletionInbox()
|
|
302
|
+
inbox.launched('tsk_bg' as TaskId)
|
|
303
|
+
let announce: ((h: TaskHandle) => void) | undefined
|
|
304
|
+
inbox.attach({
|
|
305
|
+
onTaskCompleted: (cb: (h: TaskHandle) => void) => {
|
|
306
|
+
announce = cb
|
|
307
|
+
return () => {
|
|
308
|
+
announce = undefined
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
getTask: () => undefined,
|
|
312
|
+
} as never)
|
|
313
|
+
|
|
314
|
+
// Settles from inside the tool call, so the completion is in hand
|
|
315
|
+
// BEFORE the exit is decided. The question under test is which exits
|
|
316
|
+
// hand it over, not whether the wait works.
|
|
317
|
+
const finisher = defineTool({
|
|
318
|
+
name: 'finisher',
|
|
319
|
+
description: 'the worker finishes while this runs',
|
|
320
|
+
inputSchema: z.object({}),
|
|
321
|
+
category: 'analysis',
|
|
322
|
+
permissions: [],
|
|
323
|
+
readOnly: true,
|
|
324
|
+
destructive: false,
|
|
325
|
+
concurrencySafe: true,
|
|
326
|
+
...(options.terminal ? { terminal: true } : {}),
|
|
327
|
+
async execute() {
|
|
328
|
+
announce?.(completed('tsk_bg', 'THE BACKGROUND WORKER RESULT'))
|
|
329
|
+
return { success: true, output: 'this call is the answer' }
|
|
330
|
+
},
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
const tools = new ToolRegistry()
|
|
334
|
+
tools.register(finisher)
|
|
335
|
+
|
|
336
|
+
const run = await drainQuery({
|
|
337
|
+
provider: new CallsFinisherProvider(),
|
|
338
|
+
tools,
|
|
339
|
+
completionInbox: inbox,
|
|
340
|
+
agentId: 'agent_test',
|
|
341
|
+
agentName: 'Test Agent',
|
|
342
|
+
messages: [createUserMessage('delegate and report')],
|
|
343
|
+
workingDirectory,
|
|
344
|
+
...(options.stopWhen ? { stopWhen: () => true } : {}),
|
|
345
|
+
runConfig: {
|
|
346
|
+
model: 'mock-model',
|
|
347
|
+
timeoutMs: 10_000,
|
|
348
|
+
tokenBudget: 100_000,
|
|
349
|
+
maxIterations: 4,
|
|
350
|
+
maxResponseTokens: 256,
|
|
351
|
+
},
|
|
352
|
+
sessionId: 'ses_completion' as SessionId,
|
|
353
|
+
threadId: 'thd_completion' as ThreadId,
|
|
354
|
+
projectId: 'prj_completion' as ProjectId,
|
|
355
|
+
tenantId: 'tnt_completion' as TenantId,
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
userMessages: run.messages
|
|
360
|
+
.filter((m) => m.role === 'user')
|
|
361
|
+
.map((m) => (typeof m.content === 'string' ? m.content : JSON.stringify(m.content))),
|
|
362
|
+
run,
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
it('a terminal tool settles the run without discarding the worker', async () => {
|
|
367
|
+
const { userMessages } = await runEndingWith({ terminal: true })
|
|
368
|
+
|
|
369
|
+
expect(
|
|
370
|
+
userMessages.some((m) => m.includes('THE BACKGROUND WORKER RESULT')),
|
|
371
|
+
'the terminal-tool exit dropped a finished completion',
|
|
372
|
+
).toBe(true)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it("the host's stopWhen ends the run without discarding it either", async () => {
|
|
376
|
+
const { userMessages } = await runEndingWith({ stopWhen: true })
|
|
377
|
+
|
|
378
|
+
expect(
|
|
379
|
+
userMessages.some((m) => m.includes('THE BACKGROUND WORKER RESULT')),
|
|
380
|
+
'the stopWhen exit dropped a finished completion',
|
|
381
|
+
).toBe(true)
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('lets a worker that has not arrived yet outrank the host stop predicate, once', async () => {
|
|
385
|
+
// A chosen precedence, not something `stopWhen` implies. A stop
|
|
386
|
+
// predicate is a programmable halt that says nothing about whether the
|
|
387
|
+
// answer is complete — unlike a terminal tool or a captured structured
|
|
388
|
+
// output, which decide the result and leave no turn to use anything in.
|
|
389
|
+
// So this one waits, delivers, and gives the model the turn; the
|
|
390
|
+
// predicate fires again next turn with nothing pending and stops. One
|
|
391
|
+
// extra turn, bounded.
|
|
392
|
+
const workingDirectory = await mkdtemp(join(tmpdir(), 'namzu-stopwhen-hold-'))
|
|
393
|
+
workdirs.push(workingDirectory)
|
|
394
|
+
|
|
395
|
+
const inbox = new CompletionInbox()
|
|
396
|
+
let announce: ((h: TaskHandle) => void) | undefined
|
|
397
|
+
inbox.attach({
|
|
398
|
+
onTaskCompleted: (cb: (h: TaskHandle) => void) => {
|
|
399
|
+
announce = cb
|
|
400
|
+
return () => {
|
|
401
|
+
announce = undefined
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
getTask: () => undefined,
|
|
405
|
+
} as never)
|
|
406
|
+
// OUTSTANDING, not arrived: the exit-time delivery has nothing to hand
|
|
407
|
+
// over, so only a hold can produce this result.
|
|
408
|
+
inbox.expect('tsk_slow' as TaskId)
|
|
409
|
+
|
|
410
|
+
const tools = new ToolRegistry()
|
|
411
|
+
tools.register(
|
|
412
|
+
defineTool({
|
|
413
|
+
name: 'finisher',
|
|
414
|
+
description: 'the worker is still going when this returns',
|
|
415
|
+
inputSchema: z.object({}),
|
|
416
|
+
category: 'analysis',
|
|
417
|
+
permissions: [],
|
|
418
|
+
readOnly: true,
|
|
419
|
+
destructive: false,
|
|
420
|
+
concurrencySafe: true,
|
|
421
|
+
async execute() {
|
|
422
|
+
setTimeout(() => announce?.(completed('tsk_slow', 'LATE WORKER RESULT')), 20).unref?.()
|
|
423
|
+
return { success: true, output: 'launched' }
|
|
424
|
+
},
|
|
425
|
+
}),
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
const provider = new CallsFinisherProvider()
|
|
429
|
+
const run = await drainQuery({
|
|
430
|
+
provider,
|
|
431
|
+
tools,
|
|
432
|
+
completionInbox: inbox,
|
|
433
|
+
agentId: 'agent_test',
|
|
434
|
+
agentName: 'Test Agent',
|
|
435
|
+
messages: [createUserMessage('go')],
|
|
436
|
+
workingDirectory,
|
|
437
|
+
stopWhen: () => true,
|
|
438
|
+
runConfig: {
|
|
439
|
+
model: 'mock-model',
|
|
440
|
+
timeoutMs: 20_000,
|
|
441
|
+
tokenBudget: 100_000,
|
|
442
|
+
maxIterations: 4,
|
|
443
|
+
maxResponseTokens: 256,
|
|
444
|
+
},
|
|
445
|
+
sessionId: 'ses_completion' as SessionId,
|
|
446
|
+
threadId: 'thd_completion' as ThreadId,
|
|
447
|
+
projectId: 'prj_completion' as ProjectId,
|
|
448
|
+
tenantId: 'tnt_completion' as TenantId,
|
|
449
|
+
} as never)
|
|
450
|
+
|
|
451
|
+
const userText = (run.messages as { role: string; content: unknown }[])
|
|
452
|
+
.filter((m) => m.role === 'user')
|
|
453
|
+
.map((m) => (typeof m.content === 'string' ? m.content : ''))
|
|
454
|
+
|
|
455
|
+
expect(
|
|
456
|
+
userText.some((m) => m.includes('LATE WORKER RESULT')),
|
|
457
|
+
'the stop predicate discarded a worker that had not landed yet',
|
|
458
|
+
).toBe(true)
|
|
459
|
+
// The extra turn is the point: the model was asked again, once.
|
|
460
|
+
expect(provider.calls, 'the model never got a turn to use the result').toBe(2)
|
|
461
|
+
|
|
462
|
+
// And the run says WHY it is over. The extra turn is prose, and
|
|
463
|
+
// `stopWhen` is consulted only after a tool batch — deliberately, so a
|
|
464
|
+
// predicate can see what the tools returned — so the predicate is never
|
|
465
|
+
// asked again and the run leaves by the ordinary route. Reporting
|
|
466
|
+
// `end_turn` there would name the shape of the last message rather than
|
|
467
|
+
// the host's decision, and this repo carries thirteen `StopReason`
|
|
468
|
+
// values precisely so a run that ends for a nameable reason names it.
|
|
469
|
+
expect(run.stopReason).toBe('stop_condition')
|
|
470
|
+
}, 60_000)
|
|
471
|
+
|
|
472
|
+
it('forgets the deferral if the extra turn did not end the run', async () => {
|
|
473
|
+
// The lifetime is the whole safety of the flag. It is set at the end of
|
|
474
|
+
// one iteration and read by the next, and the next clears it whether or
|
|
475
|
+
// not anything read it — so a deferral cannot colour a stop reason two
|
|
476
|
+
// turns later, when the predicate has since been asked again and said
|
|
477
|
+
// no. A flag that outlived its continuation would put `stop_condition`
|
|
478
|
+
// on a run the host had stopped asking to stop.
|
|
479
|
+
const workingDirectory = await mkdtemp(join(tmpdir(), 'namzu-defer-life-'))
|
|
480
|
+
workdirs.push(workingDirectory)
|
|
481
|
+
|
|
482
|
+
const inbox = new CompletionInbox()
|
|
483
|
+
let announce: ((h: TaskHandle) => void) | undefined
|
|
484
|
+
inbox.attach({
|
|
485
|
+
onTaskCompleted: (cb: (h: TaskHandle) => void) => {
|
|
486
|
+
announce = cb
|
|
487
|
+
return () => {
|
|
488
|
+
announce = undefined
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
getTask: () => undefined,
|
|
492
|
+
} as never)
|
|
493
|
+
inbox.expect('tsk_slow' as TaskId)
|
|
494
|
+
|
|
495
|
+
const tools = new ToolRegistry()
|
|
496
|
+
tools.register(
|
|
497
|
+
defineTool({
|
|
498
|
+
name: 'finisher',
|
|
499
|
+
description: 'launches, then does nothing',
|
|
500
|
+
inputSchema: z.object({}),
|
|
501
|
+
category: 'analysis',
|
|
502
|
+
permissions: [],
|
|
503
|
+
readOnly: true,
|
|
504
|
+
destructive: false,
|
|
505
|
+
concurrencySafe: true,
|
|
506
|
+
async execute() {
|
|
507
|
+
setTimeout(() => announce?.(completed('tsk_slow', 'LATE RESULT')), 20).unref?.()
|
|
508
|
+
return { success: true, output: 'ok' }
|
|
509
|
+
},
|
|
510
|
+
}),
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
/** Tool, tool, prose — so the predicate is asked twice. */
|
|
514
|
+
class ToolToolProse implements LLMProvider {
|
|
515
|
+
readonly id = 'tool-tool-prose'
|
|
516
|
+
readonly name = 'Tool Tool Prose'
|
|
517
|
+
calls = 0
|
|
518
|
+
async *chatStream(): AsyncIterable<StreamChunk> {
|
|
519
|
+
this.calls += 1
|
|
520
|
+
if (this.calls <= 2) {
|
|
521
|
+
yield {
|
|
522
|
+
id: `m${this.calls}`,
|
|
523
|
+
delta: {
|
|
524
|
+
toolCalls: [
|
|
525
|
+
{
|
|
526
|
+
index: 0,
|
|
527
|
+
id: `toolu_${this.calls}`,
|
|
528
|
+
type: 'function',
|
|
529
|
+
function: { name: 'finisher', arguments: '{}' },
|
|
530
|
+
},
|
|
531
|
+
],
|
|
532
|
+
},
|
|
533
|
+
}
|
|
534
|
+
yield { id: `m${this.calls}`, delta: {}, finishReason: 'tool_calls', usage: ZERO_USAGE }
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
yield { id: 'm3', delta: { content: 'Done.' } }
|
|
538
|
+
yield { id: 'm3', delta: {}, finishReason: 'stop', usage: ZERO_USAGE }
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// True once — the deferral — then never again.
|
|
543
|
+
let asked = 0
|
|
544
|
+
const run = await drainQuery({
|
|
545
|
+
provider: new ToolToolProse(),
|
|
546
|
+
tools,
|
|
547
|
+
completionInbox: inbox,
|
|
548
|
+
agentId: 'agent_test',
|
|
549
|
+
agentName: 'Test Agent',
|
|
550
|
+
messages: [createUserMessage('go')],
|
|
551
|
+
workingDirectory,
|
|
552
|
+
stopWhen: () => ++asked === 1,
|
|
553
|
+
runConfig: {
|
|
554
|
+
model: 'mock-model',
|
|
555
|
+
timeoutMs: 20_000,
|
|
556
|
+
tokenBudget: 100_000,
|
|
557
|
+
maxIterations: 6,
|
|
558
|
+
maxResponseTokens: 256,
|
|
559
|
+
},
|
|
560
|
+
sessionId: 'ses_completion' as SessionId,
|
|
561
|
+
threadId: 'thd_completion' as ThreadId,
|
|
562
|
+
projectId: 'prj_completion' as ProjectId,
|
|
563
|
+
tenantId: 'tnt_completion' as TenantId,
|
|
564
|
+
} as never)
|
|
565
|
+
|
|
566
|
+
expect(asked, 'the predicate was never asked a second time').toBeGreaterThan(1)
|
|
567
|
+
expect(run.stopReason, 'a spent deferral coloured a later stop reason').toBe('end_turn')
|
|
568
|
+
}, 60_000)
|
|
569
|
+
|
|
570
|
+
it('names the work it walked away from, without cancelling it', async () => {
|
|
571
|
+
// The other half of the principle. Delivering what arrived is honest;
|
|
572
|
+
// staying silent about what did not is the false impression the whole
|
|
573
|
+
// mechanism exists to prevent. Naming rather than cancelling, because
|
|
574
|
+
// "the parent answered early" is a weaker warrant for killing a child
|
|
575
|
+
// than "the clock ran out" — and this subsystem already ruled that the
|
|
576
|
+
// clock does not license it.
|
|
577
|
+
const workingDirectory = await mkdtemp(join(tmpdir(), 'namzu-abandoned-'))
|
|
578
|
+
workdirs.push(workingDirectory)
|
|
579
|
+
|
|
580
|
+
const inbox = new CompletionInbox()
|
|
581
|
+
inbox.attach({ onTaskCompleted: () => () => {}, getTask: () => undefined } as never)
|
|
582
|
+
// Launched, never settles, and the terminal tool ends the run over it.
|
|
583
|
+
inbox.expect('tsk_still_running' as TaskId)
|
|
584
|
+
|
|
585
|
+
const tools = new ToolRegistry()
|
|
586
|
+
tools.register(
|
|
587
|
+
defineTool({
|
|
588
|
+
name: 'finisher',
|
|
589
|
+
description: 'ends the run',
|
|
590
|
+
inputSchema: z.object({}),
|
|
591
|
+
category: 'analysis',
|
|
592
|
+
permissions: [],
|
|
593
|
+
readOnly: true,
|
|
594
|
+
destructive: false,
|
|
595
|
+
concurrencySafe: true,
|
|
596
|
+
terminal: true,
|
|
597
|
+
async execute() {
|
|
598
|
+
return { success: true, output: 'this call is the answer' }
|
|
599
|
+
},
|
|
600
|
+
} as never),
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
const run = await drainQuery({
|
|
604
|
+
provider: new CallsFinisherProvider(),
|
|
605
|
+
tools,
|
|
606
|
+
completionInbox: inbox,
|
|
607
|
+
agentId: 'agent_test',
|
|
608
|
+
agentName: 'Test Agent',
|
|
609
|
+
messages: [createUserMessage('go')],
|
|
610
|
+
workingDirectory,
|
|
611
|
+
runConfig: {
|
|
612
|
+
model: 'mock-model',
|
|
613
|
+
timeoutMs: 10_000,
|
|
614
|
+
tokenBudget: 100_000,
|
|
615
|
+
maxIterations: 4,
|
|
616
|
+
maxResponseTokens: 256,
|
|
617
|
+
},
|
|
618
|
+
sessionId: 'ses_completion' as SessionId,
|
|
619
|
+
threadId: 'thd_completion' as ThreadId,
|
|
620
|
+
projectId: 'prj_completion' as ProjectId,
|
|
621
|
+
tenantId: 'tnt_completion' as TenantId,
|
|
622
|
+
} as never)
|
|
623
|
+
|
|
624
|
+
expect(run.abandonedTaskIds, 'the run said nothing about the worker it left running').toEqual([
|
|
625
|
+
'tsk_still_running',
|
|
626
|
+
])
|
|
627
|
+
// Named, not stopped: the inbox still counts it as outstanding, and
|
|
628
|
+
// nothing asked the gateway to cancel anything.
|
|
629
|
+
expect(inbox.outstandingTaskIds).toEqual(['tsk_still_running'])
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
it('leaves the field absent when the run walked away from nothing', async () => {
|
|
633
|
+
// A field that is always present says nothing; one that appears only
|
|
634
|
+
// when there is something to report is a signal a host can branch on.
|
|
635
|
+
const { run } = await runEndingWith({ terminal: true })
|
|
636
|
+
|
|
637
|
+
expect(run.abandonedTaskIds).toBeUndefined()
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
it('hands it over exactly once when the ordinary exit already did', async () => {
|
|
641
|
+
// The `dc16d58` failure in a new place: an exit-time delivery that
|
|
642
|
+
// re-sends what the in-loop drain already sent is the duplicate bug
|
|
643
|
+
// again. `drain()` empties, so the second call finds nothing.
|
|
644
|
+
const { userMessages } = await runEndingWith({})
|
|
645
|
+
|
|
646
|
+
expect(userMessages.filter((m) => m.includes('THE BACKGROUND WORKER RESULT'))).toHaveLength(1)
|
|
647
|
+
})
|
|
648
|
+
})
|