@stigmer/runner 3.22.0 → 3.22.1-dev.20260922095755
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 +1 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/attach-session.js +0 -1
- package/dist/activities/attach-session.js.map +1 -1
- package/dist/activities/call-agent-status.d.ts +18 -11
- package/dist/activities/call-agent-status.js +21 -24
- package/dist/activities/call-agent-status.js.map +1 -1
- package/dist/activities/call-agent.d.ts +2 -2
- package/dist/activities/call-agent.js +4 -6
- package/dist/activities/call-agent.js.map +1 -1
- package/dist/activities/call-function.d.ts +4 -2
- package/dist/activities/call-function.js +13 -4
- package/dist/activities/call-function.js.map +1 -1
- package/dist/activities/classify-tool-approvals.js +1 -1
- package/dist/activities/classify-tool-approvals.js.map +1 -1
- package/dist/activities/discover-mcp-server.js +0 -1
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/emit-event.d.ts +6 -1
- package/dist/activities/emit-event.js +5 -14
- package/dist/activities/emit-event.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -1
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
- package/dist/activities/execute-cursor/adapter.js +0 -1
- package/dist/activities/execute-cursor/adapter.js.map +1 -1
- package/dist/activities/execute-cursor/turn-setup.d.ts +1 -1
- package/dist/activities/execute-cursor/turn-setup.js +5 -6
- package/dist/activities/execute-cursor/turn-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
- package/dist/activities/execute-deep-agent/adapter.js +1 -1
- package/dist/activities/execute-deep-agent/adapter.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-setup.d.ts +1 -1
- package/dist/activities/execute-deep-agent/turn-setup.js +5 -3
- package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
- package/dist/activities/generate-session-subject.js +1 -2
- package/dist/activities/generate-session-subject.js.map +1 -1
- package/dist/activities/hydrate-workflow-execution.js +0 -1
- package/dist/activities/hydrate-workflow-execution.js.map +1 -1
- package/dist/activities/promote-task-output.d.ts +2 -1
- package/dist/activities/promote-task-output.js +10 -14
- package/dist/activities/promote-task-output.js.map +1 -1
- package/dist/activities/workflow-event-activities.d.ts +9 -7
- package/dist/activities/workflow-event-activities.js +14 -18
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +13 -13
- package/dist/client/stigmer-client.js +4 -7
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/config.d.ts +40 -20
- package/dist/config.js +25 -9
- package/dist/config.js.map +1 -1
- package/dist/harness/registry.js +0 -1
- package/dist/harness/registry.js.map +1 -1
- package/dist/harness/turn-context.js +2 -4
- package/dist/harness/turn-context.js.map +1 -1
- package/dist/main.js +13 -6
- package/dist/main.js.map +1 -1
- package/dist/runner-manager.d.ts +8 -6
- package/dist/runner-manager.js +20 -10
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner-token-coordinator.d.ts +8 -3
- package/dist/runner-token-coordinator.js +8 -3
- package/dist/runner-token-coordinator.js.map +1 -1
- package/dist/runner.d.ts +10 -2
- package/dist/runner.js +31 -25
- package/dist/runner.js.map +1 -1
- package/dist/shared/artifact-storage.d.ts +6 -6
- package/dist/shared/artifact-storage.js +5 -12
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/checkpointer/factory.js +5 -1
- package/dist/shared/checkpointer/factory.js.map +1 -1
- package/dist/shared/checkpointer/http-saver.d.ts +13 -2
- package/dist/shared/checkpointer/http-saver.js +17 -5
- package/dist/shared/checkpointer/http-saver.js.map +1 -1
- package/dist/shared/checkpointer/types.d.ts +10 -1
- package/dist/shared/checkpointer/types.js +16 -0
- package/dist/shared/checkpointer/types.js.map +1 -1
- package/dist/shared/extract-structured-output.js +1 -1
- package/dist/shared/extract-structured-output.js.map +1 -1
- package/package.json +5 -5
- package/src/__test-utils__/config-fixture.ts +2 -2
- package/src/__test-utils__/module-specifiers.ts +45 -3
- package/src/__tests__/config-boundary.test.ts +135 -0
- package/src/__tests__/config.test.ts +7 -16
- package/src/__tests__/runner-manager.test.ts +28 -6
- package/src/__tests__/runner.test.ts +28 -4
- package/src/activities/__tests__/attach-session.test.ts +1 -1
- package/src/activities/__tests__/call-agent-contracts.test.ts +4 -7
- package/src/activities/__tests__/call-agent-status.test.ts +17 -16
- package/src/activities/__tests__/call-agent.test.ts +28 -7
- package/src/activities/__tests__/call-function.test.ts +5 -4
- package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -1
- package/src/activities/__tests__/discover-mcp-server.test.ts +1 -1
- package/src/activities/__tests__/generate-session-subject-wire.test.ts +1 -1
- package/src/activities/__tests__/workflow-event-activities.test.ts +21 -23
- package/src/activities/attach-session.ts +0 -1
- package/src/activities/call-agent-status.ts +28 -20
- package/src/activities/call-agent.ts +5 -6
- package/src/activities/call-function.ts +15 -3
- package/src/activities/classify-tool-approvals.ts +1 -1
- package/src/activities/discover-mcp-server.ts +0 -1
- package/src/activities/emit-event.ts +7 -16
- package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -1
- package/src/activities/execute-cursor/adapter.ts +0 -1
- package/src/activities/execute-cursor/turn-setup.ts +6 -7
- package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +1 -1
- package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +2 -2
- package/src/activities/execute-deep-agent/__tests__/summarization-verification.test.ts +1 -1
- package/src/activities/execute-deep-agent/adapter.ts +1 -1
- package/src/activities/execute-deep-agent/turn-setup.ts +6 -4
- package/src/activities/generate-session-subject.ts +1 -2
- package/src/activities/hydrate-workflow-execution.ts +0 -1
- package/src/activities/promote-task-output.ts +11 -15
- package/src/activities/workflow-event-activities.ts +17 -18
- package/src/client/__tests__/stigmer-client.test.ts +8 -19
- package/src/client/stigmer-client.ts +18 -20
- package/src/config.ts +51 -23
- package/src/harness/registry.ts +0 -1
- package/src/harness/turn-context.ts +2 -4
- package/src/main.ts +13 -6
- package/src/runner-manager.ts +25 -15
- package/src/runner-token-coordinator.ts +8 -3
- package/src/runner.ts +39 -30
- package/src/shared/__tests__/artifact-storage-extended.test.ts +9 -9
- package/src/shared/__tests__/artifact-storage.test.ts +33 -30
- package/src/shared/__tests__/extract-structured-output.test.ts +2 -2
- package/src/shared/artifact-storage.ts +12 -17
- package/src/shared/checkpointer/__tests__/factory.test.ts +23 -3
- package/src/shared/checkpointer/__tests__/http-saver.test.ts +25 -2
- package/src/shared/checkpointer/factory.ts +5 -1
- package/src/shared/checkpointer/http-saver.ts +20 -6
- package/src/shared/checkpointer/types.ts +12 -1
- package/src/shared/extract-structured-output.ts +1 -1
- package/src/shared/filereview/__tests__/cas-substrate.test.ts +1 -1
- package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +1 -1
- package/src/workflow-engine/__tests__/tasks/emit-event.test.ts +32 -54
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HttpCheckpointSaver: the proxy wire shape ($binary transport, the routes,
|
|
3
|
+
* the header), the bounded retry loop and its classification, and the one
|
|
4
|
+
* thing that must never regress silently — the bearer credential is read
|
|
5
|
+
* from the shared ref on every request, so a rotation between two requests
|
|
6
|
+
* of one saver reaches the second (a pool member claimed for a session; a
|
|
7
|
+
* sandbox token renewed in place).
|
|
8
|
+
*/
|
|
9
|
+
|
|
1
10
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
11
|
import { HttpCheckpointSaver } from "../http-saver.js";
|
|
12
|
+
import type { TokenRef } from "../../../config.js";
|
|
3
13
|
import type { Checkpoint, CheckpointMetadata } from "@langchain/langgraph-checkpoint";
|
|
4
14
|
|
|
5
15
|
const PROXY = "https://proxy.test";
|
|
6
|
-
const TOKEN = "test-token";
|
|
16
|
+
const TOKEN: TokenRef = { current: "test-token" };
|
|
7
17
|
|
|
8
18
|
function makeConfig(overrides: Record<string, string> = {}) {
|
|
9
19
|
return {
|
|
@@ -71,6 +81,19 @@ describe("HttpCheckpointSaver", () => {
|
|
|
71
81
|
expect(opts.headers.Authorization).toBe("Bearer test-token");
|
|
72
82
|
expect(url).toContain("/v1/proxy/checkpoints/checkpoint");
|
|
73
83
|
});
|
|
84
|
+
|
|
85
|
+
it("presents the credential the ref holds NOW: a rotation between two requests reaches the second", async () => {
|
|
86
|
+
const ref: TokenRef = { current: "boot-credential" };
|
|
87
|
+
const rotating = new HttpCheckpointSaver(PROXY, ref);
|
|
88
|
+
fetchSpy.mockResolvedValue({ status: 404, ok: false });
|
|
89
|
+
|
|
90
|
+
await rotating.getTuple(makeConfig());
|
|
91
|
+
ref.current = "session-credential";
|
|
92
|
+
await rotating.getTuple(makeConfig());
|
|
93
|
+
|
|
94
|
+
expect(fetchSpy.mock.calls[0][1].headers.Authorization).toBe("Bearer boot-credential");
|
|
95
|
+
expect(fetchSpy.mock.calls[1][1].headers.Authorization).toBe("Bearer session-credential");
|
|
96
|
+
});
|
|
74
97
|
});
|
|
75
98
|
|
|
76
99
|
describe("put", () => {
|
|
@@ -360,7 +383,7 @@ describe("HttpCheckpointSaver", () => {
|
|
|
360
383
|
|
|
361
384
|
describe("URL construction", () => {
|
|
362
385
|
it("strips trailing slashes from proxy endpoint", () => {
|
|
363
|
-
const s = new HttpCheckpointSaver("https://proxy.test///", "tok");
|
|
386
|
+
const s = new HttpCheckpointSaver("https://proxy.test///", { current: "tok" });
|
|
364
387
|
fetchSpy.mockResolvedValue({ status: 404, ok: false });
|
|
365
388
|
s.getTuple(makeConfig());
|
|
366
389
|
const [url] = fetchSpy.mock.calls[0];
|
|
@@ -68,7 +68,11 @@ export async function createCheckpointer(
|
|
|
68
68
|
"proxyEndpoint is required for HTTP checkpointer",
|
|
69
69
|
);
|
|
70
70
|
}
|
|
71
|
-
|
|
71
|
+
// The ref must be present AND hold a credential at open: a saver that
|
|
72
|
+
// opened without one would fail its first request with a bare 401
|
|
73
|
+
// instead of this named error, and a proxy-mode runner always has one
|
|
74
|
+
// (loadConfig refuses to boot without STIGMER_TOKEN).
|
|
75
|
+
if (!config.authToken?.current) {
|
|
72
76
|
throw new CheckpointerCreationError(
|
|
73
77
|
"http",
|
|
74
78
|
"authToken is required for HTTP checkpointer",
|
|
@@ -26,6 +26,14 @@
|
|
|
26
26
|
* upserts. Budget exhaustion still fails loudly — checkpoints are not
|
|
27
27
|
* lossy-tolerable (unlike status updates, see status.ts), and silently
|
|
28
28
|
* dropping one would corrupt resume state.
|
|
29
|
+
*
|
|
30
|
+
* The bearer credential is a shared ref read on EVERY request, never a
|
|
31
|
+
* header frozen at construction: a saver lives for a whole turn, and the
|
|
32
|
+
* runner's control-plane credential rotates underneath it (a pool member
|
|
33
|
+
* claimed for a session, a sandbox token renewed in place). A frozen header
|
|
34
|
+
* kept presenting the boot credential and every read after the rotation
|
|
35
|
+
* was refused (config.ts header; ProxyArtifactStorage holds its credential
|
|
36
|
+
* the same way).
|
|
29
37
|
*/
|
|
30
38
|
|
|
31
39
|
import type { RunnableConfig } from "@langchain/core/runnables";
|
|
@@ -38,6 +46,7 @@ import {
|
|
|
38
46
|
type ChannelVersions,
|
|
39
47
|
} from "@langchain/langgraph-checkpoint";
|
|
40
48
|
import type { CheckpointMetadata, PendingWrite } from "@langchain/langgraph-checkpoint";
|
|
49
|
+
import type { TokenRef } from "../../config.js";
|
|
41
50
|
import type { RetryOptions } from "../grpc-retry.js";
|
|
42
51
|
import { fetchWithRetry, type FetchRetryPolicy } from "../http-retry.js";
|
|
43
52
|
|
|
@@ -119,20 +128,17 @@ const DEFAULT_RETRY = {
|
|
|
119
128
|
|
|
120
129
|
export class HttpCheckpointSaver extends BaseCheckpointSaver {
|
|
121
130
|
private readonly baseUrl: string;
|
|
122
|
-
private readonly
|
|
131
|
+
private readonly authToken: Readonly<TokenRef>;
|
|
123
132
|
private readonly retryPolicy: FetchRetryPolicy;
|
|
124
133
|
|
|
125
134
|
constructor(
|
|
126
135
|
proxyEndpoint: string,
|
|
127
|
-
authToken:
|
|
136
|
+
authToken: Readonly<TokenRef>,
|
|
128
137
|
options: HttpCheckpointSaverOptions = {},
|
|
129
138
|
) {
|
|
130
139
|
super();
|
|
131
140
|
this.baseUrl = `${proxyEndpoint.replace(/\/+$/, "")}/v1/proxy/checkpoints`;
|
|
132
|
-
this.
|
|
133
|
-
Authorization: `Bearer ${authToken}`,
|
|
134
|
-
"Content-Type": "application/json",
|
|
135
|
-
};
|
|
141
|
+
this.authToken = authToken;
|
|
136
142
|
this.retryPolicy = {
|
|
137
143
|
label: "HttpCheckpointSaver",
|
|
138
144
|
baseDelayMs: options.baseDelayMs ?? DEFAULT_RETRY.baseDelayMs,
|
|
@@ -143,6 +149,14 @@ export class HttpCheckpointSaver extends BaseCheckpointSaver {
|
|
|
143
149
|
};
|
|
144
150
|
}
|
|
145
151
|
|
|
152
|
+
/** The request headers as of now: the credential is resolved here, per request (the header). */
|
|
153
|
+
private get headers(): Record<string, string> {
|
|
154
|
+
return {
|
|
155
|
+
Authorization: `Bearer ${this.authToken.current ?? ""}`,
|
|
156
|
+
"Content-Type": "application/json",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
146
160
|
private fetchWithRetry(url: string, init?: RequestInit): Promise<Response> {
|
|
147
161
|
return fetchWithRetry(url, init, this.retryPolicy);
|
|
148
162
|
}
|
|
@@ -6,11 +6,22 @@
|
|
|
6
6
|
* ExecuteDeepAgent invocations so HITL/pause/transient-recovery truly resume.
|
|
7
7
|
* - http: routes through the Stigmer Side-Channel Proxy (cloud / managed).
|
|
8
8
|
* - memory: ephemeral in-process storage — explicit opt-in, used by tests.
|
|
9
|
+
*
|
|
10
|
+
* The http backend's credential is a shared ref, never a string: the saver
|
|
11
|
+
* reads it per request, so a rotation of the runner's control-plane
|
|
12
|
+
* credential (a pool claim, a sandbox-token renewal) reaches a checkpoint
|
|
13
|
+
* written after it — including mid-turn, since a saver lives for a whole
|
|
14
|
+
* turn (config.ts header; the artifact store holds its credential the same
|
|
15
|
+
* way).
|
|
9
16
|
*/
|
|
17
|
+
|
|
18
|
+
import type { TokenRef } from "../../config.js";
|
|
19
|
+
|
|
10
20
|
export interface CheckpointerConfig {
|
|
11
21
|
readonly type: "memory" | "http" | "sqlite";
|
|
12
22
|
readonly proxyEndpoint?: string;
|
|
13
|
-
|
|
23
|
+
/** The credential the http saver presents, read per request. Required when type is "http". */
|
|
24
|
+
readonly authToken?: Readonly<TokenRef>;
|
|
14
25
|
/** Absolute path to the SQLite database file. Required when type is "sqlite". */
|
|
15
26
|
readonly sqlitePath?: string;
|
|
16
27
|
}
|
|
@@ -60,7 +60,7 @@ export async function extractStructuredOutput(
|
|
|
60
60
|
const { model: llm } = await buildChatModel({
|
|
61
61
|
modelName: extractionModel,
|
|
62
62
|
proxyEndpoint,
|
|
63
|
-
stigmerToken: config.
|
|
63
|
+
stigmerToken: config.stigmerTokenRef.current ?? undefined,
|
|
64
64
|
maxTokens: 4096,
|
|
65
65
|
});
|
|
66
66
|
|
|
@@ -412,7 +412,7 @@ describe("casBlobReader — ProxyArtifactStorage presigned+fetch path (cloud)",
|
|
|
412
412
|
});
|
|
413
413
|
vi.stubGlobal("fetch", fetchMock);
|
|
414
414
|
try {
|
|
415
|
-
const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
|
|
415
|
+
const storage = new ProxyArtifactStorage("https://proxy.example.com", { current: "tok" });
|
|
416
416
|
const reader = casBlobReader(storage);
|
|
417
417
|
const got = await reader(key);
|
|
418
418
|
expect(got.equals(payload)).toBe(true);
|
|
@@ -186,7 +186,7 @@ describe("proxy-mode file-review reconcile (real ProxyArtifactStorage)", () => {
|
|
|
186
186
|
|
|
187
187
|
beforeEach(async () => {
|
|
188
188
|
proxy = await startMockProxy();
|
|
189
|
-
storage = new ProxyArtifactStorage(proxy.endpoint, "tok");
|
|
189
|
+
storage = new ProxyArtifactStorage(proxy.endpoint, { current: "tok" });
|
|
190
190
|
});
|
|
191
191
|
afterEach(async () => {
|
|
192
192
|
await proxy.close();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
|
|
2
2
|
import { emitEventAction, type EmitEventConfig, type SignalDeliveryTarget } from "../../../activities/emit-event.js";
|
|
3
|
+
import type { StigmerClient } from "../../../client/stigmer-client.js";
|
|
3
4
|
|
|
4
5
|
// ─── Server-mediated signal-delivery mocks ───────────────────────────────────
|
|
5
6
|
// deliverSignal routes through StigmerClient.sendWorkflowSignal (the server's
|
|
@@ -12,25 +13,9 @@ const mockSendWorkflowSignal = vi.fn(async (
|
|
|
12
13
|
_payload: unknown,
|
|
13
14
|
_options?: { timeoutMs?: number },
|
|
14
15
|
) => ({}));
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
vi.mock("../../../client/stigmer-client.js", () => ({
|
|
20
|
-
StigmerClient: vi.fn().mockImplementation(
|
|
21
|
-
(opts: { endpoint: string; token?: string | null }) => mockClientCtor(opts),
|
|
22
|
-
),
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
|
-
// Mutable so each test can vary what config.ts resolves. deliverSignal builds
|
|
26
|
-
// its client from the canonical stigmerBackendEndpoint resolution.
|
|
27
|
-
let mockBackendEndpoint = "http://localhost:7234";
|
|
28
|
-
vi.mock("../../../config.js", () => ({
|
|
29
|
-
loadConfig: () => ({
|
|
30
|
-
stigmerBackendEndpoint: mockBackendEndpoint,
|
|
31
|
-
stigmerToken: null,
|
|
32
|
-
}),
|
|
33
|
-
}));
|
|
16
|
+
// The client is the caller's (call-function.ts builds it from the runner's
|
|
17
|
+
// Config); this module constructs none, so the test hands in a fake.
|
|
18
|
+
const client = { sendWorkflowSignal: mockSendWorkflowSignal } as unknown as StigmerClient;
|
|
34
19
|
|
|
35
20
|
describe("emitEventAction", () => {
|
|
36
21
|
describe("envelope construction", () => {
|
|
@@ -39,7 +24,7 @@ describe("emitEventAction", () => {
|
|
|
39
24
|
event: { type: "workflow.step.completed" },
|
|
40
25
|
};
|
|
41
26
|
|
|
42
|
-
const result = await emitEventAction(config, "exec-123");
|
|
27
|
+
const result = await emitEventAction(config, "exec-123", undefined, client);
|
|
43
28
|
|
|
44
29
|
expect(result.specversion).toBe("1.0");
|
|
45
30
|
expect(result.type).toBe("workflow.step.completed");
|
|
@@ -55,7 +40,7 @@ describe("emitEventAction", () => {
|
|
|
55
40
|
event: { type: "user.created", source: "/services/auth" },
|
|
56
41
|
};
|
|
57
42
|
|
|
58
|
-
const result = await emitEventAction(config, "exec-456");
|
|
43
|
+
const result = await emitEventAction(config, "exec-456", undefined, client);
|
|
59
44
|
|
|
60
45
|
expect(result.source).toBe("/services/auth");
|
|
61
46
|
});
|
|
@@ -65,7 +50,7 @@ describe("emitEventAction", () => {
|
|
|
65
50
|
event: { type: "order.shipped", subject: "order-789" },
|
|
66
51
|
};
|
|
67
52
|
|
|
68
|
-
const result = await emitEventAction(config, "exec-1");
|
|
53
|
+
const result = await emitEventAction(config, "exec-1", undefined, client);
|
|
69
54
|
|
|
70
55
|
expect(result.subject).toBe("order-789");
|
|
71
56
|
});
|
|
@@ -78,7 +63,7 @@ describe("emitEventAction", () => {
|
|
|
78
63
|
},
|
|
79
64
|
};
|
|
80
65
|
|
|
81
|
-
const result = await emitEventAction(config, "exec-1");
|
|
66
|
+
const result = await emitEventAction(config, "exec-1", undefined, client);
|
|
82
67
|
|
|
83
68
|
expect(result.data).toEqual({ build_id: "b-123", status: "success", duration_ms: 4500 });
|
|
84
69
|
});
|
|
@@ -88,7 +73,7 @@ describe("emitEventAction", () => {
|
|
|
88
73
|
event: { type: "ping", data: {} },
|
|
89
74
|
};
|
|
90
75
|
|
|
91
|
-
const result = await emitEventAction(config, "exec-1");
|
|
76
|
+
const result = await emitEventAction(config, "exec-1", undefined, client);
|
|
92
77
|
|
|
93
78
|
expect(result.data).toBeUndefined();
|
|
94
79
|
});
|
|
@@ -98,31 +83,31 @@ describe("emitEventAction", () => {
|
|
|
98
83
|
event: { type: "test" },
|
|
99
84
|
};
|
|
100
85
|
|
|
101
|
-
const result = await emitEventAction(config, "exec-1");
|
|
86
|
+
const result = await emitEventAction(config, "exec-1", undefined, client);
|
|
102
87
|
|
|
103
88
|
expect("subject" in result).toBe(false);
|
|
104
89
|
});
|
|
105
90
|
|
|
106
91
|
it("throws when event field is missing", async () => {
|
|
107
92
|
const config = {} as EmitEventConfig;
|
|
108
|
-
await expect(emitEventAction(config, "exec-1")).rejects.toThrow("'event' field is required");
|
|
93
|
+
await expect(emitEventAction(config, "exec-1", undefined, client)).rejects.toThrow("'event' field is required");
|
|
109
94
|
});
|
|
110
95
|
|
|
111
96
|
it("throws when event.type is missing", async () => {
|
|
112
97
|
const config = { event: {} } as unknown as EmitEventConfig;
|
|
113
|
-
await expect(emitEventAction(config, "exec-1")).rejects.toThrow("'event.type' field is required");
|
|
98
|
+
await expect(emitEventAction(config, "exec-1", undefined, client)).rejects.toThrow("'event.type' field is required");
|
|
114
99
|
});
|
|
115
100
|
|
|
116
101
|
it("generates unique IDs for each invocation", async () => {
|
|
117
102
|
const config: EmitEventConfig = { event: { type: "test" } };
|
|
118
|
-
const r1 = await emitEventAction(config, "e1");
|
|
119
|
-
const r2 = await emitEventAction(config, "e1");
|
|
103
|
+
const r1 = await emitEventAction(config, "e1", undefined, client);
|
|
104
|
+
const r2 = await emitEventAction(config, "e1", undefined, client);
|
|
120
105
|
expect(r1.id).not.toBe(r2.id);
|
|
121
106
|
});
|
|
122
107
|
|
|
123
108
|
it("time field is a valid ISO 8601 string", async () => {
|
|
124
109
|
const config: EmitEventConfig = { event: { type: "test" } };
|
|
125
|
-
const result = await emitEventAction(config, "e1");
|
|
110
|
+
const result = await emitEventAction(config, "e1", undefined, client);
|
|
126
111
|
const parsed = new Date(result.time as string);
|
|
127
112
|
expect(parsed.toISOString()).toBe(result.time);
|
|
128
113
|
});
|
|
@@ -134,7 +119,7 @@ describe("emitEventAction", () => {
|
|
|
134
119
|
event: { type: "test.event" },
|
|
135
120
|
};
|
|
136
121
|
|
|
137
|
-
const result = await emitEventAction(config, "exec-1");
|
|
122
|
+
const result = await emitEventAction(config, "exec-1", undefined, client);
|
|
138
123
|
|
|
139
124
|
expect(result.type).toBe("test.event");
|
|
140
125
|
expect(result.delivery_errors).toBeUndefined();
|
|
@@ -146,7 +131,7 @@ describe("emitEventAction", () => {
|
|
|
146
131
|
delivery: [],
|
|
147
132
|
};
|
|
148
133
|
|
|
149
|
-
const result = await emitEventAction(config, "exec-1");
|
|
134
|
+
const result = await emitEventAction(config, "exec-1", undefined, client);
|
|
150
135
|
|
|
151
136
|
expect(result.type).toBe("test.event");
|
|
152
137
|
expect(result.delivery_errors).toBeUndefined();
|
|
@@ -171,7 +156,7 @@ describe("emitEventAction", () => {
|
|
|
171
156
|
delivery: [{ webhook: { url: "https://events.example.com/ingest" } }],
|
|
172
157
|
};
|
|
173
158
|
|
|
174
|
-
const result = await emitEventAction(config, "exec-1");
|
|
159
|
+
const result = await emitEventAction(config, "exec-1", undefined, client);
|
|
175
160
|
|
|
176
161
|
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
177
162
|
const call = fetchSpy.mock.calls[0] as unknown as [string, RequestInit];
|
|
@@ -202,7 +187,7 @@ describe("emitEventAction", () => {
|
|
|
202
187
|
}],
|
|
203
188
|
};
|
|
204
189
|
|
|
205
|
-
await emitEventAction(config, "exec-1", { API_TOKEN: "secret123" });
|
|
190
|
+
await emitEventAction(config, "exec-1", { API_TOKEN: "secret123" }, client);
|
|
206
191
|
|
|
207
192
|
const call = fetchSpy.mock.calls[0] as unknown as [string, RequestInit];
|
|
208
193
|
expect((call[1].headers as Record<string, string>)["Authorization"]).toBe("Bearer secret123");
|
|
@@ -218,7 +203,7 @@ describe("emitEventAction", () => {
|
|
|
218
203
|
delivery: [{ webhook: { url: "https://down.example.com" } }],
|
|
219
204
|
};
|
|
220
205
|
|
|
221
|
-
const result = await emitEventAction(config, "exec-1");
|
|
206
|
+
const result = await emitEventAction(config, "exec-1", undefined, client);
|
|
222
207
|
|
|
223
208
|
expect(result.delivery_errors).toHaveLength(1);
|
|
224
209
|
expect((result.delivery_errors as any)[0].target).toBe("webhook:https://down.example.com");
|
|
@@ -235,7 +220,7 @@ describe("emitEventAction", () => {
|
|
|
235
220
|
delivery: [{ webhook: { url: "https://unreachable.example.com" } }],
|
|
236
221
|
};
|
|
237
222
|
|
|
238
|
-
const result = await emitEventAction(config, "exec-1");
|
|
223
|
+
const result = await emitEventAction(config, "exec-1", undefined, client);
|
|
239
224
|
|
|
240
225
|
expect(result.delivery_errors).toHaveLength(1);
|
|
241
226
|
expect((result.delivery_errors as any)[0].error).toContain("ECONNREFUSED");
|
|
@@ -255,7 +240,7 @@ describe("emitEventAction", () => {
|
|
|
255
240
|
],
|
|
256
241
|
};
|
|
257
242
|
|
|
258
|
-
const result = await emitEventAction(config, "exec-1");
|
|
243
|
+
const result = await emitEventAction(config, "exec-1", undefined, client);
|
|
259
244
|
|
|
260
245
|
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
261
246
|
expect(result.delivery_errors).toBeUndefined();
|
|
@@ -265,13 +250,10 @@ describe("emitEventAction", () => {
|
|
|
265
250
|
describe("signal delivery (server-mediated SendSignal lane)", () => {
|
|
266
251
|
beforeEach(() => {
|
|
267
252
|
mockSendWorkflowSignal.mockClear();
|
|
268
|
-
mockClientCtor.mockClear();
|
|
269
253
|
mockSendWorkflowSignal.mockResolvedValue({});
|
|
270
|
-
mockBackendEndpoint = "http://localhost:7234";
|
|
271
254
|
});
|
|
272
255
|
|
|
273
|
-
it("routes through the
|
|
274
|
-
mockBackendEndpoint = "https://api.stigmer.example.com";
|
|
256
|
+
it("routes through the client it is handed, addressed by execution_id", async () => {
|
|
275
257
|
|
|
276
258
|
const config: EmitEventConfig = {
|
|
277
259
|
event: { type: "approval.granted" },
|
|
@@ -280,11 +262,8 @@ describe("emitEventAction", () => {
|
|
|
280
262
|
],
|
|
281
263
|
};
|
|
282
264
|
|
|
283
|
-
const result = await emitEventAction(config, "exec-1");
|
|
265
|
+
const result = await emitEventAction(config, "exec-1", undefined, client);
|
|
284
266
|
|
|
285
|
-
expect(mockClientCtor).toHaveBeenCalledWith(
|
|
286
|
-
expect.objectContaining({ endpoint: "https://api.stigmer.example.com" }),
|
|
287
|
-
);
|
|
288
267
|
expect(mockSendWorkflowSignal).toHaveBeenCalledTimes(1);
|
|
289
268
|
const [executionId, signalName, envelope, options] =
|
|
290
269
|
mockSendWorkflowSignal.mock.calls[0] as unknown as [
|
|
@@ -314,7 +293,7 @@ describe("emitEventAction", () => {
|
|
|
314
293
|
],
|
|
315
294
|
};
|
|
316
295
|
|
|
317
|
-
const result = await emitEventAction(config, "exec-1");
|
|
296
|
+
const result = await emitEventAction(config, "exec-1", undefined, client);
|
|
318
297
|
|
|
319
298
|
expect(result.delivery_errors).toHaveLength(1);
|
|
320
299
|
expect((result.delivery_errors as any)[0].target).toBe("signal:wfx_done/ping");
|
|
@@ -334,7 +313,7 @@ describe("emitEventAction", () => {
|
|
|
334
313
|
],
|
|
335
314
|
};
|
|
336
315
|
|
|
337
|
-
const result = await emitEventAction(config, "exec-1");
|
|
316
|
+
const result = await emitEventAction(config, "exec-1", undefined, client);
|
|
338
317
|
|
|
339
318
|
expect(mockSendWorkflowSignal).not.toHaveBeenCalled();
|
|
340
319
|
expect(result.delivery_errors).toHaveLength(1);
|
|
@@ -354,14 +333,14 @@ describe("emitEventAction", () => {
|
|
|
354
333
|
],
|
|
355
334
|
};
|
|
356
335
|
|
|
357
|
-
const result = await emitEventAction(config, "exec-1");
|
|
336
|
+
const result = await emitEventAction(config, "exec-1", undefined, client);
|
|
358
337
|
|
|
359
338
|
expect(mockSendWorkflowSignal).not.toHaveBeenCalled();
|
|
360
339
|
expect(result.delivery_errors).toHaveLength(1);
|
|
361
340
|
expect((result.delivery_errors as any)[0].error).toContain("'signal_name'");
|
|
362
341
|
});
|
|
363
342
|
|
|
364
|
-
it("
|
|
343
|
+
it("sends every signal target through the one client", async () => {
|
|
365
344
|
const config: EmitEventConfig = {
|
|
366
345
|
event: { type: "fanout" },
|
|
367
346
|
delivery: [
|
|
@@ -370,14 +349,13 @@ describe("emitEventAction", () => {
|
|
|
370
349
|
],
|
|
371
350
|
};
|
|
372
351
|
|
|
373
|
-
const result = await emitEventAction(config, "exec-1");
|
|
352
|
+
const result = await emitEventAction(config, "exec-1", undefined, client);
|
|
374
353
|
|
|
375
|
-
expect(mockClientCtor).toHaveBeenCalledTimes(1);
|
|
376
354
|
expect(mockSendWorkflowSignal).toHaveBeenCalledTimes(2);
|
|
377
355
|
expect(result.delivery_errors).toBeUndefined();
|
|
378
356
|
});
|
|
379
357
|
|
|
380
|
-
it("
|
|
358
|
+
it("never touches the client for webhook-only delivery", async () => {
|
|
381
359
|
const originalFetch = globalThis.fetch;
|
|
382
360
|
globalThis.fetch = vi.fn(() =>
|
|
383
361
|
Promise.resolve(new Response(null, { status: 200 })),
|
|
@@ -389,9 +367,9 @@ describe("emitEventAction", () => {
|
|
|
389
367
|
delivery: [{ webhook: { url: "https://events.example.com" } }],
|
|
390
368
|
};
|
|
391
369
|
|
|
392
|
-
await emitEventAction(config, "exec-1");
|
|
370
|
+
await emitEventAction(config, "exec-1", undefined, client);
|
|
393
371
|
|
|
394
|
-
expect(
|
|
372
|
+
expect(mockSendWorkflowSignal).not.toHaveBeenCalled();
|
|
395
373
|
} finally {
|
|
396
374
|
globalThis.fetch = originalFetch;
|
|
397
375
|
}
|