@stigmer/server 3.36.0 → 3.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/domain/agentexecution/artifacts.d.ts.map +1 -1
- package/dist/domain/agentexecution/artifacts.js +15 -6
- package/dist/domain/agentexecution/artifacts.js.map +1 -1
- package/dist/domain/agentexecution/constants.d.ts +0 -10
- package/dist/domain/agentexecution/constants.d.ts.map +1 -1
- package/dist/domain/agentexecution/constants.js +0 -12
- package/dist/domain/agentexecution/constants.js.map +1 -1
- package/dist/domain/agentexecution/lifecycle.d.ts.map +1 -1
- package/dist/domain/agentexecution/lifecycle.js +5 -3
- package/dist/domain/agentexecution/lifecycle.js.map +1 -1
- package/dist/domain/agentexecution/platform-rows.d.ts +63 -0
- package/dist/domain/agentexecution/platform-rows.d.ts.map +1 -0
- package/dist/domain/agentexecution/platform-rows.js +106 -0
- package/dist/domain/agentexecution/platform-rows.js.map +1 -0
- package/dist/domain/agentexecution/subscribe.d.ts.map +1 -1
- package/dist/domain/agentexecution/subscribe.js +7 -4
- package/dist/domain/agentexecution/subscribe.js.map +1 -1
- package/dist/domain/agentexecution/update-status.d.ts.map +1 -1
- package/dist/domain/agentexecution/update-status.js +48 -17
- package/dist/domain/agentexecution/update-status.js.map +1 -1
- package/dist/domain/agentexecution/usage.d.ts +0 -2
- package/dist/domain/agentexecution/usage.d.ts.map +1 -1
- package/dist/domain/agentexecution/usage.js +12 -10
- package/dist/domain/agentexecution/usage.js.map +1 -1
- package/dist/domain/organization/controller.d.ts +4 -1
- package/dist/domain/organization/controller.d.ts.map +1 -1
- package/dist/domain/organization/controller.js +30 -8
- package/dist/domain/organization/controller.js.map +1 -1
- package/dist/domain/workflow/controller.js +2 -1
- package/dist/domain/workflow/controller.js.map +1 -1
- package/dist/domain/workflow/steps.d.ts +7 -4
- package/dist/domain/workflow/steps.d.ts.map +1 -1
- package/dist/domain/workflow/steps.js +29 -17
- package/dist/domain/workflow/steps.js.map +1 -1
- package/dist/domain/workflowexecution/create-steps.d.ts.map +1 -1
- package/dist/domain/workflowexecution/create-steps.js +6 -2
- package/dist/domain/workflowexecution/create-steps.js.map +1 -1
- package/dist/domain/workflowexecution/lifecycle.d.ts.map +1 -1
- package/dist/domain/workflowexecution/lifecycle.js +5 -2
- package/dist/domain/workflowexecution/lifecycle.js.map +1 -1
- package/dist/domain/workflowexecution/send-signal.d.ts.map +1 -1
- package/dist/domain/workflowexecution/send-signal.js +6 -3
- package/dist/domain/workflowexecution/send-signal.js.map +1 -1
- package/dist/domain/workflowexecution/submit-approval.d.ts.map +1 -1
- package/dist/domain/workflowexecution/submit-approval.js +6 -2
- package/dist/domain/workflowexecution/submit-approval.js.map +1 -1
- package/dist/domain/workflowexecution/submit-file-decision.d.ts.map +1 -1
- package/dist/domain/workflowexecution/submit-file-decision.js +6 -2
- package/dist/domain/workflowexecution/submit-file-decision.js.map +1 -1
- package/dist/domain/workflowexecution/submit-workflow-task-approval.d.ts.map +1 -1
- package/dist/domain/workflowexecution/submit-workflow-task-approval.js +6 -2
- package/dist/domain/workflowexecution/submit-workflow-task-approval.js.map +1 -1
- package/dist/domain/workflowexecution/subscribe-events.d.ts.map +1 -1
- package/dist/domain/workflowexecution/subscribe-events.js +6 -2
- package/dist/domain/workflowexecution/subscribe-events.js.map +1 -1
- package/dist/domain/workflowexecution/subscribe.d.ts.map +1 -1
- package/dist/domain/workflowexecution/subscribe.js +7 -4
- package/dist/domain/workflowexecution/subscribe.js.map +1 -1
- package/dist/extensions/organization-directory.d.ts +29 -6
- package/dist/extensions/organization-directory.d.ts.map +1 -1
- package/dist/extensions/organization-directory.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.d.ts +9 -4
- package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.js +10 -12
- package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
- package/dist/temporal/agentexecution/runner-result.d.ts +12 -0
- package/dist/temporal/agentexecution/runner-result.d.ts.map +1 -1
- package/dist/temporal/agentexecution/runner-result.js +15 -0
- package/dist/temporal/agentexecution/runner-result.js.map +1 -1
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.d.ts.map +1 -1
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js +44 -22
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js.map +1 -1
- package/package.json +6 -6
- package/src/domain/agentexecution/__tests__/agentexecution.test.ts +2 -8
- package/src/domain/agentexecution/__tests__/store-faults.test.ts +315 -0
- package/src/domain/agentexecution/__tests__/update-status.test.ts +217 -0
- package/src/domain/agentexecution/artifacts.ts +21 -12
- package/src/domain/agentexecution/constants.ts +0 -15
- package/src/domain/agentexecution/lifecycle.ts +5 -3
- package/src/domain/agentexecution/platform-rows.ts +133 -0
- package/src/domain/agentexecution/subscribe.ts +7 -3
- package/src/domain/agentexecution/update-status.ts +70 -19
- package/src/domain/agentexecution/usage.ts +16 -13
- package/src/domain/organization/controller.ts +47 -10
- package/src/domain/workflow/__tests__/version-audit-degradation.test.ts +192 -0
- package/src/domain/workflow/controller.ts +2 -1
- package/src/domain/workflow/steps.ts +30 -18
- package/src/domain/workflowexecution/__tests__/store-faults.test.ts +418 -0
- package/src/domain/workflowexecution/create-steps.ts +6 -2
- package/src/domain/workflowexecution/lifecycle.ts +5 -2
- package/src/domain/workflowexecution/send-signal.ts +11 -4
- package/src/domain/workflowexecution/submit-approval.ts +6 -2
- package/src/domain/workflowexecution/submit-file-decision.ts +6 -2
- package/src/domain/workflowexecution/submit-workflow-task-approval.ts +6 -2
- package/src/domain/workflowexecution/subscribe-events.ts +6 -2
- package/src/domain/workflowexecution/subscribe.ts +11 -4
- package/src/extensions/__tests__/extension-composition.test.ts +90 -18
- package/src/extensions/organization-directory.ts +33 -7
- package/src/index.ts +5 -1
- package/src/pipeline/__tests__/blind-not-found.test.ts +297 -0
- package/src/pipeline/steps/__tests__/guard-memory-capture.test.ts +67 -5
- package/src/pipeline/steps/guard-memory-capture.ts +24 -19
- package/src/temporal/agentexecution/__tests__/invoke-workflow.test.ts +51 -1
- package/src/temporal/agentexecution/__tests__/own-behalf-status-writes.test.ts +149 -9
- package/src/temporal/agentexecution/__tests__/replay-histories/runner-failed.json +757 -0
- package/src/temporal/agentexecution/__tests__/replay-histories/user-cancel.json +755 -0
- package/src/temporal/agentexecution/__tests__/replay.test.ts +4 -2
- package/src/temporal/agentexecution/runner-result.ts +16 -0
- package/src/temporal/agentexecution/workflows/invoke-agent-execution.ts +49 -22
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the store-fault contract every workflow-execution load shares: a
|
|
3
|
+
* typed ResourceNotFoundError answers NotFound with the surface's own pinned
|
|
4
|
+
* copy, and any other store failure is an infrastructure fault answered as a
|
|
5
|
+
* sanitized Internal, never a NotFound that tells a client the run (or its
|
|
6
|
+
* workflow) does not exist (stigmer/stigmer#1345).
|
|
7
|
+
*
|
|
8
|
+
* The surfaces: the five lifecycle verbs (their shared LoadExecutionById
|
|
9
|
+
* step), sendSignal, the two approval-forwarding verbs, the task-approval
|
|
10
|
+
* signal, subscribe's snapshot read, subscribeEvents' existence check, and
|
|
11
|
+
* create's CreateDefaultInstanceIfNeeded workflow load. The composed suites
|
|
12
|
+
* reach only a real store, which cannot fail selectively, so each surface
|
|
13
|
+
* runs here against a store whose one read throws. Every other dependency is
|
|
14
|
+
* untouchable: a load that fails must stop the call before any of them.
|
|
15
|
+
*
|
|
16
|
+
* Out of scope: the NotFound copy itself (wire contract, pinned where each
|
|
17
|
+
* surface is otherwise tested and by the conformance suites) and loads
|
|
18
|
+
* outside this domain.
|
|
19
|
+
*/
|
|
20
|
+
import { create } from "@bufbuild/protobuf";
|
|
21
|
+
import type { HandlerContext } from "@connectrpc/connect";
|
|
22
|
+
import { Code, ConnectError, createContextValues } from "@connectrpc/connect";
|
|
23
|
+
import { describe, expect, it } from "vitest";
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
ApprovalAction,
|
|
27
|
+
FileDecisionAction,
|
|
28
|
+
FileDecisionScope,
|
|
29
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
30
|
+
import { WorkflowExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/api_pb";
|
|
31
|
+
import {
|
|
32
|
+
CancelWorkflowExecutionInputSchema,
|
|
33
|
+
PauseWorkflowExecutionInputSchema,
|
|
34
|
+
RecoverWorkflowExecutionInputSchema,
|
|
35
|
+
ResumeWorkflowExecutionInputSchema,
|
|
36
|
+
SendSignalInputSchema,
|
|
37
|
+
SubmitWorkflowApprovalInputSchema,
|
|
38
|
+
SubmitWorkflowFileDecisionInputSchema,
|
|
39
|
+
SubmitWorkflowTaskApprovalInputSchema,
|
|
40
|
+
SubscribeEventsRequestSchema,
|
|
41
|
+
SubscribeWorkflowExecutionRequestSchema,
|
|
42
|
+
TerminateWorkflowExecutionInputSchema,
|
|
43
|
+
} from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/io_pb";
|
|
44
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
45
|
+
|
|
46
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
47
|
+
import { callerIdentityKey } from "../../../pipeline/interceptors/auth.js";
|
|
48
|
+
import { testCallerIdentity } from "../../../pipeline/__tests__/support.js";
|
|
49
|
+
import { RequestContext } from "../../../pipeline/request-context.js";
|
|
50
|
+
import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
|
|
51
|
+
import { ResourceNotFoundError } from "../../../store/interface.js";
|
|
52
|
+
import type { Store } from "../../../store/interface.js";
|
|
53
|
+
|
|
54
|
+
import { newCreateDefaultInstanceIfNeededStep } from "../create-steps.js";
|
|
55
|
+
import type { LifecycleDeps } from "../lifecycle.js";
|
|
56
|
+
import {
|
|
57
|
+
cancelExecution,
|
|
58
|
+
pauseExecution,
|
|
59
|
+
recoverExecution,
|
|
60
|
+
resumeExecution,
|
|
61
|
+
terminateExecution,
|
|
62
|
+
} from "../lifecycle.js";
|
|
63
|
+
import { sendSignal } from "../send-signal.js";
|
|
64
|
+
import { StreamBroker } from "../stream-broker.js";
|
|
65
|
+
import { submitApproval } from "../submit-approval.js";
|
|
66
|
+
import { submitFileDecision } from "../submit-file-decision.js";
|
|
67
|
+
import { submitWorkflowTaskApproval } from "../submit-workflow-task-approval.js";
|
|
68
|
+
import { subscribeEvents } from "../subscribe-events.js";
|
|
69
|
+
import { subscribeExecution } from "../subscribe.js";
|
|
70
|
+
|
|
71
|
+
const silentLogger = createLogger({
|
|
72
|
+
level: "error",
|
|
73
|
+
pretty: false,
|
|
74
|
+
write: () => {},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const EXECUTION_ID = "wex_storefault";
|
|
78
|
+
const WORKFLOW_ID = "wfl_storefault";
|
|
79
|
+
|
|
80
|
+
const EXECUTION_FAULT_COPY = "failed to load workflow execution";
|
|
81
|
+
const WORKFLOW_FAULT_COPY = "failed to load workflow";
|
|
82
|
+
|
|
83
|
+
/** A dependency the call must never reach once its load has failed. */
|
|
84
|
+
function untouchable<T extends object>(name: string): T {
|
|
85
|
+
return new Proxy({} as T, {
|
|
86
|
+
get(_target, prop) {
|
|
87
|
+
throw new Error(`${name}.${String(prop)} reached after a failed load`);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A store whose every read fails with the given error. */
|
|
93
|
+
function failingStore(error: Error): Store {
|
|
94
|
+
return {
|
|
95
|
+
getResource: () => Promise.reject(error),
|
|
96
|
+
} as unknown as Store;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const MISSING = (): Error =>
|
|
100
|
+
new ResourceNotFoundError(`workflow_execution/${EXECUTION_ID}`);
|
|
101
|
+
const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
|
|
102
|
+
|
|
103
|
+
async function errorOf(call: () => Promise<unknown>): Promise<ConnectError> {
|
|
104
|
+
const error = await call().then(
|
|
105
|
+
() => undefined,
|
|
106
|
+
(e: unknown) => e,
|
|
107
|
+
);
|
|
108
|
+
expect(error, "the call must fail").toBeInstanceOf(ConnectError);
|
|
109
|
+
return error as ConnectError;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function handlerContext(): HandlerContext {
|
|
113
|
+
const values = createContextValues();
|
|
114
|
+
values.set(callerIdentityKey, testCallerIdentity());
|
|
115
|
+
return { signal: new AbortController().signal, values } as HandlerContext;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function lifecycleDeps(store: Store): LifecycleDeps {
|
|
119
|
+
return {
|
|
120
|
+
store,
|
|
121
|
+
logger: silentLogger,
|
|
122
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
123
|
+
broker: untouchable("broker"),
|
|
124
|
+
engineState: untouchable("engineState"),
|
|
125
|
+
executionContextBuilder: untouchable("executionContextBuilder"),
|
|
126
|
+
sandboxLane: untouchable("sandboxLane"),
|
|
127
|
+
temporalConfig: untouchable("temporalConfig"),
|
|
128
|
+
sandboxTerminalObserver: untouchable("sandboxTerminalObserver"),
|
|
129
|
+
gateSteps: new Map(),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const LIFECYCLE_VERBS: ReadonlyArray<
|
|
134
|
+
readonly [string, (deps: LifecycleDeps) => Promise<unknown>]
|
|
135
|
+
> = [
|
|
136
|
+
[
|
|
137
|
+
"cancel",
|
|
138
|
+
(deps) =>
|
|
139
|
+
cancelExecution(
|
|
140
|
+
deps,
|
|
141
|
+
create(CancelWorkflowExecutionInputSchema, { id: EXECUTION_ID }),
|
|
142
|
+
testCallerIdentity(),
|
|
143
|
+
),
|
|
144
|
+
],
|
|
145
|
+
[
|
|
146
|
+
"terminate",
|
|
147
|
+
(deps) =>
|
|
148
|
+
terminateExecution(
|
|
149
|
+
deps,
|
|
150
|
+
create(TerminateWorkflowExecutionInputSchema, { id: EXECUTION_ID }),
|
|
151
|
+
testCallerIdentity(),
|
|
152
|
+
),
|
|
153
|
+
],
|
|
154
|
+
[
|
|
155
|
+
"pause",
|
|
156
|
+
(deps) =>
|
|
157
|
+
pauseExecution(
|
|
158
|
+
deps,
|
|
159
|
+
create(PauseWorkflowExecutionInputSchema, { id: EXECUTION_ID }),
|
|
160
|
+
testCallerIdentity(),
|
|
161
|
+
),
|
|
162
|
+
],
|
|
163
|
+
[
|
|
164
|
+
"resume",
|
|
165
|
+
(deps) =>
|
|
166
|
+
resumeExecution(
|
|
167
|
+
deps,
|
|
168
|
+
create(ResumeWorkflowExecutionInputSchema, { id: EXECUTION_ID }),
|
|
169
|
+
testCallerIdentity(),
|
|
170
|
+
),
|
|
171
|
+
],
|
|
172
|
+
[
|
|
173
|
+
"recover",
|
|
174
|
+
(deps) =>
|
|
175
|
+
recoverExecution(
|
|
176
|
+
deps,
|
|
177
|
+
create(RecoverWorkflowExecutionInputSchema, { id: EXECUTION_ID }),
|
|
178
|
+
testCallerIdentity(),
|
|
179
|
+
),
|
|
180
|
+
],
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
describe.each(LIFECYCLE_VERBS)("%s — LoadExecutionById", (_verb, run) => {
|
|
184
|
+
it("a missing execution answers NotFound with the lifecycle copy", async () => {
|
|
185
|
+
const error = await errorOf(() =>
|
|
186
|
+
run(lifecycleDeps(failingStore(MISSING()))),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
expect(error.code).toBe(Code.NotFound);
|
|
190
|
+
expect(error.rawMessage).toBe(
|
|
191
|
+
`workflow_execution not found: ${EXECUTION_ID}`,
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
196
|
+
const error = await errorOf(() =>
|
|
197
|
+
run(lifecycleDeps(failingStore(LOCKED()))),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(error.code).toBe(Code.Internal);
|
|
201
|
+
expect(error.rawMessage).toBe(EXECUTION_FAULT_COPY);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The pipeline verbs with their own load step: each runs Authorize and its
|
|
207
|
+
* input validation (a hand-written step or ValidateProto) first, so the
|
|
208
|
+
* inputs carry every field the validation requires, and the load is the
|
|
209
|
+
* first step that reads the store.
|
|
210
|
+
*/
|
|
211
|
+
const COMMAND_VERBS: ReadonlyArray<
|
|
212
|
+
readonly [string, string, (store: Store) => Promise<unknown>]
|
|
213
|
+
> = [
|
|
214
|
+
[
|
|
215
|
+
"sendSignal — LoadExecutionByExecutionId",
|
|
216
|
+
`workflow_execution not found: ${EXECUTION_ID}`,
|
|
217
|
+
(store) =>
|
|
218
|
+
sendSignal(
|
|
219
|
+
{
|
|
220
|
+
store,
|
|
221
|
+
logger: silentLogger,
|
|
222
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
223
|
+
engineState: untouchable("engineState"),
|
|
224
|
+
},
|
|
225
|
+
create(SendSignalInputSchema, {
|
|
226
|
+
executionId: EXECUTION_ID,
|
|
227
|
+
signalName: "resume",
|
|
228
|
+
}),
|
|
229
|
+
testCallerIdentity(),
|
|
230
|
+
),
|
|
231
|
+
],
|
|
232
|
+
[
|
|
233
|
+
"submitApproval — LoadExisting",
|
|
234
|
+
`workflow_execution not found: ${EXECUTION_ID}`,
|
|
235
|
+
(store) =>
|
|
236
|
+
submitApproval(
|
|
237
|
+
{
|
|
238
|
+
store,
|
|
239
|
+
logger: silentLogger,
|
|
240
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
241
|
+
approvalForwarder: untouchable("approvalForwarder"),
|
|
242
|
+
},
|
|
243
|
+
create(SubmitWorkflowApprovalInputSchema, {
|
|
244
|
+
executionId: EXECUTION_ID,
|
|
245
|
+
toolCallId: "call_storefault",
|
|
246
|
+
action: ApprovalAction.APPROVE,
|
|
247
|
+
}),
|
|
248
|
+
testCallerIdentity(),
|
|
249
|
+
),
|
|
250
|
+
],
|
|
251
|
+
[
|
|
252
|
+
"submitFileDecision — LoadExisting",
|
|
253
|
+
`workflow_execution not found: ${EXECUTION_ID}`,
|
|
254
|
+
(store) =>
|
|
255
|
+
submitFileDecision(
|
|
256
|
+
{
|
|
257
|
+
store,
|
|
258
|
+
logger: silentLogger,
|
|
259
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
260
|
+
fileDecisionForwarder: untouchable("fileDecisionForwarder"),
|
|
261
|
+
},
|
|
262
|
+
create(SubmitWorkflowFileDecisionInputSchema, {
|
|
263
|
+
executionId: EXECUTION_ID,
|
|
264
|
+
childAgentExecutionId: "aex_storefault",
|
|
265
|
+
changeSetId: "chs_storefault",
|
|
266
|
+
scope: FileDecisionScope.CHANGE_SET,
|
|
267
|
+
action: FileDecisionAction.APPROVE,
|
|
268
|
+
expectedDigest: "sha256:storefault",
|
|
269
|
+
}),
|
|
270
|
+
testCallerIdentity(),
|
|
271
|
+
),
|
|
272
|
+
],
|
|
273
|
+
[
|
|
274
|
+
"submitWorkflowTaskApproval — LoadExecutionForApproval",
|
|
275
|
+
`WorkflowExecution not found: ${EXECUTION_ID}`,
|
|
276
|
+
(store) =>
|
|
277
|
+
submitWorkflowTaskApproval(
|
|
278
|
+
{
|
|
279
|
+
store,
|
|
280
|
+
logger: silentLogger,
|
|
281
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
282
|
+
engineState: untouchable("engineState"),
|
|
283
|
+
},
|
|
284
|
+
create(SubmitWorkflowTaskApprovalInputSchema, {
|
|
285
|
+
executionId: EXECUTION_ID,
|
|
286
|
+
taskName: "review",
|
|
287
|
+
outcome: "approved",
|
|
288
|
+
}),
|
|
289
|
+
testCallerIdentity(),
|
|
290
|
+
),
|
|
291
|
+
],
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
describe.each(COMMAND_VERBS)("%s", (_verb, notFoundCopy, run) => {
|
|
295
|
+
it("a missing execution answers NotFound with the verb's copy", async () => {
|
|
296
|
+
const error = await errorOf(() => run(failingStore(MISSING())));
|
|
297
|
+
|
|
298
|
+
expect(error.code).toBe(Code.NotFound);
|
|
299
|
+
expect(error.rawMessage).toBe(notFoundCopy);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
303
|
+
const error = await errorOf(() => run(failingStore(LOCKED())));
|
|
304
|
+
|
|
305
|
+
expect(error.code).toBe(Code.Internal);
|
|
306
|
+
expect(error.rawMessage).toBe(EXECUTION_FAULT_COPY);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe("subscribe — the snapshot read", () => {
|
|
311
|
+
function subscribeError(
|
|
312
|
+
store: Store,
|
|
313
|
+
broker: StreamBroker,
|
|
314
|
+
): Promise<ConnectError> {
|
|
315
|
+
return errorOf(() =>
|
|
316
|
+
subscribeExecution(
|
|
317
|
+
{
|
|
318
|
+
store,
|
|
319
|
+
logger: silentLogger,
|
|
320
|
+
broker,
|
|
321
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
322
|
+
},
|
|
323
|
+
create(SubscribeWorkflowExecutionRequestSchema, {
|
|
324
|
+
executionId: EXECUTION_ID,
|
|
325
|
+
}),
|
|
326
|
+
handlerContext(),
|
|
327
|
+
).next(),
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
it("a missing execution answers NotFound with the subscribe copy and releases the subscription", async () => {
|
|
332
|
+
const broker = new StreamBroker(silentLogger);
|
|
333
|
+
const error = await subscribeError(failingStore(MISSING()), broker);
|
|
334
|
+
|
|
335
|
+
expect(error.code).toBe(Code.NotFound);
|
|
336
|
+
expect(error.rawMessage).toBe(
|
|
337
|
+
`WorkflowExecution not found: ${EXECUTION_ID}`,
|
|
338
|
+
);
|
|
339
|
+
expect(broker.getSubscriberCount(EXECUTION_ID)).toBe(0);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("any other store failure answers a sanitized Internal and releases the subscription", async () => {
|
|
343
|
+
const broker = new StreamBroker(silentLogger);
|
|
344
|
+
const error = await subscribeError(failingStore(LOCKED()), broker);
|
|
345
|
+
|
|
346
|
+
expect(error.code).toBe(Code.Internal);
|
|
347
|
+
expect(error.rawMessage).toBe(EXECUTION_FAULT_COPY);
|
|
348
|
+
expect(broker.getSubscriberCount(EXECUTION_ID)).toBe(0);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe("subscribeEvents — the existence check", () => {
|
|
353
|
+
function subscribeEventsError(store: Store): Promise<ConnectError> {
|
|
354
|
+
return errorOf(() =>
|
|
355
|
+
subscribeEvents(
|
|
356
|
+
{
|
|
357
|
+
store,
|
|
358
|
+
logger: silentLogger,
|
|
359
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
360
|
+
},
|
|
361
|
+
create(SubscribeEventsRequestSchema, { executionId: EXECUTION_ID }),
|
|
362
|
+
handlerContext(),
|
|
363
|
+
).next(),
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
it("a missing execution answers NotFound with the stream's copy", async () => {
|
|
368
|
+
const error = await subscribeEventsError(failingStore(MISSING()));
|
|
369
|
+
|
|
370
|
+
expect(error.code).toBe(Code.NotFound);
|
|
371
|
+
expect(error.rawMessage).toBe(
|
|
372
|
+
`WorkflowExecution not found: ${EXECUTION_ID}`,
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
377
|
+
const error = await subscribeEventsError(failingStore(LOCKED()));
|
|
378
|
+
|
|
379
|
+
expect(error.code).toBe(Code.Internal);
|
|
380
|
+
expect(error.rawMessage).toBe(EXECUTION_FAULT_COPY);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe("create — CreateDefaultInstanceIfNeeded's workflow load", () => {
|
|
385
|
+
async function runStep(store: Store): Promise<void> {
|
|
386
|
+
const ctx = new RequestContext(
|
|
387
|
+
WorkflowExecutionSchema,
|
|
388
|
+
create(WorkflowExecutionSchema, { spec: { workflowId: WORKFLOW_ID } }),
|
|
389
|
+
testCallerIdentity(),
|
|
390
|
+
ApiResourceKind.workflow_execution,
|
|
391
|
+
);
|
|
392
|
+
await newCreateDefaultInstanceIfNeededStep({
|
|
393
|
+
store,
|
|
394
|
+
logger: silentLogger,
|
|
395
|
+
workflowInstanceCreator: () => {
|
|
396
|
+
throw new Error("workflowInstanceCreator reached after a failed load");
|
|
397
|
+
},
|
|
398
|
+
}).execute(ctx);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
it("a missing workflow answers NotFound with the create copy", async () => {
|
|
402
|
+
const error = await errorOf(() =>
|
|
403
|
+
runStep(
|
|
404
|
+
failingStore(new ResourceNotFoundError(`workflow/${WORKFLOW_ID}`)),
|
|
405
|
+
),
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
expect(error.code).toBe(Code.NotFound);
|
|
409
|
+
expect(error.rawMessage).toBe(`Workflow not found: ${WORKFLOW_ID}`);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
413
|
+
const error = await errorOf(() => runStep(failingStore(LOCKED())));
|
|
414
|
+
|
|
415
|
+
expect(error.code).toBe(Code.Internal);
|
|
416
|
+
expect(error.rawMessage).toBe(WORKFLOW_FAULT_COPY);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
notFoundError,
|
|
36
36
|
} from "../../pipeline/errors.js";
|
|
37
37
|
import type { PipelineStep } from "../../pipeline/pipeline.js";
|
|
38
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
38
39
|
import type { Store } from "../../store/interface.js";
|
|
39
40
|
import { fromBinary } from "@bufbuild/protobuf";
|
|
40
41
|
|
|
@@ -127,8 +128,11 @@ export function newCreateDefaultInstanceIfNeededStep(
|
|
|
127
128
|
workflowId,
|
|
128
129
|
WorkflowSchema,
|
|
129
130
|
);
|
|
130
|
-
} catch {
|
|
131
|
-
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error instanceof ResourceNotFoundError) {
|
|
133
|
+
throw notFoundError("Workflow", workflowId);
|
|
134
|
+
}
|
|
135
|
+
throw internalError(error, "failed to load workflow");
|
|
132
136
|
}
|
|
133
137
|
|
|
134
138
|
const defaultInstanceId = workflow.status?.defaultInstanceId ?? "";
|
|
@@ -149,8 +149,11 @@ function newLoadExecutionByIdStep<Desc extends DescMessage>(
|
|
|
149
149
|
executionId,
|
|
150
150
|
WorkflowExecutionSchema,
|
|
151
151
|
);
|
|
152
|
-
} catch {
|
|
153
|
-
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error instanceof ResourceNotFoundError) {
|
|
154
|
+
throw notFoundError("workflow_execution", executionId);
|
|
155
|
+
}
|
|
156
|
+
throw internalError(error, "failed to load workflow execution");
|
|
154
157
|
}
|
|
155
158
|
ctx.set(LOADED_EXECUTION_KEY, execution);
|
|
156
159
|
},
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* Codes (pinned): InvalidArgument (missing fields), NotFound (unknown
|
|
14
14
|
* execution), FailedPrecondition (terminal phase, or no engine),
|
|
15
15
|
* AlreadyExists (DELIVERED duplicate — stop retrying), Aborted (live
|
|
16
|
-
* in-flight claim — retry shortly)
|
|
16
|
+
* in-flight claim — retry shortly), Internal (a store fault loading the
|
|
17
|
+
* execution, never folded into NotFound). Dedupe-store errors degrade to
|
|
17
18
|
* no-dedupe rather than failing the send; a failed send RELEASES the
|
|
18
19
|
* claim (status-guarded) so the retry claims freshly instead of waiting
|
|
19
20
|
* out the in-flight hold.
|
|
@@ -41,7 +42,10 @@ import { newPipeline } from "../../pipeline/pipeline.js";
|
|
|
41
42
|
import type { CallerIdentity } from "../../extensions/identity.js";
|
|
42
43
|
import { RequestContext } from "../../pipeline/request-context.js";
|
|
43
44
|
import { newAuthorizeStep } from "../../pipeline/steps/authorize.js";
|
|
44
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
IN_FLIGHT_CLAIM_TTL_MS,
|
|
47
|
+
ResourceNotFoundError,
|
|
48
|
+
} from "../../store/interface.js";
|
|
45
49
|
import type { Store } from "../../store/interface.js";
|
|
46
50
|
|
|
47
51
|
import {
|
|
@@ -109,8 +113,11 @@ export async function sendSignal(
|
|
|
109
113
|
ctx.input.executionId,
|
|
110
114
|
WorkflowExecutionSchema,
|
|
111
115
|
);
|
|
112
|
-
} catch {
|
|
113
|
-
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error instanceof ResourceNotFoundError) {
|
|
118
|
+
throw notFoundError("workflow_execution", ctx.input.executionId);
|
|
119
|
+
}
|
|
120
|
+
throw internalError(error, "failed to load workflow execution");
|
|
114
121
|
}
|
|
115
122
|
ctx.set(LOADED_EXECUTION_KEY, execution);
|
|
116
123
|
},
|
|
@@ -38,6 +38,7 @@ import type { CallerIdentity } from "../../extensions/identity.js";
|
|
|
38
38
|
import { RequestContext } from "../../pipeline/request-context.js";
|
|
39
39
|
import { newAuthorizeStep } from "../../pipeline/steps/authorize.js";
|
|
40
40
|
import { newValidateProtoStep } from "../../pipeline/steps/validation.js";
|
|
41
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
41
42
|
import type { Store } from "../../store/interface.js";
|
|
42
43
|
|
|
43
44
|
/**
|
|
@@ -101,8 +102,11 @@ export async function submitApproval(
|
|
|
101
102
|
ctx.input.executionId,
|
|
102
103
|
WorkflowExecutionSchema,
|
|
103
104
|
);
|
|
104
|
-
} catch {
|
|
105
|
-
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error instanceof ResourceNotFoundError) {
|
|
107
|
+
throw notFoundError("workflow_execution", ctx.input.executionId);
|
|
108
|
+
}
|
|
109
|
+
throw internalError(error, "failed to load workflow execution");
|
|
106
110
|
}
|
|
107
111
|
ctx.set(TARGET_EXECUTION_KEY, execution);
|
|
108
112
|
},
|
|
@@ -36,6 +36,7 @@ import { newAuthorizeStep } from "../../pipeline/steps/authorize.js";
|
|
|
36
36
|
import type { CallerIdentity } from "../../extensions/identity.js";
|
|
37
37
|
import { RequestContext } from "../../pipeline/request-context.js";
|
|
38
38
|
import { newValidateProtoStep } from "../../pipeline/steps/validation.js";
|
|
39
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
39
40
|
import type { Store } from "../../store/interface.js";
|
|
40
41
|
|
|
41
42
|
/**
|
|
@@ -99,8 +100,11 @@ export async function submitFileDecision(
|
|
|
99
100
|
ctx.input.executionId,
|
|
100
101
|
WorkflowExecutionSchema,
|
|
101
102
|
);
|
|
102
|
-
} catch {
|
|
103
|
-
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (error instanceof ResourceNotFoundError) {
|
|
105
|
+
throw notFoundError("workflow_execution", ctx.input.executionId);
|
|
106
|
+
}
|
|
107
|
+
throw internalError(error, "failed to load workflow execution");
|
|
104
108
|
}
|
|
105
109
|
ctx.set(TARGET_EXECUTION_KEY, execution);
|
|
106
110
|
},
|
|
@@ -40,6 +40,7 @@ import type { Authorizer } from "../../extensions/authorizer.js";
|
|
|
40
40
|
import { newAuthorizeStep } from "../../pipeline/steps/authorize.js";
|
|
41
41
|
import type { CallerIdentity } from "../../extensions/identity.js";
|
|
42
42
|
import { RequestContext } from "../../pipeline/request-context.js";
|
|
43
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
43
44
|
import type { Store } from "../../store/interface.js";
|
|
44
45
|
|
|
45
46
|
import {
|
|
@@ -106,8 +107,11 @@ export async function submitWorkflowTaskApproval(
|
|
|
106
107
|
ctx.input.executionId,
|
|
107
108
|
WorkflowExecutionSchema,
|
|
108
109
|
);
|
|
109
|
-
} catch {
|
|
110
|
-
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof ResourceNotFoundError) {
|
|
112
|
+
throw notFoundError("WorkflowExecution", ctx.input.executionId);
|
|
113
|
+
}
|
|
114
|
+
throw internalError(error, "failed to load workflow execution");
|
|
111
115
|
}
|
|
112
116
|
ctx.set(LOADED_EXECUTION_KEY, execution);
|
|
113
117
|
},
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
notFoundError,
|
|
45
45
|
} from "../../pipeline/errors.js";
|
|
46
46
|
import { authorizeDirect } from "../../pipeline/steps/authorize.js";
|
|
47
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
47
48
|
import type {
|
|
48
49
|
Store,
|
|
49
50
|
WorkflowExecutionEventRecord,
|
|
@@ -88,8 +89,11 @@ export async function* subscribeEvents(
|
|
|
88
89
|
executionId,
|
|
89
90
|
WorkflowExecutionSchema,
|
|
90
91
|
);
|
|
91
|
-
} catch {
|
|
92
|
-
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error instanceof ResourceNotFoundError) {
|
|
94
|
+
throw notFoundError("WorkflowExecution", executionId);
|
|
95
|
+
}
|
|
96
|
+
throw internalError(error, "failed to load workflow execution");
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
let typeFilter: Set<string> | undefined;
|
|
@@ -40,8 +40,13 @@ import { WorkflowExecutionQueryController } from "@stigmer/protos/ai/stigmer/age
|
|
|
40
40
|
import type { Logger } from "../../boot/logger.js";
|
|
41
41
|
import type { Authorizer } from "../../extensions/authorizer.js";
|
|
42
42
|
import { callerIdentityOf } from "../../pipeline/interceptors/auth.js";
|
|
43
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
internalError,
|
|
45
|
+
invalidArgumentError,
|
|
46
|
+
notFoundError,
|
|
47
|
+
} from "../../pipeline/errors.js";
|
|
44
48
|
import { authorizeDirect } from "../../pipeline/steps/authorize.js";
|
|
49
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
45
50
|
import type { Store } from "../../store/interface.js";
|
|
46
51
|
|
|
47
52
|
import type { StreamBroker } from "./stream-broker.js";
|
|
@@ -103,9 +108,11 @@ export async function* subscribeExecution(
|
|
|
103
108
|
id,
|
|
104
109
|
WorkflowExecutionSchema,
|
|
105
110
|
);
|
|
106
|
-
} catch {
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error instanceof ResourceNotFoundError) {
|
|
113
|
+
throw notFoundError("WorkflowExecution", id);
|
|
114
|
+
}
|
|
115
|
+
throw internalError(error, "failed to load workflow execution");
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
yield snapshot;
|