@stigmer/runner 3.40.1 → 3.41.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 +3 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/__test-utils__/execution-record-fixture.d.ts +2 -1
- package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
- package/dist/activities/call-agent.js +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -1
- package/dist/activities/discover-mcp-server.js +13 -29
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/fold.d.ts +4 -2
- package/dist/activities/execute-cursor/__test-utils__/fold.js +3 -4
- package/dist/activities/execute-cursor/__test-utils__/fold.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -0
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
- package/dist/activities/execute-cursor/approval-policy.d.ts +1 -29
- package/dist/activities/execute-cursor/approval-policy.js +11 -35
- package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
- package/dist/activities/execute-cursor/boundary-rows.js +4 -4
- package/dist/activities/execute-cursor/boundary-rows.js.map +1 -1
- package/dist/activities/execute-cursor/translator.d.ts +12 -17
- package/dist/activities/execute-cursor/translator.js +13 -30
- package/dist/activities/execute-cursor/translator.js.map +1 -1
- package/dist/activities/execute-cursor/turn-stream.js +4 -2
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +11 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +22 -3
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
- package/dist/activities/execute-deep-agent/inline-publisher.d.ts +33 -2
- package/dist/activities/execute-deep-agent/inline-publisher.js +33 -7
- package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
- package/dist/activities/execute-deep-agent/platform-route.d.ts +48 -13
- package/dist/activities/execute-deep-agent/platform-route.js +87 -19
- package/dist/activities/execute-deep-agent/platform-route.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +20 -3
- package/dist/activities/execute-deep-agent/streaming-side-effects.js +26 -4
- package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
- package/dist/activities/execute-deep-agent/translator.d.ts +5 -4
- package/dist/activities/execute-deep-agent/translator.js +5 -4
- package/dist/activities/execute-deep-agent/translator.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-settle.js +6 -6
- package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-setup.js +7 -7
- package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-stream.d.ts +10 -6
- package/dist/activities/execute-deep-agent/turn-stream.js +6 -16
- package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn.d.ts +4 -1
- package/dist/activities/execute-deep-agent/turn.js +15 -6
- package/dist/activities/execute-deep-agent/turn.js.map +1 -1
- package/dist/activities/workflow-event-activities.js +2 -0
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/exit-backstop.d.ts +64 -0
- package/dist/exit-backstop.js +79 -0
- package/dist/exit-backstop.js.map +1 -0
- package/dist/harness/terminal-table.d.ts +12 -5
- package/dist/harness/terminal-table.js +13 -13
- package/dist/harness/terminal-table.js.map +1 -1
- package/dist/harness/transcript/builder.d.ts +21 -0
- package/dist/harness/transcript/builder.js +53 -31
- package/dist/harness/transcript/builder.js.map +1 -1
- package/dist/harness/transcript/events.d.ts +22 -26
- package/dist/harness/transcript/events.js +6 -5
- package/dist/harness/transcript/events.js.map +1 -1
- package/dist/harness/turn-context.d.ts +3 -1
- package/dist/harness/turn-context.js +5 -3
- package/dist/harness/turn-context.js.map +1 -1
- package/dist/main.js +10 -4
- package/dist/main.js.map +1 -1
- package/dist/middleware/cost-advisory.d.ts +6 -3
- package/dist/middleware/cost-advisory.js +6 -3
- package/dist/middleware/cost-advisory.js.map +1 -1
- package/dist/middleware/execution-budget.d.ts +22 -7
- package/dist/middleware/execution-budget.js +57 -35
- package/dist/middleware/execution-budget.js.map +1 -1
- package/dist/middleware/index.d.ts +10 -7
- package/dist/middleware/index.js +10 -7
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/types.d.ts +6 -1
- package/dist/runner-manager.js +3 -2
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.d.ts +25 -0
- package/dist/runner.js +41 -14
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +23 -13
- package/dist/shared/approval-policy.js +47 -14
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/caller-identity.d.ts +4 -5
- package/dist/shared/caller-identity.js +4 -5
- package/dist/shared/caller-identity.js.map +1 -1
- package/dist/shared/channel-attachment.js +3 -2
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/shared/connect-backfill.d.ts +2 -1
- package/dist/shared/connect-backfill.js +2 -2
- package/dist/shared/connect-backfill.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +10 -1
- package/dist/shared/mcp-resolver.js +11 -2
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/memory-attachment.js +3 -2
- package/dist/shared/memory-attachment.js.map +1 -1
- package/dist/shared/model-error.d.ts +25 -3
- package/dist/shared/model-error.js +78 -10
- package/dist/shared/model-error.js.map +1 -1
- package/dist/shared/platform-server-address.d.ts +71 -0
- package/dist/shared/platform-server-address.js +103 -0
- package/dist/shared/platform-server-address.js.map +1 -0
- package/dist/shared/synthesized-attachment.d.ts +0 -5
- package/dist/shared/synthesized-attachment.js +0 -12
- package/dist/shared/synthesized-attachment.js.map +1 -1
- package/dist/shared/tool-rounds.d.ts +51 -23
- package/dist/shared/tool-rounds.js +67 -24
- package/dist/shared/tool-rounds.js.map +1 -1
- package/dist/shared/workspace/platform-dir.d.ts +15 -0
- package/dist/shared/workspace/platform-dir.js +18 -1
- package/dist/shared/workspace/platform-dir.js.map +1 -1
- package/dist/shared/workspace/stigmer-link.d.ts +21 -14
- package/dist/shared/workspace/stigmer-link.js +91 -29
- package/dist/shared/workspace/stigmer-link.js.map +1 -1
- package/dist/tools/url-guard.d.ts +5 -1
- package/dist/tools/url-guard.js +9 -2
- package/dist/tools/url-guard.js.map +1 -1
- package/dist/worker.d.ts +11 -2
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/dist/workflow-engine/tasks/human-input.d.ts +16 -0
- package/dist/workflow-engine/tasks/human-input.js +79 -37
- package/dist/workflow-engine/tasks/human-input.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +14 -4
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.d.ts +13 -0
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/engine-core.d.ts +25 -0
- package/dist/workflows/engine-core.js.map +1 -1
- package/dist/workflows/execute-from-execution.d.ts +3 -0
- package/dist/workflows/execute-from-execution.js.map +1 -1
- package/dist/workflows/human-input-orchestrator.d.ts +9 -12
- package/dist/workflows/human-input-orchestrator.js +12 -44
- package/dist/workflows/human-input-orchestrator.js.map +1 -1
- package/package.json +5 -5
- package/src/__test-utils__/config-fixture.ts +1 -0
- package/src/__test-utils__/execution-record-fixture.ts +2 -1
- package/src/__test-utils__/harness-contract/runtime-contract.ts +5 -3
- package/src/__test-utils__/test-gate.ts +8 -0
- package/src/__tests__/exit-backstop.test.ts +91 -0
- package/src/__tests__/golden-e2e.test.ts +80 -62
- package/src/__tests__/history-encryption-e2e.test.ts +51 -27
- package/src/__tests__/runner.test.ts +60 -2
- package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
- package/src/activities/__tests__/discover-mcp-server.hang.test.ts +5 -1
- package/src/activities/__tests__/discover-mcp-server.test.ts +90 -49
- package/src/activities/__tests__/workflow-event-activities.test.ts +28 -7
- package/src/activities/call-agent.ts +1 -1
- package/src/activities/discover-mcp-server.ts +27 -37
- package/src/activities/execute-cursor/__test-utils__/fold.ts +6 -4
- package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +4 -29
- package/src/activities/execute-cursor/__tests__/hermetic/bypassed-tool-call.test.ts +120 -0
- package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +5 -5
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.error-delta.status.json +0 -3
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.output-first.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +0 -3
- package/src/activities/execute-cursor/__tests__/hermetic/shell-output-delta.test.ts +9 -7
- package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +6 -1
- package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +8 -11
- package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +1 -3
- package/src/activities/execute-cursor/__tests__/translator.test.ts +68 -15
- package/src/activities/execute-cursor/approval-policy.ts +11 -36
- package/src/activities/execute-cursor/boundary-rows.ts +3 -3
- package/src/activities/execute-cursor/translator.ts +15 -32
- package/src/activities/execute-cursor/turn-stream.ts +4 -1
- package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +33 -2
- package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +4 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +8 -73
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +17 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +86 -26
- package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +9 -5
- package/src/activities/execute-deep-agent/__tests__/hermetic/thrown-turn-drains-publishes.test.ts +159 -0
- package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +84 -0
- package/src/activities/execute-deep-agent/__tests__/platform-route.test.ts +47 -10
- package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +34 -0
- package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +13 -5
- package/src/activities/execute-deep-agent/inline-publisher.ts +47 -7
- package/src/activities/execute-deep-agent/platform-route.ts +109 -20
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +27 -4
- package/src/activities/execute-deep-agent/translator.ts +5 -4
- package/src/activities/execute-deep-agent/turn-settle.ts +6 -6
- package/src/activities/execute-deep-agent/turn-setup.ts +7 -7
- package/src/activities/execute-deep-agent/turn-stream.ts +15 -22
- package/src/activities/execute-deep-agent/turn.ts +16 -7
- package/src/activities/workflow-event-activities.ts +2 -0
- package/src/config.ts +11 -0
- package/src/exit-backstop.ts +97 -0
- package/src/harness/README.md +5 -5
- package/src/harness/terminal-table.ts +13 -13
- package/src/harness/transcript/__tests__/builder.test.ts +93 -21
- package/src/harness/transcript/builder.ts +53 -30
- package/src/harness/transcript/events.ts +22 -27
- package/src/harness/turn-context.ts +5 -3
- package/src/main.ts +9 -5
- package/src/middleware/__tests__/execution-budget.test.ts +181 -103
- package/src/middleware/cost-advisory.ts +6 -3
- package/src/middleware/execution-budget.ts +60 -34
- package/src/middleware/index.ts +10 -7
- package/src/middleware/types.ts +6 -1
- package/src/runner-manager.ts +3 -2
- package/src/runner.ts +57 -15
- package/src/shared/__tests__/approval-policy.test.ts +41 -0
- package/src/shared/__tests__/artifact-storage.test.ts +1 -0
- package/src/shared/__tests__/connect-backfill.test.ts +15 -11
- package/src/shared/__tests__/mcp-resolver.test.ts +72 -10
- package/src/shared/__tests__/model-error.test.ts +71 -3
- package/src/shared/__tests__/platform-server-address.test.ts +122 -0
- package/src/shared/__tests__/tool-rounds.test.ts +64 -14
- package/src/shared/approval-policy.ts +57 -14
- package/src/shared/caller-identity.ts +4 -5
- package/src/shared/channel-attachment.ts +2 -2
- package/src/shared/connect-backfill.ts +5 -1
- package/src/shared/mcp-resolver.ts +11 -1
- package/src/shared/memory-attachment.ts +2 -2
- package/src/shared/model-error.ts +87 -9
- package/src/shared/platform-server-address.ts +123 -0
- package/src/shared/synthesized-attachment.ts +0 -12
- package/src/shared/tool-rounds.ts +70 -24
- package/src/shared/workspace/__tests__/stigmer-link.test.ts +107 -10
- package/src/shared/workspace/platform-dir.ts +19 -1
- package/src/shared/workspace/stigmer-link.ts +99 -29
- package/src/tools/__tests__/url-guard.test.ts +19 -1
- package/src/tools/url-guard.ts +9 -1
- package/src/worker.ts +12 -2
- package/src/workflow-engine/__tests__/golden-execution.test.ts +2 -1
- package/src/workflow-engine/__tests__/loader.test.ts +1 -1
- package/src/workflow-engine/__tests__/tasks/human-input.test.ts +133 -57
- package/src/workflow-engine/tasks/human-input.ts +92 -42
- package/src/workflow-engine/types.ts +16 -4
- package/src/workflows/call-agent-orchestrator.ts +7 -0
- package/src/workflows/engine-core.ts +18 -0
- package/src/workflows/execute-from-execution.ts +2 -1
- package/src/workflows/human-input-orchestrator.ts +15 -56
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { EXIT_BACKSTOP_MS, armExitBackstop, describeResources } from "../exit-backstop.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The exit backstop's contract (exit-backstop.ts): it never holds the loop,
|
|
6
|
+
* it stays silent before its bound, and at the bound it names what is still
|
|
7
|
+
* alive and exits 0, once. The real-process behaviour (a healthy runner
|
|
8
|
+
* exits in milliseconds, a leaked handle is cut at the bound) is proven
|
|
9
|
+
* against the built runner and by the execution conformance harness's
|
|
10
|
+
* slow-exit line; these pin the logic.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
vi.useRealTimers();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function arm(overrides: { boundMs?: number; resources?: string[] } = {}) {
|
|
18
|
+
const exit = vi.fn<(code: number) => void>();
|
|
19
|
+
const log = vi.fn<(line: string) => void>();
|
|
20
|
+
const timer = armExitBackstop({
|
|
21
|
+
boundMs: overrides.boundMs,
|
|
22
|
+
exit,
|
|
23
|
+
log,
|
|
24
|
+
activeResources: () => overrides.resources ?? ["TCPSocketWrap", "Timeout", "TCPSocketWrap"],
|
|
25
|
+
});
|
|
26
|
+
return { exit, log, timer };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("armExitBackstop", () => {
|
|
30
|
+
it("does not hold the event loop", () => {
|
|
31
|
+
const { timer } = arm();
|
|
32
|
+
try {
|
|
33
|
+
expect(timer.hasRef()).toBe(false);
|
|
34
|
+
} finally {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("stays silent before the bound", () => {
|
|
40
|
+
vi.useFakeTimers();
|
|
41
|
+
const { exit, log } = arm();
|
|
42
|
+
|
|
43
|
+
vi.advanceTimersByTime(EXIT_BACKSTOP_MS - 1);
|
|
44
|
+
|
|
45
|
+
expect(exit).not.toHaveBeenCalled();
|
|
46
|
+
expect(log).not.toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("at the bound, names what is still alive and exits 0, once", () => {
|
|
50
|
+
vi.useFakeTimers();
|
|
51
|
+
const { exit, log } = arm();
|
|
52
|
+
|
|
53
|
+
vi.advanceTimersByTime(EXIT_BACKSTOP_MS);
|
|
54
|
+
vi.advanceTimersByTime(EXIT_BACKSTOP_MS);
|
|
55
|
+
|
|
56
|
+
expect(log).toHaveBeenCalledTimes(1);
|
|
57
|
+
expect(log.mock.calls[0]?.[0]).toBe(
|
|
58
|
+
`[runner] Process still alive ${EXIT_BACKSTOP_MS}ms after shutdown; forcing exit. ` +
|
|
59
|
+
"Active resources: TCPSocketWrap×2, Timeout×1",
|
|
60
|
+
);
|
|
61
|
+
expect(exit).toHaveBeenCalledTimes(1);
|
|
62
|
+
expect(exit).toHaveBeenCalledWith(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("honours an explicit bound", () => {
|
|
66
|
+
vi.useFakeTimers();
|
|
67
|
+
const { exit } = arm({ boundMs: 250 });
|
|
68
|
+
|
|
69
|
+
vi.advanceTimersByTime(249);
|
|
70
|
+
expect(exit).not.toHaveBeenCalled();
|
|
71
|
+
vi.advanceTimersByTime(1);
|
|
72
|
+
expect(exit).toHaveBeenCalledWith(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("keeps the bound above the conformance harness's 2 s slow-exit line", () => {
|
|
76
|
+
// Below it, the harness would stop reporting the very leaks this cuts.
|
|
77
|
+
expect(EXIT_BACKSTOP_MS).toBeGreaterThan(2_000);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("describeResources", () => {
|
|
82
|
+
it("groups and sorts, so the same leak reads the same on every exit", () => {
|
|
83
|
+
expect(describeResources(["Timeout", "TCPSocketWrap", "PipeWrap", "TCPSocketWrap"])).toBe(
|
|
84
|
+
"PipeWrap×1, TCPSocketWrap×2, Timeout×1",
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("says so when Node reports nothing", () => {
|
|
89
|
+
expect(describeResources([])).toBe("none reported");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -7,14 +7,21 @@
|
|
|
7
7
|
* - Expression evaluation via local activities (jq-wasm)
|
|
8
8
|
* - Timer handling (sleep/wait)
|
|
9
9
|
*
|
|
10
|
-
* Prerequisites:
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Prerequisites: none beyond `npm install`. `TestWorkflowEnvironment.createTimeSkipping()`
|
|
11
|
+
* downloads and runs Temporal's own test server, which skips a timer as soon as
|
|
12
|
+
* the workflow waits on it, so the goldens that wait minutes (#16 wait-delay,
|
|
13
|
+
* #21 retry-backoff) finish inside the per-test budget.
|
|
13
14
|
*
|
|
14
15
|
* The workflow-engine uses `deepClone` from `clone.ts` which falls
|
|
15
16
|
* back to JSON round-trip when `structuredClone` is unavailable in
|
|
16
|
-
* the Temporal deterministic V8 sandbox.
|
|
17
|
-
*
|
|
17
|
+
* the Temporal deterministic V8 sandbox.
|
|
18
|
+
*
|
|
19
|
+
* Only one failure skips: the Temporal test server cannot boot (no network
|
|
20
|
+
* for the download, say). Each golden then skips by name with the boot
|
|
21
|
+
* error. Anything after the boot (the worker, the workflow bundle, a
|
|
22
|
+
* golden itself) fails. The suite once swallowed every error and returned
|
|
23
|
+
* early from each test, which vitest counts as a pass, so it reported
|
|
24
|
+
* sixteen passes without running a golden (stigmer#1300).
|
|
18
25
|
*
|
|
19
26
|
* Run with: npx vitest run src/__tests__/golden-e2e.test.ts
|
|
20
27
|
*/
|
|
@@ -24,8 +31,9 @@ import { readFileSync } from "node:fs";
|
|
|
24
31
|
import { join, dirname } from "node:path";
|
|
25
32
|
import { fileURLToPath } from "node:url";
|
|
26
33
|
import { loadWorkflowFromYaml } from "../workflow-engine/loader.js";
|
|
34
|
+
import { IN_TEST_GATE } from "../__test-utils__/test-gate.js";
|
|
27
35
|
import { evaluateExpressionBatch } from "../workflow-engine/expression.js";
|
|
28
|
-
import type { ExecuteServerlessWorkflowInput } from "../workflows/engine-core.js";
|
|
36
|
+
import type { EngineActivities, ExecuteServerlessWorkflowInput } from "../workflows/engine-core.js";
|
|
29
37
|
import type { WorkflowModel } from "../workflow-engine/types.js";
|
|
30
38
|
import { INLINE_MODEL_WORKFLOW_TYPE } from "../__test-utils__/workflows/inline-model.js";
|
|
31
39
|
|
|
@@ -48,7 +56,8 @@ type Worker = import("@temporalio/worker").Worker;
|
|
|
48
56
|
let env: TestWorkflowEnvironment | null = null;
|
|
49
57
|
let worker: Worker | null = null;
|
|
50
58
|
let workerRunPromise: Promise<void> | null = null;
|
|
51
|
-
|
|
59
|
+
/** Why the Temporal test server could not boot; `null` once it has. */
|
|
60
|
+
let bootError: string | null = "not attempted";
|
|
52
61
|
|
|
53
62
|
function createMockActivities() {
|
|
54
63
|
return {
|
|
@@ -81,7 +90,12 @@ function createMockActivities() {
|
|
|
81
90
|
ClearWorkflowApprovalStatus: async (): Promise<void> => {},
|
|
82
91
|
UpdateWorkflowFileReviewStatus: async (): Promise<void> => {},
|
|
83
92
|
GetAwaitingFileReviewChangeSetIds: async (): Promise<string[]> => [],
|
|
84
|
-
|
|
93
|
+
GetAgentExecutionProgress: async (): Promise<null> => null,
|
|
94
|
+
ResetEventSequence: async (): Promise<number> => 0,
|
|
95
|
+
EmitWorkflowEvents: async (): Promise<void> => {},
|
|
96
|
+
LoadRecoveryContext: async (): Promise<unknown[]> => [],
|
|
97
|
+
PromoteTaskOutput: async (): Promise<void> => {},
|
|
98
|
+
} satisfies Record<keyof EngineActivities, unknown>;
|
|
85
99
|
}
|
|
86
100
|
|
|
87
101
|
async function runGoldenWorkflow(model: WorkflowModel, workflowInput: unknown = null): Promise<unknown> {
|
|
@@ -96,34 +110,36 @@ async function runGoldenWorkflow(model: WorkflowModel, workflowInput: unknown =
|
|
|
96
110
|
taskQueue: TASK_QUEUE,
|
|
97
111
|
workflowId: `golden-e2e-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
98
112
|
args: [input],
|
|
99
|
-
|
|
113
|
+
// Workflow time, which the test server skips through: #16 alone waits 96s of
|
|
114
|
+
// timers. Real time stays bounded by vitest's per-test timeout.
|
|
115
|
+
workflowExecutionTimeout: "10m",
|
|
100
116
|
});
|
|
101
117
|
}
|
|
102
118
|
|
|
103
119
|
describe("Golden E2E — Temporal TestWorkflowEnvironment", () => {
|
|
104
120
|
beforeAll(async () => {
|
|
121
|
+
const { TestWorkflowEnvironment: TWE } = await import("@temporalio/testing");
|
|
122
|
+
const { Worker: W } = await import("@temporalio/worker");
|
|
123
|
+
|
|
105
124
|
try {
|
|
106
|
-
|
|
107
|
-
const { Worker: W } = await import("@temporalio/worker");
|
|
108
|
-
|
|
109
|
-
env = await TWE.createLocal();
|
|
110
|
-
worker = await W.create({
|
|
111
|
-
connection: env.nativeConnection,
|
|
112
|
-
taskQueue: TASK_QUEUE,
|
|
113
|
-
workflowsPath: WORKFLOWS_PATH,
|
|
114
|
-
activities: createMockActivities(),
|
|
115
|
-
});
|
|
116
|
-
workerRunPromise = worker.run();
|
|
117
|
-
|
|
118
|
-
// Smoke test: execute simplest workflow to verify sandbox compatibility
|
|
119
|
-
const smokeModel = loadWorkflowFromYaml(loadGolden("01-operation-basic.yaml"));
|
|
120
|
-
await runGoldenWorkflow(smokeModel);
|
|
121
|
-
envReady = true;
|
|
125
|
+
env = await TWE.createTimeSkipping();
|
|
122
126
|
} catch (err: unknown) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
// Inside a gate the test server is a provided dependency: not booting is a failure.
|
|
128
|
+
if (IN_TEST_GATE) throw err;
|
|
129
|
+
bootError = err instanceof Error ? err.message : String(err);
|
|
130
|
+
console.warn(`Temporal test server cannot boot; every golden skips: ${bootError}`);
|
|
131
|
+
return;
|
|
126
132
|
}
|
|
133
|
+
bootError = null;
|
|
134
|
+
|
|
135
|
+
// Past the boot, a failure is the suite's to report, not to skip.
|
|
136
|
+
worker = await W.create({
|
|
137
|
+
connection: env.nativeConnection,
|
|
138
|
+
taskQueue: TASK_QUEUE,
|
|
139
|
+
workflowsPath: WORKFLOWS_PATH,
|
|
140
|
+
activities: createMockActivities(),
|
|
141
|
+
});
|
|
142
|
+
workerRunPromise = worker.run();
|
|
127
143
|
}, 60_000);
|
|
128
144
|
|
|
129
145
|
afterAll(async () => {
|
|
@@ -135,117 +151,119 @@ describe("Golden E2E — Temporal TestWorkflowEnvironment", () => {
|
|
|
135
151
|
}, 30_000);
|
|
136
152
|
|
|
137
153
|
// ─────────────────────────────────────────────────────────────────
|
|
138
|
-
// Each
|
|
139
|
-
// cannot start or the smoke test fails.
|
|
154
|
+
// Each golden skips by name when the test server could not boot.
|
|
140
155
|
// ─────────────────────────────────────────────────────────────────
|
|
141
156
|
|
|
142
|
-
it("#01 operation-basic — sequential set tasks", async () => {
|
|
143
|
-
if (
|
|
157
|
+
it("#01 operation-basic — sequential set tasks", async (testCtx) => {
|
|
158
|
+
if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
|
|
144
159
|
const model = loadWorkflowFromYaml(loadGolden("01-operation-basic.yaml"));
|
|
145
160
|
const result = await runGoldenWorkflow(model);
|
|
146
161
|
expect(result).toEqual({ workflow_completed: true });
|
|
147
162
|
});
|
|
148
163
|
|
|
149
|
-
it("#14 try-catch-raise — error handling pipeline", async () => {
|
|
150
|
-
if (
|
|
164
|
+
it("#14 try-catch-raise — error handling pipeline", async (testCtx) => {
|
|
165
|
+
if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
|
|
151
166
|
const model = loadWorkflowFromYaml(loadGolden("14-try-catch-raise.yaml"));
|
|
152
167
|
const result = await runGoldenWorkflow(model);
|
|
153
168
|
expect(result).toEqual({ phase: "completed" });
|
|
154
169
|
});
|
|
155
170
|
|
|
156
|
-
it("#15 fork-parallel — non-compete branches", async () => {
|
|
157
|
-
if (
|
|
171
|
+
it("#15 fork-parallel — non-compete branches", async (testCtx) => {
|
|
172
|
+
if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
|
|
158
173
|
const model = loadWorkflowFromYaml(loadGolden("15-fork-parallel.yaml"));
|
|
159
174
|
const result = await runGoldenWorkflow(model);
|
|
160
175
|
expect(result).toBeDefined();
|
|
161
176
|
});
|
|
162
177
|
|
|
163
|
-
it("#02 switch-conditional — HTTP + conditional branching", async () => {
|
|
164
|
-
if (
|
|
178
|
+
it("#02 switch-conditional — HTTP + conditional branching", async (testCtx) => {
|
|
179
|
+
if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
|
|
165
180
|
const model = loadWorkflowFromYaml(loadGolden("02-switch-conditional.yaml"));
|
|
166
181
|
const result = await runGoldenWorkflow(model);
|
|
167
182
|
expect(result).toBeDefined();
|
|
168
183
|
});
|
|
169
184
|
|
|
170
|
-
it("#03 foreach-loop — iteration with HTTP calls", async () => {
|
|
171
|
-
if (
|
|
185
|
+
it("#03 foreach-loop — iteration with HTTP calls", async (testCtx) => {
|
|
186
|
+
if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
|
|
172
187
|
const model = loadWorkflowFromYaml(loadGolden("03-foreach-loop.yaml"));
|
|
173
188
|
const result = await runGoldenWorkflow(model, { items: ["a", "b", "c"] });
|
|
174
189
|
expect(result).toBeDefined();
|
|
175
190
|
});
|
|
176
191
|
|
|
177
|
-
it("#04 parallel-concurrent — fork with HTTP branches", async () => {
|
|
178
|
-
if (
|
|
192
|
+
it("#04 parallel-concurrent — fork with HTTP branches", async (testCtx) => {
|
|
193
|
+
if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
|
|
179
194
|
const model = loadWorkflowFromYaml(loadGolden("04-parallel-concurrent.yaml"));
|
|
180
195
|
const result = await runGoldenWorkflow(model);
|
|
181
196
|
expect(result).toBeDefined();
|
|
182
197
|
});
|
|
183
198
|
|
|
184
|
-
it("#06 sleep-delay — wait + HTTP calls", async () => {
|
|
185
|
-
if (
|
|
199
|
+
it("#06 sleep-delay — wait + HTTP calls", async (testCtx) => {
|
|
200
|
+
if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
|
|
186
201
|
const model = loadWorkflowFromYaml(loadGolden("06-sleep-delay.yaml"));
|
|
187
202
|
const result = await runGoldenWorkflow(model);
|
|
188
203
|
expect(result).toBeDefined();
|
|
189
204
|
});
|
|
190
205
|
|
|
191
|
-
it("#07 inject-transform — expressions + HTTP", async () => {
|
|
192
|
-
if (
|
|
206
|
+
it("#07 inject-transform — expressions + HTTP", async (testCtx) => {
|
|
207
|
+
if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
|
|
193
208
|
const model = loadWorkflowFromYaml(loadGolden("07-inject-transform.yaml"));
|
|
194
209
|
const result = await runGoldenWorkflow(model, { a: 10, b: 20 });
|
|
195
210
|
expect(result).toBeDefined();
|
|
196
211
|
});
|
|
197
212
|
|
|
198
|
-
it("#08 error-retry — try/catch with HTTP", async () => {
|
|
199
|
-
if (
|
|
213
|
+
it("#08 error-retry — try/catch with HTTP", async (testCtx) => {
|
|
214
|
+
if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
|
|
200
215
|
const model = loadWorkflowFromYaml(loadGolden("08-error-retry.yaml"));
|
|
201
216
|
const result = await runGoldenWorkflow(model);
|
|
202
217
|
expect(result).toBeDefined();
|
|
203
218
|
});
|
|
204
219
|
|
|
205
|
-
it("#11 claimcheck-large-payload — sequential HTTP calls", async () => {
|
|
206
|
-
if (
|
|
220
|
+
it("#11 claimcheck-large-payload — sequential HTTP calls", async (testCtx) => {
|
|
221
|
+
if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
|
|
207
222
|
const model = loadWorkflowFromYaml(loadGolden("11-claimcheck-large-payload.yaml"));
|
|
208
223
|
const result = await runGoldenWorkflow(model);
|
|
209
224
|
expect(result).toBeDefined();
|
|
210
225
|
});
|
|
211
226
|
|
|
212
|
-
it("#12 claimcheck-between-steps — HTTP chain", async () => {
|
|
213
|
-
if (
|
|
227
|
+
it("#12 claimcheck-between-steps — HTTP chain", async (testCtx) => {
|
|
228
|
+
if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
|
|
214
229
|
const model = loadWorkflowFromYaml(loadGolden("12-claimcheck-between-steps.yaml"));
|
|
215
230
|
const result = await runGoldenWorkflow(model);
|
|
216
231
|
expect(result).toBeDefined();
|
|
217
232
|
});
|
|
218
233
|
|
|
219
|
-
it("#16 wait-delay — timer durations", async () => {
|
|
220
|
-
if (
|
|
234
|
+
it("#16 wait-delay — timer durations", async (testCtx) => {
|
|
235
|
+
if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
|
|
221
236
|
const model = loadWorkflowFromYaml(loadGolden("16-wait-delay.yaml"));
|
|
222
237
|
const result = await runGoldenWorkflow(model);
|
|
223
238
|
expect(result).toEqual({ completed: true });
|
|
224
239
|
});
|
|
225
240
|
|
|
226
|
-
|
|
227
|
-
|
|
241
|
+
// quarantined: stigmer#1322 — `run.workflow` starts its child by the workflow's
|
|
242
|
+
// name, which no worker registers; the kind is refused at save until that issue
|
|
243
|
+
// lands, and this golden is its end-to-end reproduction.
|
|
244
|
+
it.skip("#18 run-task — script + shell + workflow", async (testCtx) => {
|
|
245
|
+
if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
|
|
228
246
|
const model = loadWorkflowFromYaml(loadGolden("18-run-task.yaml"));
|
|
229
247
|
const result = await runGoldenWorkflow(model);
|
|
230
248
|
expect(result).toEqual({ all_runs_done: true });
|
|
231
249
|
});
|
|
232
250
|
|
|
233
|
-
it("#19 emit-event — CloudEvents envelope", async () => {
|
|
234
|
-
if (
|
|
251
|
+
it("#19 emit-event — CloudEvents envelope", async (testCtx) => {
|
|
252
|
+
if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
|
|
235
253
|
const model = loadWorkflowFromYaml(loadGolden("19-emit-event.yaml"));
|
|
236
254
|
const result = await runGoldenWorkflow(model);
|
|
237
255
|
expect(result).toEqual({ events_emitted: true });
|
|
238
256
|
});
|
|
239
257
|
|
|
240
|
-
it("#21 retry-backoff — catch-level retry", async () => {
|
|
241
|
-
if (
|
|
258
|
+
it("#21 retry-backoff — catch-level retry", async (testCtx) => {
|
|
259
|
+
if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
|
|
242
260
|
const model = loadWorkflowFromYaml(loadGolden("21-retry-backoff.yaml"));
|
|
243
261
|
const result = await runGoldenWorkflow(model);
|
|
244
262
|
expect(result).toEqual({ phase: "completed" });
|
|
245
263
|
});
|
|
246
264
|
|
|
247
|
-
it("#23 notification — webhook notification", async () => {
|
|
248
|
-
if (
|
|
265
|
+
it("#23 notification — webhook notification", async (testCtx) => {
|
|
266
|
+
if (bootError !== null) return testCtx.skip(`Temporal test server cannot boot: ${bootError}`);
|
|
249
267
|
const model = loadWorkflowFromYaml(loadGolden("23-notification.yaml"));
|
|
250
268
|
const result = await runGoldenWorkflow(model);
|
|
251
269
|
expect(result).toEqual({ notifications_sent: true });
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* converter has no Void special-case — a data-bearing encrypted result
|
|
16
16
|
* would fail its converter lookup.
|
|
17
17
|
*
|
|
18
|
-
* Follows the golden-e2e pattern:
|
|
19
|
-
* Temporal
|
|
18
|
+
* Follows the golden-e2e pattern: the test skips by name only when the
|
|
19
|
+
* Temporal dev server cannot boot; any later failure fails it.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
@@ -24,9 +24,11 @@ import { randomBytes } from "node:crypto";
|
|
|
24
24
|
import { join, dirname } from "node:path";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
26
|
import { loadWorkflowFromYaml } from "../workflow-engine/loader.js";
|
|
27
|
+
import { IN_TEST_GATE } from "../__test-utils__/test-gate.js";
|
|
27
28
|
import { evaluateExpressionBatch } from "../workflow-engine/expression.js";
|
|
28
29
|
import { EncryptionPayloadCodec } from "@stigmer/temporal-codecs";
|
|
29
|
-
import type { ExecuteServerlessWorkflowInput } from "../workflows/engine-core.js";
|
|
30
|
+
import type { EngineActivities, ExecuteServerlessWorkflowInput } from "../workflows/engine-core.js";
|
|
31
|
+
import type { HydrateActivities } from "../workflows/execute-from-execution.js";
|
|
30
32
|
|
|
31
33
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
32
34
|
const WORKFLOWS_PATH = join(__dirname, "../workflows/index.ts");
|
|
@@ -68,7 +70,8 @@ type Worker = import("@temporalio/worker").Worker;
|
|
|
68
70
|
let env: TestWorkflowEnvironment | null = null;
|
|
69
71
|
let worker: Worker | null = null;
|
|
70
72
|
let workerRunPromise: Promise<void> | null = null;
|
|
71
|
-
|
|
73
|
+
/** Why the Temporal dev server could not boot; `null` once it has. */
|
|
74
|
+
let bootError: string | null = "not attempted";
|
|
72
75
|
|
|
73
76
|
function createMockActivities() {
|
|
74
77
|
return {
|
|
@@ -90,6 +93,23 @@ function createMockActivities() {
|
|
|
90
93
|
EmitWorkflowEvents: async (): Promise<void> => {},
|
|
91
94
|
LoadRecoveryContext: async (): Promise<unknown[]> => [],
|
|
92
95
|
PromoteTaskOutput: async (): Promise<void> => {},
|
|
96
|
+
CallGrpc: unscheduled("CallGrpc"),
|
|
97
|
+
CallFunction: unscheduled("CallFunction"),
|
|
98
|
+
RunScript: unscheduled("RunScript"),
|
|
99
|
+
RunShell: unscheduled("RunShell"),
|
|
100
|
+
CallAgent: unscheduled("CallAgent"),
|
|
101
|
+
UpdateWorkflowTaskApprovalStatus: unscheduled("UpdateWorkflowTaskApprovalStatus"),
|
|
102
|
+
ClearWorkflowApprovalStatus: unscheduled("ClearWorkflowApprovalStatus"),
|
|
103
|
+
UpdateWorkflowFileReviewStatus: unscheduled("UpdateWorkflowFileReviewStatus"),
|
|
104
|
+
GetAwaitingFileReviewChangeSetIds: unscheduled("GetAwaitingFileReviewChangeSetIds"),
|
|
105
|
+
GetAgentExecutionProgress: unscheduled("GetAgentExecutionProgress"),
|
|
106
|
+
} satisfies Record<keyof (EngineActivities & HydrateActivities), unknown>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** An activity the secret-bearing workflow never schedules; reaching it is a test bug. */
|
|
110
|
+
function unscheduled(name: string): () => Promise<never> {
|
|
111
|
+
return async () => {
|
|
112
|
+
throw new Error(`${name} is not scheduled by the secret-bearing workflow`);
|
|
93
113
|
};
|
|
94
114
|
}
|
|
95
115
|
|
|
@@ -110,31 +130,35 @@ function collectByteFields(value: unknown, out: Uint8Array[]): void {
|
|
|
110
130
|
|
|
111
131
|
describe("History encryption tripwire — Temporal TestWorkflowEnvironment", () => {
|
|
112
132
|
beforeAll(async () => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const { Worker: W } = await import("@temporalio/worker");
|
|
133
|
+
const { TestWorkflowEnvironment: TWE } = await import("@temporalio/testing");
|
|
134
|
+
const { Worker: W } = await import("@temporalio/worker");
|
|
116
135
|
|
|
136
|
+
try {
|
|
117
137
|
env = await TWE.createLocal();
|
|
118
|
-
worker = await W.create({
|
|
119
|
-
connection: env.nativeConnection,
|
|
120
|
-
taskQueue: TASK_QUEUE,
|
|
121
|
-
workflowsPath: WORKFLOWS_PATH,
|
|
122
|
-
activities: createMockActivities(),
|
|
123
|
-
dataConverter: {
|
|
124
|
-
payloadCodecs: [
|
|
125
|
-
new EncryptionPayloadCodec({
|
|
126
|
-
primary: { keyId: "tripwire-key", key: randomBytes(32) },
|
|
127
|
-
}),
|
|
128
|
-
],
|
|
129
|
-
},
|
|
130
|
-
});
|
|
131
|
-
workerRunPromise = worker.run();
|
|
132
|
-
envReady = true;
|
|
133
138
|
} catch (err: unknown) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
139
|
+
// Inside a gate the test server is a provided dependency: not booting is a failure.
|
|
140
|
+
if (IN_TEST_GATE) throw err;
|
|
141
|
+
bootError = err instanceof Error ? err.message : String(err);
|
|
142
|
+
console.warn(`Temporal dev server cannot boot; the tripwire skips: ${bootError}`);
|
|
143
|
+
return;
|
|
137
144
|
}
|
|
145
|
+
bootError = null;
|
|
146
|
+
|
|
147
|
+
// Past the boot, a failure is the suite's to report, not to skip.
|
|
148
|
+
worker = await W.create({
|
|
149
|
+
connection: env.nativeConnection,
|
|
150
|
+
taskQueue: TASK_QUEUE,
|
|
151
|
+
workflowsPath: WORKFLOWS_PATH,
|
|
152
|
+
activities: createMockActivities(),
|
|
153
|
+
dataConverter: {
|
|
154
|
+
payloadCodecs: [
|
|
155
|
+
new EncryptionPayloadCodec({
|
|
156
|
+
primary: { keyId: "tripwire-key", key: randomBytes(32) },
|
|
157
|
+
}),
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
workerRunPromise = worker.run();
|
|
138
162
|
}, 60_000);
|
|
139
163
|
|
|
140
164
|
afterAll(async () => {
|
|
@@ -145,8 +169,8 @@ describe("History encryption tripwire — Temporal TestWorkflowEnvironment", ()
|
|
|
145
169
|
if (env) await env.teardown();
|
|
146
170
|
}, 30_000);
|
|
147
171
|
|
|
148
|
-
it("records no plaintext secret bytes anywhere in workflow history", async () => {
|
|
149
|
-
if (
|
|
172
|
+
it("records no plaintext secret bytes anywhere in workflow history", async (testCtx) => {
|
|
173
|
+
if (bootError !== null || !env) return testCtx.skip(`Temporal dev server cannot boot: ${bootError}`);
|
|
150
174
|
|
|
151
175
|
const workflowId = `tripwire-${Date.now()}`;
|
|
152
176
|
// The starting client has NO codec, mirroring the Java/Go
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { mapOptionsToConfig } from "../runner.js";
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { mapOptionsToConfig, releaseAfterDrain } from "../runner.js";
|
|
3
3
|
import type { StigmerRunnerOptions, StigmerRunner } from "../runner.js";
|
|
4
4
|
import type { TokenRef } from "../config.js";
|
|
5
5
|
|
|
@@ -224,3 +224,61 @@ describe("public API exports", () => {
|
|
|
224
224
|
expect(typeof mod.createStigmerRunner).toBe("function");
|
|
225
225
|
});
|
|
226
226
|
});
|
|
227
|
+
|
|
228
|
+
describe("releaseAfterDrain", () => {
|
|
229
|
+
function resources(opts: { harnessFails?: boolean; closeFails?: boolean } = {}) {
|
|
230
|
+
const order: string[] = [];
|
|
231
|
+
return {
|
|
232
|
+
order,
|
|
233
|
+
value: {
|
|
234
|
+
shutdownHarnesses: vi.fn(async () => {
|
|
235
|
+
order.push("harnesses");
|
|
236
|
+
if (opts.harnessFails) {
|
|
237
|
+
throw new Error("harness boom");
|
|
238
|
+
}
|
|
239
|
+
}),
|
|
240
|
+
connection: {
|
|
241
|
+
close: vi.fn(async () => {
|
|
242
|
+
order.push("connection");
|
|
243
|
+
if (opts.closeFails) {
|
|
244
|
+
throw new Error("close boom");
|
|
245
|
+
}
|
|
246
|
+
}),
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
it("releases the harnesses, then closes the Temporal connection", async () => {
|
|
253
|
+
const r = resources();
|
|
254
|
+
const logError = vi.fn<(line: string) => void>();
|
|
255
|
+
|
|
256
|
+
await releaseAfterDrain(r.value, logError);
|
|
257
|
+
|
|
258
|
+
expect(r.order).toEqual(["harnesses", "connection"]);
|
|
259
|
+
expect(logError).not.toHaveBeenCalled();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("still closes the connection when a harness teardown fails, and logs it", async () => {
|
|
263
|
+
const r = resources({ harnessFails: true });
|
|
264
|
+
const logError = vi.fn<(line: string) => void>();
|
|
265
|
+
|
|
266
|
+
await releaseAfterDrain(r.value, logError);
|
|
267
|
+
|
|
268
|
+
expect(r.order).toEqual(["harnesses", "connection"]);
|
|
269
|
+
expect(logError).toHaveBeenCalledTimes(1);
|
|
270
|
+
expect(logError.mock.calls[0]?.[0]).toContain("Harness shutdown failed after the worker drained");
|
|
271
|
+
expect(logError.mock.calls[0]?.[0]).toContain("harness boom");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("logs a failed close instead of throwing, so a clean stop stays clean", async () => {
|
|
275
|
+
const r = resources({ closeFails: true });
|
|
276
|
+
const logError = vi.fn<(line: string) => void>();
|
|
277
|
+
|
|
278
|
+
await expect(releaseAfterDrain(r.value, logError)).resolves.toBeUndefined();
|
|
279
|
+
|
|
280
|
+
expect(logError).toHaveBeenCalledTimes(1);
|
|
281
|
+
expect(logError.mock.calls[0]?.[0]).toContain("Temporal connection close failed after the worker drained");
|
|
282
|
+
expect(logError.mock.calls[0]?.[0]).toContain("close boom");
|
|
283
|
+
});
|
|
284
|
+
});
|
|
@@ -727,6 +727,7 @@ function makeConfig() {
|
|
|
727
727
|
temporalConnection: {},
|
|
728
728
|
stigmerBackendEndpoint: "http://localhost:7234",
|
|
729
729
|
mcpBridgeEndpoint: null,
|
|
730
|
+
mcpPublicEndpoint: null,
|
|
730
731
|
stigmerTokenRef: { current: "test-token" },
|
|
731
732
|
cursorApiKey: "",
|
|
732
733
|
workspaceRootDir: "/tmp/test",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
16
16
|
import { createServer, type Server } from "node:http";
|
|
17
|
+
import { testConfig } from "../../__test-utils__/config-fixture.js";
|
|
18
|
+
|
|
19
|
+
/** The runner endpoints discovery fills STIGMER_SERVER_ADDRESS from. */
|
|
20
|
+
const PLATFORM_ENDPOINTS = testConfig();
|
|
17
21
|
|
|
18
22
|
vi.mock("../../idle-watchdog.js", () => ({
|
|
19
23
|
activityStarted: vi.fn(),
|
|
@@ -85,7 +89,7 @@ describe("discovery against a 4xx-then-silent-SSE endpoint (issue #239)", () =>
|
|
|
85
89
|
|
|
86
90
|
const promise = discoverMcpServer(
|
|
87
91
|
{ mcpServerId: "mcp-monday" },
|
|
88
|
-
{ stigmerClient: stigmerClient as never, transportPosture: "stdio-forbidden" },
|
|
92
|
+
{ stigmerClient: stigmerClient as never, transportPosture: "stdio-forbidden", platformEndpoints: PLATFORM_ENDPOINTS },
|
|
89
93
|
);
|
|
90
94
|
// Attach the rejection expectation BEFORE advancing so the rejection is
|
|
91
95
|
// never momentarily unhandled.
|