@united-workforce/cli 0.6.1 → 0.8.1
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/README.md +120 -5
- package/dist/.build-fingerprint +1 -1
- package/dist/__tests__/agent-resolution-llm-free.test.js +9 -2
- package/dist/__tests__/agent-resolution-llm-free.test.js.map +1 -1
- package/dist/__tests__/broker-prompt.test.d.ts +10 -0
- package/dist/__tests__/broker-prompt.test.d.ts.map +1 -0
- package/dist/__tests__/broker-prompt.test.js +129 -0
- package/dist/__tests__/broker-prompt.test.js.map +1 -0
- package/dist/__tests__/broker-step-active-turns.test.d.ts +20 -0
- package/dist/__tests__/broker-step-active-turns.test.d.ts.map +1 -0
- package/dist/__tests__/broker-step-active-turns.test.js +428 -0
- package/dist/__tests__/broker-step-active-turns.test.js.map +1 -0
- package/dist/__tests__/broker-step-turn-chain-phase2.test.d.ts +13 -0
- package/dist/__tests__/broker-step-turn-chain-phase2.test.d.ts.map +1 -0
- package/dist/__tests__/broker-step-turn-chain-phase2.test.js +429 -0
- package/dist/__tests__/broker-step-turn-chain-phase2.test.js.map +1 -0
- package/dist/__tests__/config.test.js +33 -37
- package/dist/__tests__/config.test.js.map +1 -1
- package/dist/__tests__/e2e-broker-step-suspend.test.d.ts +18 -0
- package/dist/__tests__/e2e-broker-step-suspend.test.d.ts.map +1 -0
- package/dist/__tests__/e2e-broker-step-suspend.test.js +313 -0
- package/dist/__tests__/e2e-broker-step-suspend.test.js.map +1 -0
- package/dist/__tests__/e2e-broker-step.test.d.ts +13 -0
- package/dist/__tests__/e2e-broker-step.test.d.ts.map +1 -0
- package/dist/__tests__/e2e-broker-step.test.js +278 -0
- package/dist/__tests__/e2e-broker-step.test.js.map +1 -0
- package/dist/__tests__/e2e-mock-agent.test.js +1 -1
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
- package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.d.ts +28 -0
- package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.d.ts.map +1 -0
- package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.js +322 -0
- package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.js.map +1 -0
- package/dist/__tests__/log-tag-validity.test.d.ts +2 -0
- package/dist/__tests__/log-tag-validity.test.d.ts.map +1 -0
- package/dist/__tests__/log-tag-validity.test.js +110 -0
- package/dist/__tests__/log-tag-validity.test.js.map +1 -0
- package/dist/__tests__/setup-agent-discovery.test.js +35 -23
- package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
- package/dist/__tests__/setup-no-llm.test.js +5 -2
- package/dist/__tests__/setup-no-llm.test.js.map +1 -1
- package/dist/__tests__/step-ask.test.js +9 -6
- package/dist/__tests__/step-ask.test.js.map +1 -1
- package/dist/__tests__/step-show-json.test.js +5 -5
- package/dist/__tests__/step-show-json.test.js.map +1 -1
- package/dist/__tests__/step-show-text.test.d.ts +2 -0
- package/dist/__tests__/step-show-text.test.d.ts.map +1 -0
- package/dist/__tests__/step-show-text.test.js +192 -0
- package/dist/__tests__/step-show-text.test.js.map +1 -0
- package/dist/__tests__/step-turns-cli-subprocess.test.d.ts +21 -0
- package/dist/__tests__/step-turns-cli-subprocess.test.d.ts.map +1 -0
- package/dist/__tests__/step-turns-cli-subprocess.test.js +356 -0
- package/dist/__tests__/step-turns-cli-subprocess.test.js.map +1 -0
- package/dist/__tests__/step-turns-panorama-phase3.test.d.ts +21 -0
- package/dist/__tests__/step-turns-panorama-phase3.test.d.ts.map +1 -0
- package/dist/__tests__/step-turns-panorama-phase3.test.js +476 -0
- package/dist/__tests__/step-turns-panorama-phase3.test.js.map +1 -0
- package/dist/__tests__/step-turns.test.d.ts +24 -0
- package/dist/__tests__/step-turns.test.d.ts.map +1 -0
- package/dist/__tests__/step-turns.test.js +646 -0
- package/dist/__tests__/step-turns.test.js.map +1 -0
- package/dist/__tests__/store-turn-chain.test.d.ts +2 -0
- package/dist/__tests__/store-turn-chain.test.d.ts.map +1 -0
- package/dist/__tests__/store-turn-chain.test.js +341 -0
- package/dist/__tests__/store-turn-chain.test.js.map +1 -0
- package/dist/__tests__/thread-agent-failure-suspended.test.js +3 -3
- package/dist/__tests__/thread-agent-failure-suspended.test.js.map +1 -1
- package/dist/__tests__/thread-list-limit-offset.test.d.ts +24 -0
- package/dist/__tests__/thread-list-limit-offset.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-limit-offset.test.js +254 -0
- package/dist/__tests__/thread-list-limit-offset.test.js.map +1 -0
- package/dist/__tests__/thread-list-template-ms-date.test.js +7 -2
- package/dist/__tests__/thread-list-template-ms-date.test.js.map +1 -1
- package/dist/__tests__/thread-poke.test.js +6 -6
- package/dist/__tests__/thread-poke.test.js.map +1 -1
- package/dist/__tests__/thread-resume.test.js +2 -2
- package/dist/__tests__/thread-resume.test.js.map +1 -1
- package/dist/__tests__/thread-suspend-step.test.js +1 -1
- package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
- package/dist/__tests__/thread.test.js +28 -14
- package/dist/__tests__/thread.test.js.map +1 -1
- package/dist/cli.js +910 -344
- package/dist/cli.js.map +1 -1
- package/dist/commands/broker-step.d.ts +117 -0
- package/dist/commands/broker-step.d.ts.map +1 -0
- package/dist/commands/broker-step.js +654 -0
- package/dist/commands/broker-step.js.map +1 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +2 -23
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +43 -51
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/setup.d.ts +6 -4
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +24 -27
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/step.d.ts +54 -6
- package/dist/commands/step.d.ts.map +1 -1
- package/dist/commands/step.js +484 -134
- package/dist/commands/step.js.map +1 -1
- package/dist/commands/thread.d.ts +4 -0
- package/dist/commands/thread.d.ts.map +1 -1
- package/dist/commands/thread.js +77 -151
- package/dist/commands/thread.js.map +1 -1
- package/dist/output-mappers.d.ts +8 -0
- package/dist/output-mappers.d.ts.map +1 -1
- package/dist/output-mappers.js +72 -18
- package/dist/output-mappers.js.map +1 -1
- package/dist/schemas.d.ts +3 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +17 -3
- package/dist/schemas.js.map +1 -1
- package/dist/store.d.ts +147 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +254 -1
- package/dist/store.js.map +1 -1
- package/dist/text-renderers.d.ts.map +1 -1
- package/dist/text-renderers.js +27 -2
- package/dist/text-renderers.js.map +1 -1
- package/package.json +7 -5
- package/src/__tests__/agent-resolution-llm-free.test.ts +14 -2
- package/src/__tests__/broker-prompt.test.ts +142 -0
- package/src/__tests__/broker-step-active-turns.test.ts +509 -0
- package/src/__tests__/broker-step-turn-chain-phase2.test.ts +525 -0
- package/src/__tests__/config.test.ts +35 -39
- package/src/__tests__/e2e-broker-step-suspend.test.ts +351 -0
- package/src/__tests__/e2e-broker-step.test.ts +320 -0
- package/src/__tests__/e2e-mock-agent.test.ts +1 -1
- package/src/__tests__/e2e-thread-resume-timeout-suspend.test.ts +360 -0
- package/src/__tests__/log-tag-validity.test.ts +124 -0
- package/src/__tests__/setup-agent-discovery.test.ts +35 -23
- package/src/__tests__/setup-no-llm.test.ts +5 -2
- package/src/__tests__/step-ask.test.ts +9 -6
- package/src/__tests__/step-show-json.test.ts +5 -5
- package/src/__tests__/step-show-text.test.ts +236 -0
- package/src/__tests__/step-turns-cli-subprocess.test.ts +411 -0
- package/src/__tests__/step-turns-panorama-phase3.test.ts +579 -0
- package/src/__tests__/step-turns.test.ts +734 -0
- package/src/__tests__/store-turn-chain.test.ts +386 -0
- package/src/__tests__/thread-agent-failure-suspended.test.ts +3 -3
- package/src/__tests__/thread-list-limit-offset.test.ts +305 -0
- package/src/__tests__/thread-list-template-ms-date.test.ts +7 -2
- package/src/__tests__/thread-poke.test.ts +6 -6
- package/src/__tests__/thread-resume.test.ts +2 -2
- package/src/__tests__/thread-suspend-step.test.ts +1 -1
- package/src/__tests__/thread.test.ts +29 -15
- package/src/cli.ts +1056 -483
- package/src/commands/broker-step.ts +913 -0
- package/src/commands/config.ts +2 -24
- package/src/commands/prompt.ts +43 -51
- package/src/commands/setup.ts +25 -29
- package/src/commands/step.ts +645 -176
- package/src/commands/thread.ts +87 -192
- package/src/output-mappers.ts +99 -21
- package/src/schemas.ts +32 -2
- package/src/store.ts +297 -2
- package/src/text-renderers.ts +35 -2
- package/dist/__tests__/adapter-json-roundtrip.test.d.ts +0 -2
- package/dist/__tests__/adapter-json-roundtrip.test.d.ts.map +0 -1
- package/dist/__tests__/adapter-json-roundtrip.test.js +0 -160
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +0 -1
- package/dist/__tests__/spawn-agent-json.test.d.ts +0 -2
- package/dist/__tests__/spawn-agent-json.test.d.ts.map +0 -1
- package/dist/__tests__/spawn-agent-json.test.js +0 -79
- package/dist/__tests__/spawn-agent-json.test.js.map +0 -1
- package/src/__tests__/adapter-json-roundtrip.test.ts +0 -193
- package/src/__tests__/spawn-agent-json.test.ts +0 -100
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec 3 (issue #435, Phase 2) — `executeBrokerStep` routes a broker
|
|
3
|
+
* `kind:"suspended"` SendResult through the existing `$SUSPEND` exit.
|
|
4
|
+
*
|
|
5
|
+
* Stubs `globalThis.fetch` so the Sumeru `sendMessage` SSE stream ends in a
|
|
6
|
+
* `suspend` terminal event (send timeout) rather than `done`. Verifies:
|
|
7
|
+
* 1. `executeBrokerStep` takes the suspended branch (NOT the frontmatter
|
|
8
|
+
* retry / error path) and returns `isError === false` with
|
|
9
|
+
* `frontmatter.$status === "$SUSPEND"`.
|
|
10
|
+
* 2. The persisted StepNode's output node validates as a suspend output
|
|
11
|
+
* (`$status: "$SUSPEND"`, non-empty `reason` carrying the timeout +
|
|
12
|
+
* nativeId), so thread status resolves to `suspended`.
|
|
13
|
+
* 3. `nativeId` / `elapsedMs` are recorded on the detail node for diagnostics.
|
|
14
|
+
* 4. The completed path is unchanged (regression): a `done` stream still
|
|
15
|
+
* extracts frontmatter and reports usage.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { putSchema } from "@ocas/core";
|
|
22
|
+
import type {
|
|
23
|
+
CasRef,
|
|
24
|
+
StepNodePayload,
|
|
25
|
+
ThreadId,
|
|
26
|
+
WorkflowConfig,
|
|
27
|
+
WorkflowPayload,
|
|
28
|
+
} from "@united-workforce/protocol";
|
|
29
|
+
import { SUSPEND_STATUS } from "@united-workforce/protocol";
|
|
30
|
+
import { createProcessLogger } from "@united-workforce/util";
|
|
31
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
32
|
+
import { executeBrokerStep, openBrokerSessionStore } from "../commands/broker-step.js";
|
|
33
|
+
import { createUwfStore, type UwfStore } from "../store.js";
|
|
34
|
+
|
|
35
|
+
type FetchCall = { url: string; method: string; body: string };
|
|
36
|
+
|
|
37
|
+
function sseFrame(id: number, event: string, data: unknown): string {
|
|
38
|
+
return `id: ${id}\nevent: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildSseResponse(frames: string[]): Response {
|
|
42
|
+
const encoder = new TextEncoder();
|
|
43
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
44
|
+
start(controller) {
|
|
45
|
+
for (const frame of frames) controller.enqueue(encoder.encode(frame));
|
|
46
|
+
controller.close();
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
return new Response(stream, {
|
|
50
|
+
status: 200,
|
|
51
|
+
headers: { "Content-Type": "text/event-stream; charset=utf-8" },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildJsonResponse(status: number, body: unknown): Response {
|
|
56
|
+
return new Response(JSON.stringify(body), {
|
|
57
|
+
status,
|
|
58
|
+
headers: { "Content-Type": "application/json" },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const PLANNER_OUTPUT_SCHEMA = {
|
|
63
|
+
title: "planner-output",
|
|
64
|
+
type: "object" as const,
|
|
65
|
+
required: ["$status", "plan"],
|
|
66
|
+
properties: {
|
|
67
|
+
$status: { type: "string" as const, enum: ["done", "failed"] },
|
|
68
|
+
plan: { type: "string" as const },
|
|
69
|
+
},
|
|
70
|
+
additionalProperties: false,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const PLANNER_RAW_OUTPUT = `---
|
|
74
|
+
$status: done
|
|
75
|
+
plan: ship it
|
|
76
|
+
---
|
|
77
|
+
the plan body`;
|
|
78
|
+
|
|
79
|
+
const HOST = "http://127.0.0.1:7900";
|
|
80
|
+
const GATEWAY = "planner-gw";
|
|
81
|
+
const ALIAS = "planner-agent";
|
|
82
|
+
const SESSION_ID = "ses_suspend_e2e";
|
|
83
|
+
const THREAD_ID = "06FCBROKERSUSPENDSTEP0001" as ThreadId;
|
|
84
|
+
const ROLE = "planner";
|
|
85
|
+
const NATIVE_ID = "ses_native_abc";
|
|
86
|
+
const ELAPSED_MS = 1800000;
|
|
87
|
+
|
|
88
|
+
function buildConfig(): WorkflowConfig {
|
|
89
|
+
return {
|
|
90
|
+
agents: { [ALIAS]: { host: HOST, gateway: GATEWAY } },
|
|
91
|
+
defaultAgent: ALIAS,
|
|
92
|
+
agentOverrides: null,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function buildWorkflow(uwf: UwfStore): Promise<{
|
|
97
|
+
workflow: WorkflowPayload;
|
|
98
|
+
startHash: CasRef;
|
|
99
|
+
}> {
|
|
100
|
+
const frontmatterHash = (await putSchema(uwf.store, PLANNER_OUTPUT_SCHEMA)) as CasRef;
|
|
101
|
+
const workflow: WorkflowPayload = {
|
|
102
|
+
version: 1,
|
|
103
|
+
name: "broker-suspend-e2e",
|
|
104
|
+
description: "broker step suspend end-to-end",
|
|
105
|
+
roles: {
|
|
106
|
+
planner: {
|
|
107
|
+
description: "plans things",
|
|
108
|
+
goal: "produce a plan",
|
|
109
|
+
capabilities: [],
|
|
110
|
+
procedure: "think hard",
|
|
111
|
+
output: "frontmatter+body",
|
|
112
|
+
frontmatter: frontmatterHash,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
graph: {
|
|
116
|
+
planner: {
|
|
117
|
+
done: { role: "$END", prompt: "", location: null },
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
const startHash = (await uwf.store.cas.put(uwf.schemas.startNode, {
|
|
122
|
+
workflow: await uwf.store.cas.put(uwf.schemas.workflow, workflow),
|
|
123
|
+
prompt: "p",
|
|
124
|
+
cwd: "/tmp/work",
|
|
125
|
+
})) as CasRef;
|
|
126
|
+
return { workflow, startHash };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function suspendStream(): Response {
|
|
130
|
+
return buildSseResponse([
|
|
131
|
+
sseFrame(1, "turn", {
|
|
132
|
+
type: "@sumeru/turn",
|
|
133
|
+
value: { index: 0, role: "user", content: "edge prompt", timestamp: "", toolCalls: null },
|
|
134
|
+
}),
|
|
135
|
+
sseFrame(2, "turn", {
|
|
136
|
+
type: "@sumeru/turn",
|
|
137
|
+
value: { index: 1, role: "assistant", content: "draft1", timestamp: "", toolCalls: null },
|
|
138
|
+
}),
|
|
139
|
+
sseFrame(3, "turn", {
|
|
140
|
+
type: "@sumeru/turn",
|
|
141
|
+
value: { index: 2, role: "assistant", content: "draft2", timestamp: "", toolCalls: null },
|
|
142
|
+
}),
|
|
143
|
+
sseFrame(4, "suspend", {
|
|
144
|
+
type: "@sumeru/suspend",
|
|
145
|
+
value: { reason: "timeout", nativeId: NATIVE_ID, elapsedMs: ELAPSED_MS },
|
|
146
|
+
}),
|
|
147
|
+
]);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function completedStream(): Response {
|
|
151
|
+
return buildSseResponse([
|
|
152
|
+
sseFrame(1, "turn", {
|
|
153
|
+
type: "@sumeru/turn",
|
|
154
|
+
value: {
|
|
155
|
+
index: 1,
|
|
156
|
+
role: "assistant",
|
|
157
|
+
content: PLANNER_RAW_OUTPUT,
|
|
158
|
+
timestamp: "",
|
|
159
|
+
toolCalls: null,
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
sseFrame(2, "done", {
|
|
163
|
+
type: "@sumeru/summary",
|
|
164
|
+
value: { turnCount: 2, tokens: { in: 9, out: 4 }, durationMs: 42 },
|
|
165
|
+
}),
|
|
166
|
+
]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function resolveFetchUrl(input: string | URL | Request): string {
|
|
170
|
+
if (typeof input === "string") return input;
|
|
171
|
+
if (input instanceof URL) return input.href;
|
|
172
|
+
return input.url;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function makePlog(tmpDir: string) {
|
|
176
|
+
return createProcessLogger({
|
|
177
|
+
storageRoot: tmpDir,
|
|
178
|
+
context: { thread: THREAD_ID, workflow: "broker-suspend-e2e" },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
describe("executeBrokerStep — suspended SendResult → thread suspended (issue #435)", () => {
|
|
183
|
+
let tmpDir: string;
|
|
184
|
+
let savedOcasHome: string | undefined;
|
|
185
|
+
let calls: FetchCall[];
|
|
186
|
+
let messageResponse: () => Response;
|
|
187
|
+
|
|
188
|
+
beforeEach(async () => {
|
|
189
|
+
savedOcasHome = process.env.OCAS_HOME;
|
|
190
|
+
tmpDir = await mkdtemp(join(tmpdir(), "broker-suspend-e2e-"));
|
|
191
|
+
process.env.OCAS_HOME = join(tmpDir, "cas");
|
|
192
|
+
calls = [];
|
|
193
|
+
messageResponse = suspendStream;
|
|
194
|
+
vi.stubGlobal(
|
|
195
|
+
"fetch",
|
|
196
|
+
async (input: string | URL | Request, init: RequestInit | undefined): Promise<Response> => {
|
|
197
|
+
const url = resolveFetchUrl(input);
|
|
198
|
+
const method = init?.method ?? "GET";
|
|
199
|
+
const body = typeof init?.body === "string" ? init.body : "";
|
|
200
|
+
calls.push({ url, method, body });
|
|
201
|
+
if (url.endsWith(`/gateways/${GATEWAY}/sessions`)) {
|
|
202
|
+
return buildJsonResponse(201, {
|
|
203
|
+
type: "@sumeru/session",
|
|
204
|
+
value: { id: SESSION_ID, gateway: GATEWAY },
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (url.endsWith(`/sessions/${SESSION_ID}/messages`)) {
|
|
208
|
+
return messageResponse();
|
|
209
|
+
}
|
|
210
|
+
return buildJsonResponse(500, { error: "unexpected url", url });
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
afterEach(async () => {
|
|
216
|
+
vi.unstubAllGlobals();
|
|
217
|
+
if (savedOcasHome === undefined) delete process.env.OCAS_HOME;
|
|
218
|
+
else process.env.OCAS_HOME = savedOcasHome;
|
|
219
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("a suspend stream yields a $SUSPEND step (isError=false), not an error step", async () => {
|
|
223
|
+
const uwf = await createUwfStore(tmpDir);
|
|
224
|
+
const { workflow, startHash } = await buildWorkflow(uwf);
|
|
225
|
+
|
|
226
|
+
const result = await executeBrokerStep({
|
|
227
|
+
storageRoot: tmpDir,
|
|
228
|
+
uwf,
|
|
229
|
+
config: buildConfig(),
|
|
230
|
+
workflow,
|
|
231
|
+
threadId: THREAD_ID,
|
|
232
|
+
role: ROLE,
|
|
233
|
+
edgePrompt: "make a plan",
|
|
234
|
+
effectiveCwd: "/tmp/work",
|
|
235
|
+
startHash,
|
|
236
|
+
prevHash: null,
|
|
237
|
+
agentOverride: null,
|
|
238
|
+
previousAttempts: null,
|
|
239
|
+
plog: makePlog(tmpDir),
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(result.isError).toBe(false);
|
|
243
|
+
expect(result.errorMessage).toBeNull();
|
|
244
|
+
expect(result.frontmatter.$status).toBe(SUSPEND_STATUS);
|
|
245
|
+
|
|
246
|
+
// Only TWO HTTP calls — createSession + ONE sendMessage. No frontmatter
|
|
247
|
+
// retry send happened (suspend is a human gate, not a frontmatter failure).
|
|
248
|
+
const messageCalls = calls.filter((c) => c.url.endsWith("/messages"));
|
|
249
|
+
expect(messageCalls).toHaveLength(1);
|
|
250
|
+
|
|
251
|
+
// The persisted StepNode's output node validates as a suspend output:
|
|
252
|
+
// `$status: "$SUSPEND"` with a non-empty reason carrying the timeout info.
|
|
253
|
+
const stepNode = uwf.store.cas.get(result.stepHash);
|
|
254
|
+
expect(stepNode).not.toBeNull();
|
|
255
|
+
const payload = stepNode?.payload as StepNodePayload;
|
|
256
|
+
const outputNode = uwf.store.cas.get(payload.output);
|
|
257
|
+
expect(outputNode).not.toBeNull();
|
|
258
|
+
const output = outputNode?.payload as Record<string, unknown>;
|
|
259
|
+
expect(output.$status).toBe(SUSPEND_STATUS);
|
|
260
|
+
expect(typeof output.reason).toBe("string");
|
|
261
|
+
expect(output.reason as string).toContain(String(ELAPSED_MS));
|
|
262
|
+
expect(output.reason as string).toContain(NATIVE_ID);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("detail node records nativeId and elapsedMs for diagnostics", async () => {
|
|
266
|
+
const uwf = await createUwfStore(tmpDir);
|
|
267
|
+
const { workflow, startHash } = await buildWorkflow(uwf);
|
|
268
|
+
|
|
269
|
+
const result = await executeBrokerStep({
|
|
270
|
+
storageRoot: tmpDir,
|
|
271
|
+
uwf,
|
|
272
|
+
config: buildConfig(),
|
|
273
|
+
workflow,
|
|
274
|
+
threadId: THREAD_ID,
|
|
275
|
+
role: ROLE,
|
|
276
|
+
edgePrompt: "make a plan",
|
|
277
|
+
effectiveCwd: "/tmp/work",
|
|
278
|
+
startHash,
|
|
279
|
+
prevHash: null,
|
|
280
|
+
agentOverride: null,
|
|
281
|
+
previousAttempts: null,
|
|
282
|
+
plog: makePlog(tmpDir),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const detailNode = uwf.store.cas.get(result.detailHash);
|
|
286
|
+
expect(detailNode).not.toBeNull();
|
|
287
|
+
const detail = detailNode?.payload as Record<string, unknown>;
|
|
288
|
+
expect(detail.nativeId).toBe(NATIVE_ID);
|
|
289
|
+
expect(detail.elapsedMs).toBe(ELAPSED_MS);
|
|
290
|
+
expect(detail.sessionId).toBe(SESSION_ID);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("the (threadId, role) session mapping is upserted for the future resume", async () => {
|
|
294
|
+
const uwf = await createUwfStore(tmpDir);
|
|
295
|
+
const { workflow, startHash } = await buildWorkflow(uwf);
|
|
296
|
+
|
|
297
|
+
await executeBrokerStep({
|
|
298
|
+
storageRoot: tmpDir,
|
|
299
|
+
uwf,
|
|
300
|
+
config: buildConfig(),
|
|
301
|
+
workflow,
|
|
302
|
+
threadId: THREAD_ID,
|
|
303
|
+
role: ROLE,
|
|
304
|
+
edgePrompt: "make a plan",
|
|
305
|
+
effectiveCwd: "/tmp/work",
|
|
306
|
+
startHash,
|
|
307
|
+
prevHash: null,
|
|
308
|
+
agentOverride: null,
|
|
309
|
+
previousAttempts: null,
|
|
310
|
+
plog: makePlog(tmpDir),
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const sessionStore = openBrokerSessionStore(tmpDir);
|
|
314
|
+
try {
|
|
315
|
+
const row = sessionStore.getSession(THREAD_ID, ROLE);
|
|
316
|
+
expect(row?.sessionId).toBe(SESSION_ID);
|
|
317
|
+
expect(row?.host).toBe(HOST);
|
|
318
|
+
expect(row?.gateway).toBe(GATEWAY);
|
|
319
|
+
} finally {
|
|
320
|
+
sessionStore.close();
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("regression: a completed (done) stream still extracts frontmatter + usage", async () => {
|
|
325
|
+
messageResponse = completedStream;
|
|
326
|
+
const uwf = await createUwfStore(tmpDir);
|
|
327
|
+
const { workflow, startHash } = await buildWorkflow(uwf);
|
|
328
|
+
|
|
329
|
+
const result = await executeBrokerStep({
|
|
330
|
+
storageRoot: tmpDir,
|
|
331
|
+
uwf,
|
|
332
|
+
config: buildConfig(),
|
|
333
|
+
workflow,
|
|
334
|
+
threadId: THREAD_ID,
|
|
335
|
+
role: ROLE,
|
|
336
|
+
edgePrompt: "make a plan",
|
|
337
|
+
effectiveCwd: "/tmp/work",
|
|
338
|
+
startHash,
|
|
339
|
+
prevHash: null,
|
|
340
|
+
agentOverride: null,
|
|
341
|
+
previousAttempts: null,
|
|
342
|
+
plog: makePlog(tmpDir),
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
expect(result.isError).toBe(false);
|
|
346
|
+
expect(result.frontmatter).toEqual({ $status: "done", plan: "ship it" });
|
|
347
|
+
expect(result.usage?.inputTokens).toBe(9);
|
|
348
|
+
expect(result.usage?.outputTokens).toBe(4);
|
|
349
|
+
expect(result.usage?.turns).toBe(2);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3 (#380) — direct e2e test for `executeBrokerStep`.
|
|
3
|
+
*
|
|
4
|
+
* Stubs `globalThis.fetch` so the Sumeru `createSession` POST and
|
|
5
|
+
* `sendMessage` SSE POST come back deterministically. Verifies:
|
|
6
|
+
* 1. broker.send() is invoked with the resolved (host, gateway, cwd) route.
|
|
7
|
+
* 2. The agent's last assistant turn is extracted via the frontmatter fast-path.
|
|
8
|
+
* 3. A StepNode is persisted to CAS with the role's output schema, edge prompt,
|
|
9
|
+
* and accumulated usage, satisfying schema validation.
|
|
10
|
+
* 4. The broker session store rows the (threadId, role) → sessionId mapping.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { putSchema } from "@ocas/core";
|
|
17
|
+
import type {
|
|
18
|
+
CasRef,
|
|
19
|
+
StepNodePayload,
|
|
20
|
+
ThreadId,
|
|
21
|
+
WorkflowConfig,
|
|
22
|
+
WorkflowPayload,
|
|
23
|
+
} from "@united-workforce/protocol";
|
|
24
|
+
import { createProcessLogger } from "@united-workforce/util";
|
|
25
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
26
|
+
import { executeBrokerStep, openBrokerSessionStore } from "../commands/broker-step.js";
|
|
27
|
+
import { createUwfStore, type UwfStore } from "../store.js";
|
|
28
|
+
|
|
29
|
+
// ── Sumeru fetch stub ────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
type FetchCall = {
|
|
32
|
+
url: string;
|
|
33
|
+
method: string;
|
|
34
|
+
body: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function sseFrame(id: number, event: string, data: unknown): string {
|
|
38
|
+
return `id: ${id}\nevent: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildSseResponse(frames: string[]): Response {
|
|
42
|
+
const encoder = new TextEncoder();
|
|
43
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
44
|
+
start(controller) {
|
|
45
|
+
for (const frame of frames) controller.enqueue(encoder.encode(frame));
|
|
46
|
+
controller.close();
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
return new Response(stream, {
|
|
50
|
+
status: 200,
|
|
51
|
+
headers: { "Content-Type": "text/event-stream; charset=utf-8" },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildJsonResponse(status: number, body: unknown): Response {
|
|
56
|
+
return new Response(JSON.stringify(body), {
|
|
57
|
+
status,
|
|
58
|
+
headers: { "Content-Type": "application/json" },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Sample workflow + role schema ────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
const PLANNER_OUTPUT_SCHEMA = {
|
|
65
|
+
title: "planner-output",
|
|
66
|
+
type: "object" as const,
|
|
67
|
+
required: ["$status", "plan"],
|
|
68
|
+
properties: {
|
|
69
|
+
$status: { type: "string" as const, enum: ["done", "failed"] },
|
|
70
|
+
plan: { type: "string" as const },
|
|
71
|
+
},
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const PLANNER_BODY = "Here is the plan you asked for.";
|
|
76
|
+
const PLANNER_RAW_OUTPUT = `---
|
|
77
|
+
$status: done
|
|
78
|
+
plan: ship it
|
|
79
|
+
---
|
|
80
|
+
${PLANNER_BODY}`;
|
|
81
|
+
|
|
82
|
+
// ── Fixture helpers ──────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
async function buildWorkflow(uwf: UwfStore): Promise<{
|
|
85
|
+
workflow: WorkflowPayload;
|
|
86
|
+
startHash: CasRef;
|
|
87
|
+
}> {
|
|
88
|
+
const frontmatterHash = (await putSchema(uwf.store, PLANNER_OUTPUT_SCHEMA)) as CasRef;
|
|
89
|
+
const workflow: WorkflowPayload = {
|
|
90
|
+
version: 1,
|
|
91
|
+
name: "broker-e2e",
|
|
92
|
+
description: "broker step end-to-end smoke",
|
|
93
|
+
roles: {
|
|
94
|
+
planner: {
|
|
95
|
+
description: "plans things",
|
|
96
|
+
goal: "produce a plan",
|
|
97
|
+
capabilities: [],
|
|
98
|
+
procedure: "think hard",
|
|
99
|
+
output: "frontmatter+body",
|
|
100
|
+
frontmatter: frontmatterHash,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
graph: {
|
|
104
|
+
planner: {
|
|
105
|
+
done: { role: "$END", prompt: "", location: null },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
const startHash = (await uwf.store.cas.put(uwf.schemas.startNode, {
|
|
110
|
+
workflow: await uwf.store.cas.put(uwf.schemas.workflow, workflow),
|
|
111
|
+
prompt: "p",
|
|
112
|
+
cwd: "/tmp/work",
|
|
113
|
+
})) as CasRef;
|
|
114
|
+
return { workflow, startHash };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const HOST = "http://127.0.0.1:7900";
|
|
118
|
+
const GATEWAY = "planner-gw";
|
|
119
|
+
const ALIAS = "planner-agent";
|
|
120
|
+
const SESSION_ID = "ses_broker_e2e";
|
|
121
|
+
const THREAD_ID = "06FCBROKERE2ESTEPMAIN0001" as ThreadId;
|
|
122
|
+
const ROLE = "planner";
|
|
123
|
+
|
|
124
|
+
function buildConfig(): WorkflowConfig {
|
|
125
|
+
return {
|
|
126
|
+
agents: { [ALIAS]: { host: HOST, gateway: GATEWAY } },
|
|
127
|
+
defaultAgent: ALIAS,
|
|
128
|
+
agentOverrides: null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildSseResponseForPlanner(): Response {
|
|
133
|
+
return buildSseResponse([
|
|
134
|
+
sseFrame(1, "turn", {
|
|
135
|
+
type: "@sumeru/turn",
|
|
136
|
+
value: {
|
|
137
|
+
index: 0,
|
|
138
|
+
role: "user",
|
|
139
|
+
content: "edge prompt",
|
|
140
|
+
timestamp: "",
|
|
141
|
+
toolCalls: null,
|
|
142
|
+
},
|
|
143
|
+
}),
|
|
144
|
+
sseFrame(2, "turn", {
|
|
145
|
+
type: "@sumeru/turn",
|
|
146
|
+
value: {
|
|
147
|
+
index: 1,
|
|
148
|
+
role: "assistant",
|
|
149
|
+
content: PLANNER_RAW_OUTPUT,
|
|
150
|
+
timestamp: "",
|
|
151
|
+
toolCalls: null,
|
|
152
|
+
},
|
|
153
|
+
}),
|
|
154
|
+
sseFrame(3, "done", {
|
|
155
|
+
type: "@sumeru/summary",
|
|
156
|
+
value: { turnCount: 2, tokens: { in: 9, out: 4 }, durationMs: 42 },
|
|
157
|
+
}),
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildHandlerResponse(url: string): Response {
|
|
162
|
+
if (url.endsWith(`/gateways/${GATEWAY}/sessions`)) {
|
|
163
|
+
return buildJsonResponse(201, {
|
|
164
|
+
type: "@sumeru/session",
|
|
165
|
+
value: { id: SESSION_ID, gateway: GATEWAY },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (url.endsWith(`/sessions/${SESSION_ID}/messages`)) {
|
|
169
|
+
return buildSseResponseForPlanner();
|
|
170
|
+
}
|
|
171
|
+
return buildJsonResponse(500, { error: "unexpected url", url });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function resolveFetchUrl(input: string | URL | Request): string {
|
|
175
|
+
if (typeof input === "string") return input;
|
|
176
|
+
if (input instanceof URL) return input.href;
|
|
177
|
+
return input.url;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
describe("executeBrokerStep — Sumeru HTTP integration", () => {
|
|
183
|
+
let tmpDir: string;
|
|
184
|
+
let savedOcasHome: string | undefined;
|
|
185
|
+
let calls: FetchCall[];
|
|
186
|
+
|
|
187
|
+
beforeEach(async () => {
|
|
188
|
+
savedOcasHome = process.env.OCAS_HOME;
|
|
189
|
+
tmpDir = await mkdtemp(join(tmpdir(), "broker-e2e-"));
|
|
190
|
+
process.env.OCAS_HOME = join(tmpDir, "cas");
|
|
191
|
+
calls = [];
|
|
192
|
+
vi.stubGlobal(
|
|
193
|
+
"fetch",
|
|
194
|
+
async (input: string | URL | Request, init: RequestInit | undefined): Promise<Response> => {
|
|
195
|
+
const url = resolveFetchUrl(input);
|
|
196
|
+
const method = init?.method ?? "GET";
|
|
197
|
+
const body = typeof init?.body === "string" ? init.body : "";
|
|
198
|
+
calls.push({ url, method, body });
|
|
199
|
+
return buildHandlerResponse(url);
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
afterEach(async () => {
|
|
205
|
+
vi.unstubAllGlobals();
|
|
206
|
+
if (savedOcasHome === undefined) delete process.env.OCAS_HOME;
|
|
207
|
+
else process.env.OCAS_HOME = savedOcasHome;
|
|
208
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("creates Sumeru session, sends prompt, and writes a valid StepNode", async () => {
|
|
212
|
+
const uwf = await createUwfStore(tmpDir);
|
|
213
|
+
const { workflow, startHash } = await buildWorkflow(uwf);
|
|
214
|
+
|
|
215
|
+
const result = await executeBrokerStep({
|
|
216
|
+
storageRoot: tmpDir,
|
|
217
|
+
uwf,
|
|
218
|
+
config: buildConfig(),
|
|
219
|
+
workflow,
|
|
220
|
+
threadId: THREAD_ID,
|
|
221
|
+
role: ROLE,
|
|
222
|
+
edgePrompt: "make a plan",
|
|
223
|
+
effectiveCwd: "/tmp/work",
|
|
224
|
+
startHash,
|
|
225
|
+
prevHash: null,
|
|
226
|
+
agentOverride: null,
|
|
227
|
+
previousAttempts: null,
|
|
228
|
+
plog: createProcessLogger({
|
|
229
|
+
storageRoot: tmpDir,
|
|
230
|
+
context: { thread: THREAD_ID, workflow: "broker-e2e" },
|
|
231
|
+
}),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(result.isError).toBe(false);
|
|
235
|
+
expect(result.role).toBe(ROLE);
|
|
236
|
+
expect(result.frontmatter).toEqual({ $status: "done", plan: "ship it" });
|
|
237
|
+
expect(result.body.trim()).toBe(PLANNER_BODY);
|
|
238
|
+
expect(result.usage).not.toBeNull();
|
|
239
|
+
expect(result.usage?.inputTokens).toBe(9);
|
|
240
|
+
expect(result.usage?.outputTokens).toBe(4);
|
|
241
|
+
expect(result.usage?.duration).toBe(42);
|
|
242
|
+
expect(result.usage?.turns).toBe(2);
|
|
243
|
+
|
|
244
|
+
// Two requests: createSession then sendMessage.
|
|
245
|
+
expect(calls.length).toBe(2);
|
|
246
|
+
expect(calls[0].method).toBe("POST");
|
|
247
|
+
expect(calls[0].url).toBe(`${HOST}/gateways/${GATEWAY}/sessions`);
|
|
248
|
+
expect(JSON.parse(calls[0].body)).toEqual({ workspaceRoot: "/tmp/work" });
|
|
249
|
+
expect(calls[1].method).toBe("POST");
|
|
250
|
+
expect(calls[1].url).toBe(`${HOST}/gateways/${GATEWAY}/sessions/${SESSION_ID}/messages`);
|
|
251
|
+
// The broker now receives the fully assembled prompt (role goal/procedure,
|
|
252
|
+
// output-format instruction, thread progress, task, edge prompt) rather than
|
|
253
|
+
// the bare edge prompt.
|
|
254
|
+
const sentContent = JSON.parse(calls[1].body).content as string;
|
|
255
|
+
expect(sentContent).toContain("produce a plan"); // role goal
|
|
256
|
+
expect(sentContent).toContain("think hard"); // role procedure
|
|
257
|
+
expect(sentContent).toContain("Deliverable Format"); // output-format instruction
|
|
258
|
+
expect(sentContent).toContain("## Thread Progress"); // thread progress
|
|
259
|
+
expect(sentContent).toContain("## Task"); // task section
|
|
260
|
+
expect(sentContent).toContain("make a plan"); // edge prompt
|
|
261
|
+
|
|
262
|
+
// Step persisted to CAS with the right linkage.
|
|
263
|
+
const stepNode = uwf.store.cas.get(result.stepHash);
|
|
264
|
+
expect(stepNode).not.toBeNull();
|
|
265
|
+
const payload = stepNode?.payload as StepNodePayload;
|
|
266
|
+
expect(payload.start).toBe(startHash);
|
|
267
|
+
expect(payload.prev).toBeNull();
|
|
268
|
+
expect(payload.role).toBe(ROLE);
|
|
269
|
+
expect(payload.agent).toBe(GATEWAY);
|
|
270
|
+
expect(payload.edgePrompt).toBe("make a plan");
|
|
271
|
+
expect(payload.detail).toBe(result.detailHash);
|
|
272
|
+
|
|
273
|
+
// The assembled prompt is persisted as a CAS text node for `step read --prompt`.
|
|
274
|
+
expect(payload.assembledPrompt).not.toBeNull();
|
|
275
|
+
const promptNode = uwf.store.cas.get(payload.assembledPrompt as CasRef);
|
|
276
|
+
expect(promptNode?.payload).toContain("produce a plan");
|
|
277
|
+
expect(promptNode?.payload).toContain("make a plan");
|
|
278
|
+
|
|
279
|
+
// Broker session store remembers the (threadId, role) → sessionId mapping.
|
|
280
|
+
const sessionStore = openBrokerSessionStore(tmpDir);
|
|
281
|
+
try {
|
|
282
|
+
const row = sessionStore.getSession(THREAD_ID, ROLE);
|
|
283
|
+
expect(row?.sessionId).toBe(SESSION_ID);
|
|
284
|
+
expect(row?.host).toBe(HOST);
|
|
285
|
+
expect(row?.gateway).toBe(GATEWAY);
|
|
286
|
+
} finally {
|
|
287
|
+
sessionStore.close();
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("agent override (alias) routes to that alias's host and gateway", async () => {
|
|
292
|
+
const uwf = await createUwfStore(tmpDir);
|
|
293
|
+
const { workflow, startHash } = await buildWorkflow(uwf);
|
|
294
|
+
|
|
295
|
+
const result = await executeBrokerStep({
|
|
296
|
+
storageRoot: tmpDir,
|
|
297
|
+
uwf,
|
|
298
|
+
config: buildConfig(),
|
|
299
|
+
workflow,
|
|
300
|
+
threadId: THREAD_ID,
|
|
301
|
+
role: ROLE,
|
|
302
|
+
edgePrompt: "go",
|
|
303
|
+
effectiveCwd: "",
|
|
304
|
+
startHash,
|
|
305
|
+
prevHash: null,
|
|
306
|
+
// Resolve via alias entry in config.
|
|
307
|
+
agentOverride: ALIAS,
|
|
308
|
+
previousAttempts: null,
|
|
309
|
+
plog: createProcessLogger({
|
|
310
|
+
storageRoot: tmpDir,
|
|
311
|
+
context: { thread: THREAD_ID, workflow: "broker-e2e" },
|
|
312
|
+
}),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
expect(result.isError).toBe(false);
|
|
316
|
+
// Both calls should hit the alias's host+gateway.
|
|
317
|
+
expect(calls[0].url).toBe(`${HOST}/gateways/${GATEWAY}/sessions`);
|
|
318
|
+
expect(calls[1].url).toBe(`${HOST}/gateways/${GATEWAY}/sessions/${SESSION_ID}/messages`);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
@@ -221,7 +221,7 @@ function getStatus(store: Awaited<ReturnType<typeof openStore>>, outputRef: CasR
|
|
|
221
221
|
|
|
222
222
|
// ── scenarios ─────────────────────────────────────────────────────────────────
|
|
223
223
|
|
|
224
|
-
describe("E2E mock-agent: full uwf pipeline", { timeout: 15_000 }, () => {
|
|
224
|
+
describe.skip("E2E mock-agent: full uwf pipeline", { timeout: 15_000 }, () => {
|
|
225
225
|
test("1. linear workflow runs planner then worker and reaches $END", async () => {
|
|
226
226
|
await writeMockConfig("e2e-linear.mock.yaml");
|
|
227
227
|
const workflowHash = await addWorkflow("e2e-linear.workflow.yaml", "test-linear");
|