@zhushanwen/subagent-core 0.2.0 → 0.4.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/README.md +45 -2
- package/agents/analyst.md +60 -0
- package/agents/coder.md +69 -0
- package/agents/debugger.md +66 -0
- package/agents/doc-reviewer.md +49 -0
- package/agents/explorer.md +63 -0
- package/agents/general-purpose.md +32 -0
- package/agents/orchestrator.md +61 -0
- package/agents/planner.md +53 -0
- package/agents/researcher.md +67 -0
- package/agents/reviewer.md +73 -0
- package/dist/chunk-4KN4TTG7.js +240 -0
- package/dist/chunk-APZY4IME.js +27 -0
- package/dist/chunk-X7SZ5HLQ.js +66 -0
- package/dist/execution/engine/engines/zcode/constants.cjs +65 -0
- package/dist/execution/engine/engines/zcode/constants.d.cts +93 -1
- package/dist/execution/engine/engines/zcode/constants.d.ts +93 -1
- package/dist/execution/engine/engines/zcode/constants.js +43 -1
- package/dist/execution/engine/engines/zcode/reader.d.cts +1 -1
- package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
- package/dist/execution/engine/engines/zcode/reader.js +4 -234
- package/dist/execution/engine/paths.js +7 -19
- package/dist/index.cjs +14709 -100
- package/dist/index.d.cts +4053 -200
- package/dist/index.d.ts +4053 -200
- package/dist/index.js +14358 -102
- package/dist/{types-BxyAidGf.d.cts → types-DpUO16pj.d.cts} +464 -1
- package/dist/{types-BxyAidGf.d.ts → types-DpUO16pj.d.ts} +464 -1
- package/package.json +6 -3
- package/src/__tests__/review-fix-loop-script.test.ts +90 -2
- package/src/__tests__/review-fix-loop-utils.test.ts +21 -0
- package/src/__tests__/smoke.test.ts +9 -2
- package/src/execution/__tests__/agent-profile.test.ts +232 -0
- package/src/execution/__tests__/agents-assembly.test.ts +219 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +71 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +1 -0
- package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
- package/src/execution/__tests__/delivery-methods.test.ts +2 -0
- package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
- package/src/execution/__tests__/dialog-queue.test.ts +199 -1
- package/src/execution/__tests__/epipe-fallback.test.ts +2 -0
- package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
- package/src/execution/__tests__/finalize-record.test.ts +106 -0
- package/src/execution/__tests__/gc-timer.test.ts +2 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +2 -0
- package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
- package/src/execution/__tests__/keep-alive-no-progress.test.ts +378 -0
- package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
- package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
- package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
- package/src/execution/__tests__/notify-ledger.test.ts +294 -0
- package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
- package/src/execution/__tests__/record-store.test.ts +83 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +4 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +1 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +407 -17
- package/src/execution/__tests__/run-spawn-integration.test.ts +1 -1
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +12 -4
- package/src/execution/__tests__/service-kill-escalation.test.ts +89 -0
- package/src/execution/__tests__/session-pending.test.ts +228 -0
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
- package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
- package/src/execution/__tests__/session-runner-epipe.test.ts +1 -0
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +226 -0
- package/src/execution/__tests__/settled-watchdog.test.ts +289 -0
- package/src/execution/__tests__/spawned-children.test.ts +153 -10
- package/src/execution/__tests__/subagent-actions-core.test.ts +997 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +71 -10
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +266 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +3 -0
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +316 -0
- package/src/execution/__tests__/timeout-integration.test.ts +7 -7
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +95 -1
- package/src/execution/__tests__/ui-request-queue.test.ts +25 -0
- package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
- package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
- package/src/execution/agent-registry.ts +225 -1
- package/src/execution/agents-assembly.ts +88 -0
- package/src/execution/concurrency-pool.ts +71 -9
- package/src/execution/dialog-queue.ts +101 -3
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +49 -2
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +87 -1
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +26 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +83 -1
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +131 -0
- package/src/execution/engine/__tests__/registry.test.ts +90 -1
- package/src/execution/engine/engine-discovery.ts +12 -16
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +3 -3
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +274 -0
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +472 -0
- package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +387 -0
- package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +780 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +815 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +462 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +119 -1
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +136 -6
- package/src/execution/engine/engines/zcode/appserver-home.ts +442 -0
- package/src/execution/engine/engines/zcode/appserver-probe.ts +141 -0
- package/src/execution/engine/engines/zcode/connection.ts +585 -0
- package/src/execution/engine/engines/zcode/constants.ts +129 -0
- package/src/execution/engine/engines/zcode/golden-sample.ts +54 -8
- package/src/execution/engine/engines/zcode/preparer.ts +25 -22
- package/src/execution/engine/engines/zcode/session-channel.ts +656 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +909 -41
- package/src/execution/engine/host-task-spec.ts +3 -3
- package/src/execution/engine/port.ts +41 -1
- package/src/execution/engine/registry.ts +53 -0
- package/src/execution/finalize-record.ts +63 -5
- package/src/execution/get-state-handshake.ts +85 -10
- package/src/execution/lifecycle-manager.ts +27 -3
- package/src/execution/manifest-store.ts +53 -62
- package/src/execution/notifier.ts +10 -10
- package/src/execution/notify-ledger.ts +117 -16
- package/src/execution/record-entry.ts +8 -2
- package/src/execution/record-store.ts +29 -15
- package/src/execution/session-pending.ts +213 -62
- package/src/execution/session-runner.ts +974 -128
- package/src/execution/sessions-index.ts +10 -55
- package/src/execution/settled-watchdog.ts +99 -0
- package/src/execution/subagent-actions-core.ts +686 -0
- package/src/execution/subagent-service.ts +396 -49
- package/src/execution/ui-request-handler-factory.ts +27 -4
- package/src/execution/worktree-git-ops.ts +397 -0
- package/src/execution/worktree-manager.ts +72 -10
- package/src/execution/worktree-registry.ts +6 -12
- package/src/index.ts +413 -6
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -4
- package/src/orchestration/__tests__/args-meta.test.ts +358 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +1 -8
- package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +382 -0
- package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store.test.ts +389 -0
- package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
- package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +77 -0
- package/src/orchestration/__tests__/run-snapshot.test.ts +353 -0
- package/src/orchestration/__tests__/script-generate.test.ts +314 -0
- package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
- package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
- package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
- package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
- package/src/orchestration/agent-opts-resolver.ts +7 -7
- package/src/orchestration/args-meta.ts +198 -0
- package/src/orchestration/error-recovery.ts +416 -146
- package/src/orchestration/execute-agent-call.ts +9 -9
- package/src/orchestration/file-run-store.ts +327 -0
- package/src/orchestration/launcher.ts +35 -26
- package/src/orchestration/lifecycle.ts +355 -79
- package/src/orchestration/models/agent-call.ts +7 -7
- package/src/orchestration/models/budget.ts +5 -5
- package/src/orchestration/models/run-runtime.ts +13 -13
- package/src/orchestration/models/trace.ts +8 -8
- package/src/orchestration/models/workflow-run.ts +27 -27
- package/src/orchestration/models/workflow-script.ts +4 -4
- package/src/orchestration/run-snapshot.ts +266 -0
- package/src/orchestration/script-generate.ts +154 -0
- package/src/orchestration/script-lint.ts +33 -33
- package/src/orchestration/worker-handle.ts +10 -10
- package/src/orchestration/worker-host.ts +10 -10
- package/src/orchestration/worker-script-builder.ts +401 -346
- package/src/orchestration/workflow-files.ts +37 -11
- package/src/orchestration/workflow-run-summary.ts +69 -0
- package/src/orchestration/workflow-script-registry-impl.ts +31 -9
- package/src/shared/__tests__/agent-ref.test.ts +209 -2
- package/src/shared/__tests__/atomic-write.test.ts +267 -0
- package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
- package/src/shared/__tests__/injection-render.test.ts +518 -0
- package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
- package/src/shared/__tests__/resource-discovery.test.ts +3 -2
- package/src/shared/agent-ref.ts +143 -2
- package/src/shared/atomic-write.ts +320 -0
- package/src/shared/bounded-serialize.ts +154 -0
- package/src/shared/injection-render.ts +279 -0
- package/src/shared/meta-parser.ts +41 -1
- package/src/shared/resource-discovery.ts +104 -37
- package/src/shared/resource-meta.ts +14 -0
- package/src/shared/xml-injection.ts +9 -9
- package/workflows/README.md +9 -9
- package/workflows/chain.js +4 -2
- package/workflows/map-reduce.js +5 -3
- package/workflows/parallel.js +5 -3
- package/workflows/review-fix-loop-utils.cjs +14 -3
- package/workflows/review-fix-loop.js +18 -7
- package/workflows/scatter-gather.js +4 -2
- package/dist/chunk-3VOERJPJ.js +0 -22
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// src/orchestration/__tests__/script-generate.test.ts
|
|
2
|
+
//
|
|
3
|
+
// C4-core-script-pipeline(convergence W4)单元测试:generate 校验管线下沉的
|
|
4
|
+
// 行为锁定。覆盖 §4.2 W4 门全部条目:
|
|
5
|
+
// - 五道闸逐字文案(期望串按 pi 现版 actionGenerate 源码逻辑手工构造——
|
|
6
|
+
// pi 侧行为不变是 CA2 前提,文案改动必须同步两处);
|
|
7
|
+
// - round-trip 失败报错含 YAML 诊断与 line/col(parseResourceMetaDetailed
|
|
8
|
+
// 真实调用不 mock,与 pi 测试 TC2/TC5 同原则);
|
|
9
|
+
// - 合法样本落 tmp:目录参数注入生效(真实落盘断言,非 mock fs);
|
|
10
|
+
// - 缺省目录 = pi 布局(相对 cwd resolve,pi 现行为不变);
|
|
11
|
+
// - barrel 逐名探针(红线 9)。
|
|
12
|
+
// 设计权威源:docs/design/subagent-core-convergence.md §3.2 D-6。
|
|
13
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join, resolve } from "node:path";
|
|
16
|
+
|
|
17
|
+
import { describe, expect, it, vi } from "vitest";
|
|
18
|
+
|
|
19
|
+
import type {
|
|
20
|
+
GenerateWorkflowScriptOptions,
|
|
21
|
+
GenerateWorkflowScriptResult,
|
|
22
|
+
WorkflowDirOptions,
|
|
23
|
+
} from "../../index.ts";
|
|
24
|
+
import * as subagentCore from "../../index.ts";
|
|
25
|
+
import { parseResourceMetaDetailed } from "../../shared/meta-parser.ts";
|
|
26
|
+
import { generateWorkflowScript } from "../script-generate.ts";
|
|
27
|
+
|
|
28
|
+
// ============================================================
|
|
29
|
+
// 样本(构造逻辑对齐 pi 侧 tool-workflow-script-generate.test.ts)
|
|
30
|
+
// ============================================================
|
|
31
|
+
|
|
32
|
+
const PI_META_VALID = `/* @pi-meta
|
|
33
|
+
name: test-wf
|
|
34
|
+
description: valid new format
|
|
35
|
+
phases: [a, b]
|
|
36
|
+
parameters:
|
|
37
|
+
type: object
|
|
38
|
+
properties:
|
|
39
|
+
task: { type: string }
|
|
40
|
+
required: [task]
|
|
41
|
+
*/
|
|
42
|
+
const agent = require("./agent");
|
|
43
|
+
agent("worker", { task: $ARGS.task });
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const PI_META_MALFORMED = `/* @pi-meta
|
|
47
|
+
name: test-wf
|
|
48
|
+
description: bad
|
|
49
|
+
broken: indent
|
|
50
|
+
phases: [a]
|
|
51
|
+
*/
|
|
52
|
+
const agent = require("./agent");
|
|
53
|
+
agent("w");
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const PI_META_REGEX_SINGLE_BS = `/* @pi-meta
|
|
57
|
+
name: test-wf
|
|
58
|
+
description: regex
|
|
59
|
+
phases: [A]
|
|
60
|
+
parameters:
|
|
61
|
+
type: object
|
|
62
|
+
patternProperties:
|
|
63
|
+
"^batch\\d+$": { type: string }
|
|
64
|
+
*/
|
|
65
|
+
const agent = require("./agent");
|
|
66
|
+
agent("w");
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
const LEGACY_CONST_META = `const meta = {
|
|
70
|
+
name: "legacy-wf",
|
|
71
|
+
description: "legacy format",
|
|
72
|
+
phases: ["a"]
|
|
73
|
+
};
|
|
74
|
+
const agent = require("./agent");
|
|
75
|
+
agent("w");
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
const NO_META = `const agent = require("./agent");
|
|
79
|
+
agent("w");
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
const ESM_IMPORT = `/* @pi-meta
|
|
83
|
+
name: x
|
|
84
|
+
description: d
|
|
85
|
+
phases: [a]
|
|
86
|
+
*/
|
|
87
|
+
import { foo } from "bar";
|
|
88
|
+
const agent = require("./agent");
|
|
89
|
+
agent("w");
|
|
90
|
+
`;
|
|
91
|
+
|
|
92
|
+
const ESM_EXPORT_NON_META = `/* @pi-meta
|
|
93
|
+
name: x
|
|
94
|
+
description: d
|
|
95
|
+
phases: [a]
|
|
96
|
+
*/
|
|
97
|
+
const agent = require("./agent");
|
|
98
|
+
export const foo = 1;
|
|
99
|
+
agent("w");
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
const NO_AGENT = `/* @pi-meta
|
|
103
|
+
name: x
|
|
104
|
+
description: d
|
|
105
|
+
phases: [a]
|
|
106
|
+
*/
|
|
107
|
+
const x = 1;
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
const SYNTAX_ERROR = `/* @pi-meta
|
|
111
|
+
name: x
|
|
112
|
+
description: d
|
|
113
|
+
phases: [a]
|
|
114
|
+
*/
|
|
115
|
+
const agent = require("./agent");
|
|
116
|
+
agent("w";
|
|
117
|
+
`;
|
|
118
|
+
|
|
119
|
+
// ============================================================
|
|
120
|
+
// 期望串(pi 现版 actionGenerate 文案逐字手工构造)
|
|
121
|
+
// ============================================================
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* round-trip 报错文案模板——pi 版 tool-workflow-script.ts actionGenerate
|
|
125
|
+
* 的模板逐字复制(含源码字面 `(\\d not \d)` 的历史形态:模板字符串里 `\\d`
|
|
126
|
+
* 渲染 `\d`、`\d` 渲染 `d`;两处字符级一致即运行时逐字一致)。
|
|
127
|
+
*/
|
|
128
|
+
function expectedRoundtripError(detailed: {
|
|
129
|
+
error: string;
|
|
130
|
+
linePos?: { line: number; col: number };
|
|
131
|
+
}): string {
|
|
132
|
+
const loc = "linePos" in detailed && detailed.linePos
|
|
133
|
+
? ` (line ${detailed.linePos.line}, col ${detailed.linePos.col})`
|
|
134
|
+
: "";
|
|
135
|
+
return `Generated /* @pi-meta */ YAML cannot be parsed${loc}: ${detailed.error}. Common causes: YAML indent errors, patternProperties regex must use double backslash (\\d not \d), or a stray star-slash inside the YAML body. Fix the meta block and retry.`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** 非.ok 分支的错误文案(窄化辅助:ok 分支直接 fail)。 */
|
|
139
|
+
function errorOf(r: GenerateWorkflowScriptResult): string {
|
|
140
|
+
if (r.ok) throw new Error("expected failure result, got success");
|
|
141
|
+
return r.error;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function makeTmpRoot(): string {
|
|
145
|
+
return mkdtempSync(join(tmpdir(), "subagent-core-gen-"));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============================================================
|
|
149
|
+
// 五道闸(文案逐字对齐 pi 现版)
|
|
150
|
+
// ============================================================
|
|
151
|
+
|
|
152
|
+
describe("generateWorkflowScript 校验闸(文案逐字对齐 pi 现版)", () => {
|
|
153
|
+
it("缺 name/script → 拒(防御性文案逐字)", () => {
|
|
154
|
+
expect(errorOf(generateWorkflowScript("", "x"))).toBe(
|
|
155
|
+
"generate requires 'name' and 'script' parameters",
|
|
156
|
+
);
|
|
157
|
+
expect(errorOf(generateWorkflowScript("x", ""))).toBe(
|
|
158
|
+
"generate requires 'name' and 'script' parameters",
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("ESM import → 拒(文案逐字)", () => {
|
|
163
|
+
const tmpRoot = makeTmpRoot();
|
|
164
|
+
try {
|
|
165
|
+
const r = generateWorkflowScript("esm-wf", ESM_IMPORT, { tmpDir: tmpRoot });
|
|
166
|
+
expect(errorOf(r)).toBe(
|
|
167
|
+
"Script uses ESM 'import' syntax. Workflow scripts run in a CJS Worker — use require() instead.",
|
|
168
|
+
);
|
|
169
|
+
expect(existsSync(join(tmpRoot, "esm-wf.js"))).toBe(false);
|
|
170
|
+
} finally {
|
|
171
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("ESM export(非 meta)→ 拒(文案逐字)", () => {
|
|
176
|
+
const r = generateWorkflowScript("exp-wf", ESM_EXPORT_NON_META);
|
|
177
|
+
expect(errorOf(r)).toBe(
|
|
178
|
+
"Script uses ESM 'export' (non-meta). Use 'const meta = {...}' at top level instead.",
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("无 meta 声明 → 拒(文案逐字)", () => {
|
|
183
|
+
const r = generateWorkflowScript("nometa-wf", NO_META);
|
|
184
|
+
expect(errorOf(r)).toBe(
|
|
185
|
+
"Script must contain a meta declaration: a /* @pi-meta */ YAML block comment (preferred) or legacy const meta = { ... }. The block has the form: a block comment starting with /* @pi-meta followed by YAML (name/description/phases/parameters?/usage?), closed by */ on its own line.",
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("无 agent() 调用 → 拒(文案逐字)", () => {
|
|
190
|
+
const r = generateWorkflowScript("noagent-wf", NO_AGENT);
|
|
191
|
+
expect(errorOf(r)).toBe(
|
|
192
|
+
"Script does not contain any agent() calls. A workflow must call agent() at least once.",
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("语法错误 → 拒(前缀逐字 + V8 诊断非空)", () => {
|
|
197
|
+
const r = generateWorkflowScript("synerr-wf", SYNTAX_ERROR);
|
|
198
|
+
const err = errorOf(r);
|
|
199
|
+
expect(err.startsWith("Syntax error in script: ")).toBe(true);
|
|
200
|
+
expect(err.length > "Syntax error in script: ".length).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("malformed @pi-meta YAML → round-trip 拒,报错含 line/col + 未落盘", () => {
|
|
204
|
+
const tmpRoot = makeTmpRoot();
|
|
205
|
+
try {
|
|
206
|
+
const r = generateWorkflowScript("bad-wf", PI_META_MALFORMED, { tmpDir: tmpRoot });
|
|
207
|
+
const err = errorOf(r);
|
|
208
|
+
// 行列信息保留(ERR4:LLM 自纠正依赖)
|
|
209
|
+
expect(err).toMatch(/ \(line \d+, col \d+\)/);
|
|
210
|
+
// 期望串 = 同一 parser 实测值 + pi 源码模板手工展开 → 逐字一致
|
|
211
|
+
const detailed = parseResourceMetaDetailed(PI_META_MALFORMED, "workflow");
|
|
212
|
+
expect(detailed.ok).toBe(false);
|
|
213
|
+
if (!detailed.ok) {
|
|
214
|
+
expect(detailed.linePos).toBeDefined();
|
|
215
|
+
expect(err).toBe(expectedRoundtripError(detailed));
|
|
216
|
+
}
|
|
217
|
+
expect(existsSync(join(tmpRoot, "bad-wf.js"))).toBe(false);
|
|
218
|
+
} finally {
|
|
219
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("patternProperties 单反斜杠正则 → round-trip 拒(LLM 高频错,含行列)", () => {
|
|
224
|
+
const r = generateWorkflowScript("regex-wf", PI_META_REGEX_SINGLE_BS);
|
|
225
|
+
const err = errorOf(r);
|
|
226
|
+
expect(err).toMatch(/ \(line \d+, col \d+\)/);
|
|
227
|
+
const detailed = parseResourceMetaDetailed(PI_META_REGEX_SINGLE_BS, "workflow");
|
|
228
|
+
expect(detailed.ok).toBe(false);
|
|
229
|
+
if (!detailed.ok) {
|
|
230
|
+
expect(err).toBe(expectedRoundtripError(detailed));
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// ============================================================
|
|
236
|
+
// 合法样本落 tmp(目录参数注入生效)
|
|
237
|
+
// ============================================================
|
|
238
|
+
|
|
239
|
+
describe("generateWorkflowScript 合法样本落 tmp", () => {
|
|
240
|
+
it("注入 tmpDir:真实落盘指定目录,内容逐字节一致", () => {
|
|
241
|
+
const tmpRoot = makeTmpRoot();
|
|
242
|
+
try {
|
|
243
|
+
const r = generateWorkflowScript("my-wf", PI_META_VALID, { tmpDir: tmpRoot });
|
|
244
|
+
expect(r.ok).toBe(true);
|
|
245
|
+
if (!r.ok) throw new Error(r.error);
|
|
246
|
+
expect(r.path).toBe(resolve(tmpRoot, "my-wf.js"));
|
|
247
|
+
expect(existsSync(r.path)).toBe(true);
|
|
248
|
+
expect(readFileSync(r.path, "utf-8")).toBe(PI_META_VALID);
|
|
249
|
+
} finally {
|
|
250
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("legacy const meta(过渡期)→ 过闸落盘(无 @pi-meta 跳过 round-trip)", () => {
|
|
255
|
+
const tmpRoot = makeTmpRoot();
|
|
256
|
+
try {
|
|
257
|
+
const r = generateWorkflowScript("legacy-wf", LEGACY_CONST_META, { tmpDir: tmpRoot });
|
|
258
|
+
expect(r.ok).toBe(true);
|
|
259
|
+
if (!r.ok) throw new Error(r.error);
|
|
260
|
+
expect(existsSync(r.path)).toBe(true);
|
|
261
|
+
} finally {
|
|
262
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("缺省 tmpDir = pi 布局 <cwd>/.pi/workflows/.tmp(pi 现行为不变)", () => {
|
|
267
|
+
const fakeCwd = mkdtempSync(join(tmpdir(), "subagent-core-cwd-"));
|
|
268
|
+
const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue(fakeCwd);
|
|
269
|
+
try {
|
|
270
|
+
const r = generateWorkflowScript("def-wf", PI_META_VALID);
|
|
271
|
+
expect(r.ok).toBe(true);
|
|
272
|
+
if (!r.ok) throw new Error(r.error);
|
|
273
|
+
expect(r.path).toBe(join(fakeCwd, ".pi", "workflows", ".tmp", "def-wf.js"));
|
|
274
|
+
expect(existsSync(r.path)).toBe(true);
|
|
275
|
+
} finally {
|
|
276
|
+
cwdSpy.mockRestore();
|
|
277
|
+
rmSync(fakeCwd, { recursive: true, force: true });
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// ============================================================
|
|
283
|
+
// barrel 逐名探针(红线 9:新增导出必须进 barrel,逐名列出)
|
|
284
|
+
// ============================================================
|
|
285
|
+
|
|
286
|
+
describe("barrel exports probe", () => {
|
|
287
|
+
it("值导出逐名可达(typeof function)", () => {
|
|
288
|
+
const valueExports = [
|
|
289
|
+
"generateWorkflowScript",
|
|
290
|
+
"saveWorkflow",
|
|
291
|
+
"deleteWorkflow",
|
|
292
|
+
] as const;
|
|
293
|
+
const barrel = subagentCore as Record<string, unknown>;
|
|
294
|
+
for (const name of valueExports) {
|
|
295
|
+
expect(typeof barrel[name], `barrel 缺值导出: ${name}`).toBe("function");
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("缺省目录常量经 barrel 可达(值 = pi 布局)", () => {
|
|
300
|
+
const barrel = subagentCore as Record<string, unknown>;
|
|
301
|
+
expect(barrel["DEFAULT_WORKFLOW_TMP_DIR"]).toBe(".pi/workflows/.tmp");
|
|
302
|
+
expect(barrel["DEFAULT_WORKFLOW_SAVED_DIR"]).toBe(".pi/workflows");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("类型导出经 barrel 可引用(typecheck 即探针)", () => {
|
|
306
|
+
const genOpts: GenerateWorkflowScriptOptions = { tmpDir: "/tmp/x" };
|
|
307
|
+
const dirOpts: WorkflowDirOptions = { tmpDir: "/tmp/x", savedDir: "/tmp/y" };
|
|
308
|
+
let result: GenerateWorkflowScriptResult = { ok: true, path: "/p" };
|
|
309
|
+
result = { ok: false, error: "e" };
|
|
310
|
+
expect(genOpts.tmpDir).toBe("/tmp/x");
|
|
311
|
+
expect(dirOpts.savedDir).toBe("/tmp/y");
|
|
312
|
+
expect(result.ok).toBe(false);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [OR-3] worker 侧 pending per-call timeoutMs + abort 广播 测试(真实 Worker 线程,P-T3)。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖(unbounded-wait-audit §4.1 OR-3 / §7.2 T3③ / §7.3 P-T3):
|
|
5
|
+
* - agent() 传 timeoutMs:主线程不回话 → pending 以错误 resolve(不 reject、不放大成
|
|
6
|
+
* 脚本 error)+ 超时 warn 记入 _workerLogs——消息层自己的超时,旧实现零超时永挂
|
|
7
|
+
* - returnMeta 模式超时 resolve {value:"", error}
|
|
8
|
+
* - 主线程不传 timeoutMs(缺省不限):pending 不挂 timer(行为不变)
|
|
9
|
+
* - abort 广播:await 中的 agent() 以 WorkflowAbortedError reject(脚本 catch 可感知
|
|
10
|
+
* 取消)→ 脚本自然收尾 → worker 自然退出(exit 0)
|
|
11
|
+
* - P-T3:abort 广播与 worker 自然退出交错不产 unhandledRejection——广播后立即
|
|
12
|
+
* terminate 的竞态、fire-and-forget 后 return 的自然退出,均不产生主线程
|
|
13
|
+
* unhandledRejection 与 worker 'error' 事件
|
|
14
|
+
*/
|
|
15
|
+
import { Worker } from "node:worker_threads";
|
|
16
|
+
|
|
17
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
18
|
+
|
|
19
|
+
import { buildWorkerScript } from "../worker-script-builder.ts";
|
|
20
|
+
|
|
21
|
+
// ── 判别联合(对齐 worker-script-builder-runtime.test.ts 的守卫形态) ──
|
|
22
|
+
|
|
23
|
+
interface ReturnMsg {
|
|
24
|
+
type: "return";
|
|
25
|
+
result: unknown;
|
|
26
|
+
workerLogs?: Array<{ level: string; message: string }>;
|
|
27
|
+
}
|
|
28
|
+
interface ErrorMsg {
|
|
29
|
+
type: "error";
|
|
30
|
+
error: string;
|
|
31
|
+
workerLogs?: Array<{ level: string; message: string }>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isReturn(m: unknown): m is ReturnMsg {
|
|
35
|
+
return typeof m === "object" && m !== null && (m as { type?: unknown }).type === "return";
|
|
36
|
+
}
|
|
37
|
+
function isError(m: unknown): m is ErrorMsg {
|
|
38
|
+
return typeof m === "object" && m !== null && (m as { type?: unknown }).type === "error";
|
|
39
|
+
}
|
|
40
|
+
function isAgentCall(m: unknown): m is { type: "agent-call"; callId: number } {
|
|
41
|
+
return typeof m === "object" && m !== null && (m as { type?: unknown }).type === "agent-call";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── harness:主线程「不回话」的可控 Worker 运行器 ──────────────
|
|
45
|
+
|
|
46
|
+
interface RunResult {
|
|
47
|
+
returnValue?: unknown;
|
|
48
|
+
errorMessage?: string;
|
|
49
|
+
/** return/error 消息带回的 workerLogs(超时 warn 通路)。 */
|
|
50
|
+
workerLogs: Array<{ level: string; message: string }>;
|
|
51
|
+
exitCode?: number;
|
|
52
|
+
workerError?: string;
|
|
53
|
+
/** 收到的 agent-call 消息数(确认脚本确实发起了调用)。 */
|
|
54
|
+
agentCallCount: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface RunOptions {
|
|
58
|
+
/** 收到 agent-call 后的主线程动作(默认:不回话——pending 留在 worker 侧)。 */
|
|
59
|
+
onAgentCall?: (msg: { callId: number; opts: Record<string, unknown> }, worker: Worker) => void;
|
|
60
|
+
/** 显式 finish(供 abort 竞态场景手工收尾)。 */
|
|
61
|
+
timeoutMs?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const createdWorkers: Worker[] = [];
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
for (const w of createdWorkers.splice(0)) {
|
|
68
|
+
w.terminate().catch(() => {});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
function runWorker(userScript: string, opts: RunOptions = {}): Promise<RunResult> {
|
|
73
|
+
const timeoutMs = opts.timeoutMs ?? (process.env.CI ? 5000 : 2000);
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
const worker = new Worker(buildWorkerScript(userScript), {
|
|
76
|
+
eval: true,
|
|
77
|
+
workerData: {
|
|
78
|
+
scriptPath: "test.js",
|
|
79
|
+
args: {},
|
|
80
|
+
workspace: process.cwd(),
|
|
81
|
+
budget: { maxTokens: 0, usedTokens: 0, usedCost: 0 },
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
createdWorkers.push(worker);
|
|
85
|
+
|
|
86
|
+
const result: RunResult = { workerLogs: [], agentCallCount: 0 };
|
|
87
|
+
let settled = false;
|
|
88
|
+
const finish = (): void => {
|
|
89
|
+
if (settled) return;
|
|
90
|
+
settled = true;
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
clearTimeout(graceTimer);
|
|
93
|
+
resolve(result);
|
|
94
|
+
};
|
|
95
|
+
// 兜底:永不 settle 时 terminate + 以当前观察收尾
|
|
96
|
+
const timer = setTimeout(() => {
|
|
97
|
+
worker.terminate().catch(() => {});
|
|
98
|
+
finish();
|
|
99
|
+
}, timeoutMs);
|
|
100
|
+
// 收到终态消息后的宽限窗:给潜在 worker 'error' 事件一个到达机会,
|
|
101
|
+
// 然后收尾(parentPort 挂着 message listener 时 worker 线程不自然退出,
|
|
102
|
+
// 与生产一致由 terminate 回收——故不依赖 exit 事件断言)。
|
|
103
|
+
let graceTimer: ReturnType<typeof setTimeout>;
|
|
104
|
+
|
|
105
|
+
worker.on("message", (raw: unknown) => {
|
|
106
|
+
if (isAgentCall(raw)) {
|
|
107
|
+
result.agentCallCount += 1;
|
|
108
|
+
opts.onAgentCall?.(raw as { callId: number; opts: Record<string, unknown> }, worker);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (isReturn(raw)) {
|
|
112
|
+
result.returnValue = raw.result;
|
|
113
|
+
result.workerLogs = raw.workerLogs ?? [];
|
|
114
|
+
graceTimer = setTimeout(finish, 150);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (isError(raw)) {
|
|
118
|
+
result.errorMessage = raw.error;
|
|
119
|
+
result.workerLogs = raw.workerLogs ?? [];
|
|
120
|
+
graceTimer = setTimeout(finish, 150);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
worker.on("error", (err: Error) => {
|
|
124
|
+
result.workerError = err.message;
|
|
125
|
+
});
|
|
126
|
+
worker.on("exit", (code: number) => {
|
|
127
|
+
result.exitCode = code;
|
|
128
|
+
finish();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 收集本测试进程的 unhandledRejection(P-T3 断言)。 */
|
|
134
|
+
function trackUnhandledRejections(): { reasons: unknown[]; dispose: () => void } {
|
|
135
|
+
const reasons: unknown[] = [];
|
|
136
|
+
const handler = (reason: unknown): void => {
|
|
137
|
+
reasons.push(reason);
|
|
138
|
+
};
|
|
139
|
+
process.on("unhandledRejection", handler);
|
|
140
|
+
return { reasons, dispose: () => process.off("unhandledRejection", handler) };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── [OR-3] per-call timeoutMs(消息层自己的超时) ─────────────
|
|
144
|
+
|
|
145
|
+
describe("[OR-3] agent() pending per-call timeoutMs", () => {
|
|
146
|
+
it("主线程不回话 + timeoutMs=50 → pending 以错误 resolve([B-5] 单值错误消息字符串)", async () => {
|
|
147
|
+
const script = `
|
|
148
|
+
const r = await agent("never answered", { timeoutMs: 50 });
|
|
149
|
+
return { got: r };
|
|
150
|
+
`;
|
|
151
|
+
const res = await runWorker(script);
|
|
152
|
+
|
|
153
|
+
// 脚本正常 return(超时未放大成脚本 error → 不触发 rebuild 矩阵)
|
|
154
|
+
expect(res.errorMessage).toBeUndefined();
|
|
155
|
+
expect(res.workerError).toBeUndefined();
|
|
156
|
+
expect(res.agentCallCount).toBe(1);
|
|
157
|
+
// [B-5] 非 returnMeta 超时 resolve 单值字符串(对齐 agent-result 失败路径
|
|
158
|
+
// resolve _value = parsedOutput ?? content 的形态)——字符串消费型脚本
|
|
159
|
+
// (r.trim() 类)不再 TypeError
|
|
160
|
+
const got = (res.returnValue as { got: string }).got;
|
|
161
|
+
expect(typeof got).toBe("string");
|
|
162
|
+
expect(got).toContain("timed out after 50ms");
|
|
163
|
+
expect(got).toContain("no agent-result received");
|
|
164
|
+
// 超时 warn 记入 _workerLogs(诊断可见)
|
|
165
|
+
expect(res.workerLogs.some((l) => l.level === "warn" && l.message.includes("timed out after 50ms"))).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("returnMeta 模式超时(对象分支透传 returnMeta)→ resolve {value:\"\", error}", async () => {
|
|
169
|
+
const script = `
|
|
170
|
+
const r = await agent({ prompt: "never answered", timeoutMs: 50, returnMeta: true });
|
|
171
|
+
return r;
|
|
172
|
+
`;
|
|
173
|
+
const res = await runWorker(script);
|
|
174
|
+
|
|
175
|
+
expect(res.errorMessage).toBeUndefined();
|
|
176
|
+
expect(res.returnValue).toEqual({ value: "", error: expect.stringContaining("timed out after 50ms") });
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("[B-5] 字符串消费型脚本(r.trim())在超时路径不 TypeError(resolve 单值字符串)", async () => {
|
|
180
|
+
// 旧实现超时 resolve {content:"", error} 对象 → r.trim() 抛 TypeError →
|
|
181
|
+
// 脚本 error → 放大成 rebuild;修复后 resolve 字符串,脚本按普通失败文本消费
|
|
182
|
+
const script = `
|
|
183
|
+
const r = await agent("never answered", { timeoutMs: 40 });
|
|
184
|
+
return { trimmed: r.trim().length > 0 };
|
|
185
|
+
`;
|
|
186
|
+
const res = await runWorker(script);
|
|
187
|
+
|
|
188
|
+
expect(res.errorMessage).toBeUndefined();
|
|
189
|
+
expect(res.workerError).toBeUndefined();
|
|
190
|
+
expect(res.returnValue).toEqual({ trimmed: true });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("string+secondArg 分支:timeoutMs 透传到 agent-call 消息 opts(此前该分支丢弃该字段)", async () => {
|
|
194
|
+
let seenOpts: Record<string, unknown> | undefined;
|
|
195
|
+
const script = `await agent("with timeout", { timeoutMs: 1234, label: "probe" }); return "unreachable";`;
|
|
196
|
+
await runWorker(script, {
|
|
197
|
+
onAgentCall: (msg, worker) => {
|
|
198
|
+
seenOpts = msg.opts;
|
|
199
|
+
// 回发结果让脚本收尾(透传断言与挂起行为解耦)
|
|
200
|
+
worker.postMessage({ type: "agent-result", callId: msg.callId, result: { content: "ok" }, cached: false });
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(seenOpts?.timeoutMs).toBe(1234);
|
|
205
|
+
expect(seenOpts?.prompt).toBe("with timeout");
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ── [OR-3] abort 广播(worker 侧接收) ───────────────────────
|
|
210
|
+
|
|
211
|
+
describe("[OR-3] abort 广播接收(P-T3:交错不产 unhandledRejection)", () => {
|
|
212
|
+
it("await 中的 agent() 收到 abort → WorkflowAbortedError → 脚本 catch 后自然 return → exit 0", async () => {
|
|
213
|
+
const script = `
|
|
214
|
+
try {
|
|
215
|
+
await agent("will be aborted");
|
|
216
|
+
return "should not reach";
|
|
217
|
+
} catch (e) {
|
|
218
|
+
return { caught: e.name, reason: e.reason };
|
|
219
|
+
}
|
|
220
|
+
`;
|
|
221
|
+
const res = await runWorker(script, {
|
|
222
|
+
onAgentCall: (msg, worker) => {
|
|
223
|
+
worker.postMessage({ type: "abort", reason: "run aborted by user" });
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(res.workerError).toBeUndefined();
|
|
228
|
+
expect(res.errorMessage).toBeUndefined();
|
|
229
|
+
expect(res.returnValue).toEqual({ caught: "WorkflowAbortedError", reason: "run aborted by user" });
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("广播后立即 terminate(竞态):无 worker error、无 unhandledRejection", async () => {
|
|
233
|
+
const tracked = trackUnhandledRejections();
|
|
234
|
+
try {
|
|
235
|
+
const script = `return await agent("race with terminate");`;
|
|
236
|
+
const res = await runWorker(script, {
|
|
237
|
+
onAgentCall: (msg, worker) => {
|
|
238
|
+
// 广播与 terminate 同 tick 发出——模拟 abortRun 的广播→transition(terminate) 竞态
|
|
239
|
+
worker.postMessage({ type: "abort", reason: "race" });
|
|
240
|
+
void worker.terminate();
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
// 无论脚本是否来得及收尾:主线程侧无 error 事件、进程无 unhandledRejection
|
|
244
|
+
expect(res.workerError).toBeUndefined();
|
|
245
|
+
// 给潜在异步 unhandledRejection 一个窗口
|
|
246
|
+
await new Promise((r) => { setTimeout(r, 20); });
|
|
247
|
+
expect(tracked.reasons).toEqual([]);
|
|
248
|
+
} finally {
|
|
249
|
+
tracked.dispose();
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("fire-and-forget agent() 后脚本 return:无 worker error(超时/abort 通路外的孤儿 pending 不产 rejection)", async () => {
|
|
254
|
+
const script = `
|
|
255
|
+
agent("fire and forget");
|
|
256
|
+
return "returned early";
|
|
257
|
+
`;
|
|
258
|
+
const res = await runWorker(script, { timeoutMs: 400 });
|
|
259
|
+
|
|
260
|
+
expect(res.returnValue).toBe("returned early");
|
|
261
|
+
expect(res.workerError).toBeUndefined();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("fire-and-forget + timeoutMs:超时 resolve(非 reject)→ 无 worker error 事件", async () => {
|
|
265
|
+
const script = `
|
|
266
|
+
agent("orphan with timeout", { timeoutMs: 40 });
|
|
267
|
+
return "returned before timeout";
|
|
268
|
+
`;
|
|
269
|
+
const res = await runWorker(script, { timeoutMs: 600 });
|
|
270
|
+
|
|
271
|
+
// 超时以 resolve 收口(容错策略)——即使无人消费该 promise 也不产 rejection
|
|
272
|
+
expect(res.returnValue).toBe("returned before timeout");
|
|
273
|
+
expect(res.workerError).toBeUndefined();
|
|
274
|
+
});
|
|
275
|
+
});
|