@shipfox/api-workflows 11.0.0 → 12.1.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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +107 -0
- package/README.md +75 -0
- package/dist/core/agent-defaults.d.ts +2 -1
- package/dist/core/agent-defaults.d.ts.map +1 -1
- package/dist/core/agent-defaults.js.map +1 -1
- package/dist/core/checkout.d.ts +7 -21
- package/dist/core/checkout.d.ts.map +1 -1
- package/dist/core/checkout.js +122 -41
- package/dist/core/checkout.js.map +1 -1
- package/dist/core/entities/job-execution.d.ts +8 -0
- package/dist/core/entities/job-execution.d.ts.map +1 -1
- package/dist/core/entities/job-execution.js +13 -1
- package/dist/core/entities/job-execution.js.map +1 -1
- package/dist/core/entities/job-listener-event.d.ts +2 -0
- package/dist/core/entities/job-listener-event.d.ts.map +1 -1
- package/dist/core/entities/job-listener-event.js.map +1 -1
- package/dist/core/entities/step.d.ts +13 -3
- package/dist/core/entities/step.d.ts.map +1 -1
- package/dist/core/entities/step.js +6 -0
- package/dist/core/entities/step.js.map +1 -1
- package/dist/core/entities/workflow-run-attempt.d.ts +1 -0
- package/dist/core/entities/workflow-run-attempt.d.ts.map +1 -1
- package/dist/core/entities/workflow-run-attempt.js.map +1 -1
- package/dist/core/entities/workflow-run.d.ts +17 -0
- package/dist/core/entities/workflow-run.d.ts.map +1 -1
- package/dist/core/entities/workflow-run.js.map +1 -1
- package/dist/core/errors.d.ts +9 -2
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +10 -5
- package/dist/core/errors.js.map +1 -1
- package/dist/core/job-execution.d.ts.map +1 -1
- package/dist/core/job-execution.js +14 -8
- package/dist/core/job-execution.js.map +1 -1
- package/dist/core/job-transition/decide-job-activation.d.ts +1 -0
- package/dist/core/job-transition/decide-job-activation.d.ts.map +1 -1
- package/dist/core/job-transition/decide-job-activation.js +1 -0
- package/dist/core/job-transition/decide-job-activation.js.map +1 -1
- package/dist/core/job-transition/derive-job-success.d.ts +1 -0
- package/dist/core/job-transition/derive-job-success.d.ts.map +1 -1
- package/dist/core/job-transition/derive-job-success.js +5 -8
- package/dist/core/job-transition/derive-job-success.js.map +1 -1
- package/dist/core/listener-execution-materialization.d.ts +6 -3
- package/dist/core/listener-execution-materialization.d.ts.map +1 -1
- package/dist/core/listener-execution-materialization.js +44 -6
- package/dist/core/listener-execution-materialization.js.map +1 -1
- package/dist/core/resolve-trigger-reference.d.ts +11 -0
- package/dist/core/resolve-trigger-reference.d.ts.map +1 -0
- package/dist/core/resolve-trigger-reference.js +39 -0
- package/dist/core/resolve-trigger-reference.js.map +1 -0
- package/dist/core/run-workflow.d.ts +1 -0
- package/dist/core/run-workflow.d.ts.map +1 -1
- package/dist/core/run-workflow.js +1 -0
- package/dist/core/run-workflow.js.map +1 -1
- package/dist/core/step-config/agent.d.ts +1 -2
- package/dist/core/step-config/agent.d.ts.map +1 -1
- package/dist/core/step-config/agent.js +22 -7
- package/dist/core/step-config/agent.js.map +1 -1
- package/dist/core/step-config/assemble-run-context.d.ts +19 -8
- package/dist/core/step-config/assemble-run-context.d.ts.map +1 -1
- package/dist/core/step-config/assemble-run-context.js +51 -19
- package/dist/core/step-config/assemble-run-context.js.map +1 -1
- package/dist/core/step-config/complete-step-dispatch-config.d.ts.map +1 -1
- package/dist/core/step-config/complete-step-dispatch-config.js +72 -4
- package/dist/core/step-config/complete-step-dispatch-config.js.map +1 -1
- package/dist/core/step-config/index.d.ts +1 -0
- package/dist/core/step-config/index.d.ts.map +1 -1
- package/dist/core/step-config/index.js +1 -0
- package/dist/core/step-config/index.js.map +1 -1
- package/dist/core/step-config/materialize-job-execution-steps.d.ts +1 -1
- package/dist/core/step-config/materialize-job-execution-steps.d.ts.map +1 -1
- package/dist/core/step-config/materialize-job-execution-steps.js +49 -8
- package/dist/core/step-config/materialize-job-execution-steps.js.map +1 -1
- package/dist/core/step-config/materialize-workflow-model.d.ts +3 -3
- package/dist/core/step-config/materialize-workflow-model.d.ts.map +1 -1
- package/dist/core/step-config/materialize-workflow-model.js +9 -5
- package/dist/core/step-config/materialize-workflow-model.js.map +1 -1
- package/dist/core/step-config/resolve-job-execution-name.d.ts +3 -3
- package/dist/core/step-config/resolve-job-execution-name.d.ts.map +1 -1
- package/dist/core/step-config/resolve-job-execution-name.js +24 -13
- package/dist/core/step-config/resolve-job-execution-name.js.map +1 -1
- package/dist/core/step-config/resolve-step-config.d.ts.map +1 -1
- package/dist/core/step-config/resolve-step-config.js +174 -9
- package/dist/core/step-config/resolve-step-config.js.map +1 -1
- package/dist/core/step-config/resolve-workflow-run-name.d.ts +19 -0
- package/dist/core/step-config/resolve-workflow-run-name.d.ts.map +1 -0
- package/dist/core/step-config/resolve-workflow-run-name.js +73 -0
- package/dist/core/step-config/resolve-workflow-run-name.js.map +1 -0
- package/dist/core/step-transition/decide-step-transition.js.map +1 -1
- package/dist/core/step-transition/evaluate-gate.d.ts +2 -1
- package/dist/core/step-transition/evaluate-gate.d.ts.map +1 -1
- package/dist/core/step-transition/evaluate-gate.js +5 -3
- package/dist/core/step-transition/evaluate-gate.js.map +1 -1
- package/dist/core/workflow-run-creation.d.ts +3 -0
- package/dist/core/workflow-run-creation.d.ts.map +1 -1
- package/dist/core/workflow-run-creation.js +22 -7
- package/dist/core/workflow-run-creation.js.map +1 -1
- package/dist/db/db.d.ts +278 -10
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +2 -0
- package/dist/db/db.js.map +1 -1
- package/dist/db/index.d.ts +3 -2
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +2 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/job-listener-events.d.ts +2 -0
- package/dist/db/job-listener-events.d.ts.map +1 -1
- package/dist/db/job-listener-events.js +1 -0
- package/dist/db/job-listener-events.js.map +1 -1
- package/dist/db/job-listeners.d.ts +2 -0
- package/dist/db/job-listeners.d.ts.map +1 -1
- package/dist/db/job-listeners.js +81 -21
- package/dist/db/job-listeners.js.map +1 -1
- package/dist/db/schema/job-executions.d.ts +3 -3
- package/dist/db/schema/job-executions.d.ts.map +1 -1
- package/dist/db/schema/job-executions.js +8 -4
- package/dist/db/schema/job-executions.js.map +1 -1
- package/dist/db/schema/job-listener-events.d.ts +20 -0
- package/dist/db/schema/job-listener-events.d.ts.map +1 -1
- package/dist/db/schema/job-listener-events.js +2 -0
- package/dist/db/schema/job-listener-events.js.map +1 -1
- package/dist/db/schema/step-attempts.js +2 -2
- package/dist/db/schema/step-attempts.js.map +1 -1
- package/dist/db/schema/steps.d.ts +4 -2
- package/dist/db/schema/steps.d.ts.map +1 -1
- package/dist/db/schema/steps.js +1 -1
- package/dist/db/schema/steps.js.map +1 -1
- package/dist/db/schema/workflow-run-attempts.d.ts +21 -2
- package/dist/db/schema/workflow-run-attempts.d.ts.map +1 -1
- package/dist/db/schema/workflow-run-attempts.js +2 -0
- package/dist/db/schema/workflow-run-attempts.js.map +1 -1
- package/dist/db/schema/workflow-run-counters.d.ts +43 -0
- package/dist/db/schema/workflow-run-counters.d.ts.map +1 -0
- package/dist/db/schema/workflow-run-counters.js +10 -0
- package/dist/db/schema/workflow-run-counters.js.map +1 -0
- package/dist/db/schema/workflow-runs.d.ts +54 -1
- package/dist/db/schema/workflow-runs.d.ts.map +1 -1
- package/dist/db/schema/workflow-runs.js +10 -2
- package/dist/db/schema/workflow-runs.js.map +1 -1
- package/dist/db/workflow-runs/job-executions.d.ts.map +1 -1
- package/dist/db/workflow-runs/job-executions.js +35 -14
- package/dist/db/workflow-runs/job-executions.js.map +1 -1
- package/dist/db/workflow-runs/jobs.d.ts +2 -0
- package/dist/db/workflow-runs/jobs.d.ts.map +1 -1
- package/dist/db/workflow-runs/jobs.js +9 -5
- package/dist/db/workflow-runs/jobs.js.map +1 -1
- package/dist/db/workflow-runs/queries.d.ts +51 -0
- package/dist/db/workflow-runs/queries.d.ts.map +1 -1
- package/dist/db/workflow-runs/queries.js +82 -2
- package/dist/db/workflow-runs/queries.js.map +1 -1
- package/dist/db/workflow-runs/run-create.d.ts +1 -0
- package/dist/db/workflow-runs/run-create.d.ts.map +1 -1
- package/dist/db/workflow-runs/run-create.js +159 -32
- package/dist/db/workflow-runs/run-create.js.map +1 -1
- package/dist/db/workflow-runs/run-rerun.d.ts.map +1 -1
- package/dist/db/workflow-runs/run-rerun.js +13 -5
- package/dist/db/workflow-runs/run-rerun.js.map +1 -1
- package/dist/db/workflow-runs/shared.d.ts +1 -0
- package/dist/db/workflow-runs/shared.d.ts.map +1 -1
- package/dist/db/workflow-runs/shared.js +2 -1
- package/dist/db/workflow-runs/shared.js.map +1 -1
- package/dist/db/workflow-runs/steps.js +3 -3
- package/dist/db/workflow-runs/steps.js.map +1 -1
- package/dist/db/workflow-runs.d.ts +4 -3
- package/dist/db/workflow-runs.d.ts.map +1 -1
- package/dist/db/workflow-runs.js +3 -2
- package/dist/db/workflow-runs.js.map +1 -1
- package/dist/metrics/instance.d.ts +1 -0
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js +9 -0
- package/dist/metrics/instance.js.map +1 -1
- package/dist/presentation/dto/checkout-token.d.ts +1 -0
- package/dist/presentation/dto/checkout-token.d.ts.map +1 -1
- package/dist/presentation/dto/checkout-token.js +1 -0
- package/dist/presentation/dto/checkout-token.js.map +1 -1
- package/dist/presentation/dto/index.d.ts +1 -1
- package/dist/presentation/dto/index.d.ts.map +1 -1
- package/dist/presentation/dto/index.js +1 -1
- package/dist/presentation/dto/index.js.map +1 -1
- package/dist/presentation/dto/step.d.ts.map +1 -1
- package/dist/presentation/dto/step.js +2 -2
- package/dist/presentation/dto/step.js.map +1 -1
- package/dist/presentation/dto/workflow-run.d.ts +3 -1
- package/dist/presentation/dto/workflow-run.d.ts.map +1 -1
- package/dist/presentation/dto/workflow-run.js +34 -0
- package/dist/presentation/dto/workflow-run.js.map +1 -1
- package/dist/presentation/inter-module.d.ts.map +1 -1
- package/dist/presentation/inter-module.js +31 -3
- package/dist/presentation/inter-module.js.map +1 -1
- package/dist/presentation/routes/checkout-token.d.ts +1 -1
- package/dist/presentation/routes/checkout-token.d.ts.map +1 -1
- package/dist/presentation/routes/checkout-token.js +32 -20
- package/dist/presentation/routes/checkout-token.js.map +1 -1
- package/dist/presentation/routes/leased-step.d.ts +2 -0
- package/dist/presentation/routes/leased-step.d.ts.map +1 -1
- package/dist/presentation/routes/leased-step.js +2 -1
- package/dist/presentation/routes/leased-step.js.map +1 -1
- package/dist/presentation/routes/list-runs.d.ts.map +1 -1
- package/dist/presentation/routes/list-runs.js +9 -3
- package/dist/presentation/routes/list-runs.js.map +1 -1
- package/dist/presentation/routes/project-access.d.ts +1 -0
- package/dist/presentation/routes/project-access.d.ts.map +1 -1
- package/dist/presentation/routes/report-step.d.ts.map +1 -1
- package/dist/presentation/routes/report-step.js +9 -2
- package/dist/presentation/routes/report-step.js.map +1 -1
- package/dist/presentation/subscribers/on-job-steps-settled.js +1 -1
- package/dist/presentation/subscribers/on-job-steps-settled.js.map +1 -1
- package/dist/presentation/subscribers/on-runner-job-claimed.d.ts.map +1 -1
- package/dist/presentation/subscribers/on-runner-job-claimed.js +21 -0
- package/dist/presentation/subscribers/on-runner-job-claimed.js.map +1 -1
- package/dist/temporal/activities/index.d.ts.map +1 -1
- package/dist/temporal/activities/index.js +2 -1
- package/dist/temporal/activities/index.js.map +1 -1
- package/dist/temporal/activities/orchestration-activities.d.ts +1 -0
- package/dist/temporal/activities/orchestration-activities.d.ts.map +1 -1
- package/dist/temporal/activities/orchestration-activities.js.map +1 -1
- package/dist/temporal/constants.d.ts +1 -0
- package/dist/temporal/constants.d.ts.map +1 -1
- package/dist/temporal/constants.js +1 -0
- package/dist/temporal/constants.js.map +1 -1
- package/dist/temporal/index.d.ts +1 -1
- package/dist/temporal/index.d.ts.map +1 -1
- package/dist/temporal/index.js +1 -1
- package/dist/temporal/index.js.map +1 -1
- package/dist/temporal/workflows/index.bundle.js +82 -43
- package/dist/temporal/workflows/index.d.ts +1 -1
- package/dist/temporal/workflows/index.d.ts.map +1 -1
- package/dist/temporal/workflows/index.js +1 -1
- package/dist/temporal/workflows/index.js.map +1 -1
- package/dist/temporal/workflows/job-execution-orchestration.d.ts +5 -0
- package/dist/temporal/workflows/job-execution-orchestration.d.ts.map +1 -1
- package/dist/temporal/workflows/job-execution-orchestration.js +77 -43
- package/dist/temporal/workflows/job-execution-orchestration.js.map +1 -1
- package/dist/temporal/workflows/test-env.d.ts +5 -1
- package/dist/temporal/workflows/test-env.d.ts.map +1 -1
- package/dist/temporal/workflows/test-env.js +32 -11
- package/dist/temporal/workflows/test-env.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0000_initial.sql +14 -2
- package/drizzle/0001_windy_sunfire.sql +1 -0
- package/drizzle/meta/0000_snapshot.json +93 -1
- package/drizzle/meta/0001_snapshot.json +1711 -0
- package/drizzle/meta/_journal.json +2 -2
- package/package.json +19 -19
- package/src/core/agent-defaults.ts +2 -1
- package/src/core/checkout.test.ts +378 -67
- package/src/core/checkout.ts +142 -59
- package/src/core/entities/job-execution.test.ts +21 -0
- package/src/core/entities/job-execution.ts +24 -0
- package/src/core/entities/job-listener-event.ts +3 -0
- package/src/core/entities/step.ts +15 -3
- package/src/core/entities/workflow-run-attempt.ts +1 -0
- package/src/core/entities/workflow-run.ts +16 -0
- package/src/core/errors.ts +21 -7
- package/src/core/job-execution.ts +27 -4
- package/src/core/job-transition/decide-job-activation.test.ts +4 -0
- package/src/core/job-transition/decide-job-activation.ts +2 -0
- package/src/core/job-transition/derive-job-success.test.ts +12 -0
- package/src/core/job-transition/derive-job-success.ts +6 -9
- package/src/core/listener-execution-materialization.test.ts +2 -0
- package/src/core/listener-execution-materialization.ts +57 -11
- package/src/core/resolve-trigger-reference.test.ts +160 -0
- package/src/core/resolve-trigger-reference.ts +53 -0
- package/src/core/run-workflow.ts +2 -0
- package/src/core/step-config/agent.ts +29 -12
- package/src/core/step-config/assemble-run-context.test.ts +149 -6
- package/src/core/step-config/assemble-run-context.ts +87 -25
- package/src/core/step-config/complete-step-dispatch-config.test.ts +125 -2
- package/src/core/step-config/complete-step-dispatch-config.ts +83 -1
- package/src/core/step-config/index.ts +7 -0
- package/src/core/step-config/materialize-job-execution-steps.test.ts +250 -1
- package/src/core/step-config/materialize-job-execution-steps.ts +57 -10
- package/src/core/step-config/materialize-workflow-model.test.ts +212 -4
- package/src/core/step-config/materialize-workflow-model.ts +33 -28
- package/src/core/step-config/resolve-job-execution-name.test.ts +37 -15
- package/src/core/step-config/resolve-job-execution-name.ts +30 -15
- package/src/core/step-config/resolve-step-config.ts +175 -10
- package/src/core/step-config/resolve-workflow-run-name.test.ts +115 -0
- package/src/core/step-config/resolve-workflow-run-name.ts +108 -0
- package/src/core/step-transition/decide-step-transition.ts +2 -2
- package/src/core/step-transition/evaluate-gate.test.ts +8 -0
- package/src/core/step-transition/evaluate-gate.ts +8 -1
- package/src/core/workflow-run-creation.test.ts +7 -4
- package/src/core/workflow-run-creation.ts +13 -7
- package/src/db/db.ts +2 -0
- package/src/db/index.ts +6 -0
- package/src/db/job-listener-events.test.ts +19 -0
- package/src/db/job-listener-events.ts +3 -0
- package/src/db/job-listeners.test.ts +187 -5
- package/src/db/job-listeners.ts +122 -30
- package/src/db/schema/job-executions.ts +12 -5
- package/src/db/schema/job-listener-events.ts +3 -0
- package/src/db/schema/step-attempts.ts +2 -2
- package/src/db/schema/steps.ts +2 -1
- package/src/db/schema/workflow-run-attempts.ts +2 -0
- package/src/db/schema/workflow-run-counters.ts +13 -0
- package/src/db/schema/workflow-runs.ts +11 -2
- package/src/db/workflow-runs/job-executions.ts +37 -15
- package/src/db/workflow-runs/jobs.ts +17 -4
- package/src/db/workflow-runs/queries.test.ts +206 -0
- package/src/db/workflow-runs/queries.ts +146 -2
- package/src/db/workflow-runs/run-create.test.ts +606 -8
- package/src/db/workflow-runs/run-create.ts +185 -40
- package/src/db/workflow-runs/run-rerun.test.ts +61 -2
- package/src/db/workflow-runs/run-rerun.ts +15 -5
- package/src/db/workflow-runs/shared.ts +2 -0
- package/src/db/workflow-runs/steps.ts +3 -3
- package/src/db/workflow-runs.ts +7 -0
- package/src/metrics/instance.ts +14 -0
- package/src/presentation/dto/checkout-token.test.ts +19 -9
- package/src/presentation/dto/checkout-token.ts +2 -1
- package/src/presentation/dto/index.ts +1 -1
- package/src/presentation/dto/job.test.ts +9 -0
- package/src/presentation/dto/step.test.ts +12 -0
- package/src/presentation/dto/step.ts +5 -2
- package/src/presentation/dto/workflow-run.ts +44 -2
- package/src/presentation/inter-module.test.ts +165 -0
- package/src/presentation/inter-module.ts +38 -4
- package/src/presentation/routes/checkout-token.test.ts +357 -142
- package/src/presentation/routes/checkout-token.ts +56 -26
- package/src/presentation/routes/get-run.test.ts +2 -0
- package/src/presentation/routes/get-step-secrets.test.ts +7 -2
- package/src/presentation/routes/leased-step.ts +9 -1
- package/src/presentation/routes/list-runs.test.ts +57 -0
- package/src/presentation/routes/list-runs.ts +8 -3
- package/src/presentation/routes/report-step.test.ts +25 -0
- package/src/presentation/routes/report-step.ts +10 -2
- package/src/presentation/subscribers/on-job-steps-settled.ts +1 -1
- package/src/presentation/subscribers/on-runner-job-claimed.test.ts +49 -0
- package/src/presentation/subscribers/on-runner-job-claimed.ts +22 -0
- package/src/temporal/activities/index.ts +1 -0
- package/src/temporal/activities/orchestration-activities.ts +1 -0
- package/src/temporal/constants.ts +1 -0
- package/src/temporal/index.ts +1 -0
- package/src/temporal/workflows/index.ts +1 -0
- package/src/temporal/workflows/job-execution-orchestration.test.ts +142 -6
- package/src/temporal/workflows/job-execution-orchestration.ts +99 -50
- package/src/temporal/workflows/run-orchestration.test.ts +1 -1
- package/src/temporal/workflows/test-env.ts +50 -13
- package/test/factories/project.ts +2 -0
- package/test/factories/workflow-model.ts +80 -26
- package/test/factories/workflow-run.ts +4 -1
- package/test/fixtures/agent-inter-module.ts +2 -1
- package/test/fixtures/job-with-steps.ts +1 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/drizzle/0001_agent_tool_materialization.sql +0 -1
|
@@ -67,6 +67,21 @@ function finalStatusesFor(jobId: string): string[] {
|
|
|
67
67
|
.map((c) => c.params.status);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
async function waitForExecutionStatus(jobExecutionId: string, status: string): Promise<void> {
|
|
71
|
+
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
72
|
+
if (
|
|
73
|
+
setExecutionStatusCalls().some(
|
|
74
|
+
(call) => call.params.jobExecutionId === jobExecutionId && call.params.status === status,
|
|
75
|
+
)
|
|
76
|
+
) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
throw new Error(`Timed out waiting for ${jobExecutionId} to reach ${status}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
70
85
|
function terminalSetJobCall(jobId: string) {
|
|
71
86
|
return setExecutionStatusCalls().find(
|
|
72
87
|
(call) => call.params.jobExecutionId === jobId && call.params.status !== 'running',
|
|
@@ -92,7 +107,7 @@ async function expectEmptyRequiredLabelsFailure(input: typeof defaultJobInput):
|
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
describe('jobExecutionOrchestration', () => {
|
|
95
|
-
test('
|
|
110
|
+
test('runner claim flips status to running, then finished releases the lease', async () => {
|
|
96
111
|
setCfg({
|
|
97
112
|
dag: makeDag([dagJob('job-1', 'build')]),
|
|
98
113
|
jobResults: new Map([['job-1', 'succeeded']]),
|
|
@@ -107,6 +122,109 @@ describe('jobExecutionOrchestration', () => {
|
|
|
107
122
|
expect(callsNamed('bulkSetStepStatuses')).toHaveLength(0);
|
|
108
123
|
});
|
|
109
124
|
|
|
125
|
+
test('keeps an execution pending until the current runner claim', async () => {
|
|
126
|
+
setCfg({dag: makeDag([]), jobResults: new Map(), skipSignal: true});
|
|
127
|
+
|
|
128
|
+
const handle = await testEnv.client.workflow.start('jobExecutionOrchestration', {
|
|
129
|
+
taskQueue: TASK_QUEUE,
|
|
130
|
+
workflowId: 'job:job-pending-before-claim',
|
|
131
|
+
args: [
|
|
132
|
+
{
|
|
133
|
+
...defaultJobInput,
|
|
134
|
+
jobId: 'job-pending-before-claim',
|
|
135
|
+
jobExecutionId: 'job-pending-before-claim',
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
141
|
+
expect(setExecutionStatusCalls()).toHaveLength(0);
|
|
142
|
+
|
|
143
|
+
await handle.signal('job-claimed', {
|
|
144
|
+
jobExecutionId: 'job-pending-before-claim',
|
|
145
|
+
claimedAt: '2026-06-22T10:05:00.000Z',
|
|
146
|
+
});
|
|
147
|
+
await waitForExecutionStatus('job-pending-before-claim', 'running');
|
|
148
|
+
await handle.signal('job-finished', {
|
|
149
|
+
jobExecutionId: 'job-pending-before-claim',
|
|
150
|
+
status: 'succeeded',
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const result = await handle.result();
|
|
154
|
+
expect(result.status).toBe('succeeded');
|
|
155
|
+
expect(finalStatusesFor('job-pending-before-claim')).toEqual(['running', 'succeeded']);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('ignores stale and duplicate claims and transitions exactly once', async () => {
|
|
159
|
+
setCfg({dag: makeDag([]), jobResults: new Map(), skipSignal: true});
|
|
160
|
+
|
|
161
|
+
const handle = await testEnv.client.workflow.start('jobExecutionOrchestration', {
|
|
162
|
+
taskQueue: TASK_QUEUE,
|
|
163
|
+
workflowId: 'job:job-claim-idempotent',
|
|
164
|
+
args: [
|
|
165
|
+
{
|
|
166
|
+
...defaultJobInput,
|
|
167
|
+
jobId: 'job-claim-idempotent',
|
|
168
|
+
jobExecutionId: 'execution-current',
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await handle.signal('job-claimed', {
|
|
174
|
+
jobExecutionId: 'execution-stale',
|
|
175
|
+
claimedAt: '2026-06-22T10:04:00.000Z',
|
|
176
|
+
});
|
|
177
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
178
|
+
expect(setExecutionStatusCalls()).toHaveLength(0);
|
|
179
|
+
|
|
180
|
+
const claim = {
|
|
181
|
+
jobExecutionId: 'execution-current',
|
|
182
|
+
claimedAt: '2026-06-22T10:05:00.000Z',
|
|
183
|
+
};
|
|
184
|
+
await handle.signal('job-claimed', claim);
|
|
185
|
+
await handle.signal('job-claimed', {...claim, claimedAt: '2026-06-22T10:06:00.000Z'});
|
|
186
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
187
|
+
await handle.signal('job-finished', {
|
|
188
|
+
jobExecutionId: 'execution-current',
|
|
189
|
+
status: 'succeeded',
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const result = await handle.result();
|
|
193
|
+
expect(result.status).toBe('succeeded');
|
|
194
|
+
expect(finalStatusesFor('execution-current')).toEqual(['running', 'succeeded']);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('a finish before a delayed claim remains terminal', async () => {
|
|
198
|
+
setCfg({dag: makeDag([]), jobResults: new Map(), skipSignal: true});
|
|
199
|
+
|
|
200
|
+
const handle = await testEnv.client.workflow.start('jobExecutionOrchestration', {
|
|
201
|
+
taskQueue: TASK_QUEUE,
|
|
202
|
+
workflowId: 'job:job-finish-before-claim',
|
|
203
|
+
args: [
|
|
204
|
+
{
|
|
205
|
+
...defaultJobInput,
|
|
206
|
+
jobId: 'job-finish-before-claim',
|
|
207
|
+
jobExecutionId: 'job-finish-before-claim',
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
await handle.signal('job-finished', {
|
|
213
|
+
jobExecutionId: 'job-finish-before-claim',
|
|
214
|
+
status: 'succeeded',
|
|
215
|
+
});
|
|
216
|
+
const result = await handle.result();
|
|
217
|
+
|
|
218
|
+
expect(result.status).toBe('succeeded');
|
|
219
|
+
expect(finalStatusesFor('job-finish-before-claim')).toEqual(['succeeded']);
|
|
220
|
+
await expect(
|
|
221
|
+
handle.signal('job-claimed', {
|
|
222
|
+
jobExecutionId: 'job-finish-before-claim',
|
|
223
|
+
claimedAt: '2026-06-22T10:05:00.000Z',
|
|
224
|
+
}),
|
|
225
|
+
).rejects.toThrow();
|
|
226
|
+
});
|
|
227
|
+
|
|
110
228
|
test('empty required labels fail before the job is marked running', async () => {
|
|
111
229
|
setCfg({
|
|
112
230
|
dag: makeDag([dagJob('job-empty-labels', 'build')]),
|
|
@@ -272,7 +390,7 @@ describe('jobExecutionOrchestration', () => {
|
|
|
272
390
|
]);
|
|
273
391
|
}, 15_000);
|
|
274
392
|
|
|
275
|
-
test('already-terminal job is not
|
|
393
|
+
test('already-terminal job is not reopened after a claim', async () => {
|
|
276
394
|
setCfg({
|
|
277
395
|
dag: makeDag([]),
|
|
278
396
|
jobResults: new Map(),
|
|
@@ -282,7 +400,7 @@ describe('jobExecutionOrchestration', () => {
|
|
|
282
400
|
const result = await executeJob({...defaultJobInput, jobId: 'job-cancelled'});
|
|
283
401
|
|
|
284
402
|
expect(result).toMatchObject({status: 'cancelled'});
|
|
285
|
-
expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(
|
|
403
|
+
expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(1);
|
|
286
404
|
expect(callsNamed('releaseLeaseActivity')).toHaveLength(0);
|
|
287
405
|
});
|
|
288
406
|
|
|
@@ -314,7 +432,7 @@ describe('jobExecutionOrchestration', () => {
|
|
|
314
432
|
|
|
315
433
|
expect(result.status).toBe('failed');
|
|
316
434
|
expect(callsNamed('failJobExecutionAsTimedOutActivity')).toHaveLength(1);
|
|
317
|
-
expect(finalStatusesFor('job-timeout')).toEqual([
|
|
435
|
+
expect(finalStatusesFor('job-timeout')).toEqual([]);
|
|
318
436
|
expect(callsNamed('bulkSetStepStatuses')).toContainEqual({
|
|
319
437
|
name: 'bulkSetStepStatuses',
|
|
320
438
|
params: {jobExecutionId: 'job-timeout', status: 'failed'},
|
|
@@ -323,6 +441,24 @@ describe('jobExecutionOrchestration', () => {
|
|
|
323
441
|
expect(callsNamed('resolveLeaseExpiredJobExecutionActivity')).toHaveLength(0);
|
|
324
442
|
});
|
|
325
443
|
|
|
444
|
+
test('does not reset the execution deadline after claim', async () => {
|
|
445
|
+
setCfg({
|
|
446
|
+
dag: makeDag([dagJob('job-claim-timeout', 'build')]),
|
|
447
|
+
jobResults: new Map(),
|
|
448
|
+
skipOutcomeSignal: true,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
const result = await executeJob({
|
|
452
|
+
...defaultJobInput,
|
|
453
|
+
jobId: 'job-claim-timeout',
|
|
454
|
+
executionTimeoutMs: 250,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
expect(result.status).toBe('failed');
|
|
458
|
+
expect(finalStatusesFor('job-claim-timeout')).toEqual(['running']);
|
|
459
|
+
expect(callsNamed('failJobExecutionAsTimedOutActivity')).toHaveLength(1);
|
|
460
|
+
});
|
|
461
|
+
|
|
326
462
|
test('surfaces a timeout activity failure', async () => {
|
|
327
463
|
setCfg({
|
|
328
464
|
dag: makeDag([dagJob('job-timeout-error', 'build')]),
|
|
@@ -339,10 +475,10 @@ describe('jobExecutionOrchestration', () => {
|
|
|
339
475
|
}),
|
|
340
476
|
).rejects.toThrow();
|
|
341
477
|
|
|
342
|
-
expect(finalStatusesFor('job-timeout-error')).toEqual([
|
|
478
|
+
expect(finalStatusesFor('job-timeout-error')).toEqual([]);
|
|
343
479
|
});
|
|
344
480
|
|
|
345
|
-
test('no signal
|
|
481
|
+
test('no signal: workflow stays blocked indefinitely', async () => {
|
|
346
482
|
setCfg({dag: makeDag([]), jobResults: new Map(), skipSignal: true});
|
|
347
483
|
|
|
348
484
|
const handle = await testEnv.client.workflow.start('jobExecutionOrchestration', {
|
|
@@ -9,25 +9,25 @@ import {
|
|
|
9
9
|
import type {RuntimeCompletionStatus} from '#core/workflow-scheduling/runtime-dag.js';
|
|
10
10
|
|
|
11
11
|
import type {createOrchestrationActivities} from '../activities/index.js';
|
|
12
|
-
import {JOB_FINISHED_SIGNAL, JOB_LEASE_EXPIRED_SIGNAL} from '../constants.js';
|
|
12
|
+
import {JOB_CLAIMED_SIGNAL, JOB_FINISHED_SIGNAL, JOB_LEASE_EXPIRED_SIGNAL} from '../constants.js';
|
|
13
|
+
import {remainingMs} from './deadline.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
16
|
+
* Enqueue, wait for the runner claim, then wait for one of the terminal facts:
|
|
16
17
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* └─ neither (6h) → failJobExecutionAsTimedOutActivity + sweep steps (NO release; the
|
|
24
|
-
* TIMED_OUT event drives cooperative cancel, the detector reaps)
|
|
18
|
+
* enqueue ──> PENDING ── job-claimed ──> RUNNING
|
|
19
|
+
* │ │
|
|
20
|
+
* │ timeout ├─ job-finished
|
|
21
|
+
* │ ├─ job-lease-expired
|
|
22
|
+
* ▼ └─ timeout
|
|
23
|
+
* TERMINAL
|
|
25
24
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
25
|
+
* The claim signal is the runner-owned lifecycle boundary. The workflow keeps the
|
|
26
|
+
* execution pending while it is queued, then performs the versioned pending → running
|
|
27
|
+
* transition after the current claim. One deadline spans both waits; claim never resets
|
|
28
|
+
* it. Signals can arrive in any order, so the precedence is finished > lease expired >
|
|
29
|
+
* claimed > timeout. Lease cleanup remains best-effort: a runners DB outage must never
|
|
30
|
+
* block the child workflow from returning the job outcome to run-orchestration.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
const {
|
|
@@ -64,6 +64,11 @@ export const jobFinishedSignal =
|
|
|
64
64
|
);
|
|
65
65
|
export const jobLeaseExpiredSignal =
|
|
66
66
|
defineSignal<[{jobExecutionId?: string | undefined}]>(JOB_LEASE_EXPIRED_SIGNAL);
|
|
67
|
+
export interface JobClaimedSignalPayload {
|
|
68
|
+
jobExecutionId: string;
|
|
69
|
+
claimedAt: string;
|
|
70
|
+
}
|
|
71
|
+
export const jobClaimedSignal = defineSignal<[JobClaimedSignalPayload]>(JOB_CLAIMED_SIGNAL);
|
|
67
72
|
|
|
68
73
|
export interface JobExecutionOrchestrationInput {
|
|
69
74
|
workspaceId: string;
|
|
@@ -116,12 +121,7 @@ async function resolveJobStatusOrFailClosed(
|
|
|
116
121
|
}
|
|
117
122
|
}
|
|
118
123
|
|
|
119
|
-
async function
|
|
120
|
-
input: JobExecutionOrchestrationInput,
|
|
121
|
-
): Promise<
|
|
122
|
-
| {kind: 'running'; runningVersion: number}
|
|
123
|
-
| {kind: 'terminal'; result: JobExecutionOrchestrationResult}
|
|
124
|
-
> {
|
|
124
|
+
async function enqueueJobExecution(input: JobExecutionOrchestrationInput): Promise<void> {
|
|
125
125
|
if (hasNoRequiredRunnerLabels(input.requiredLabels)) {
|
|
126
126
|
throw ApplicationFailure.nonRetryable(
|
|
127
127
|
`Job ${input.jobId} has no required runner labels`,
|
|
@@ -129,15 +129,6 @@ async function markJobExecutionRunningAndEnqueue(
|
|
|
129
129
|
);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
const {newVersion: runningVersion, status} = await setJobExecutionStatus({
|
|
133
|
-
jobExecutionId: input.jobExecutionId,
|
|
134
|
-
status: 'running',
|
|
135
|
-
version: input.executionVersion,
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const start = jobExecutionStartOutcome({newVersion: runningVersion, status});
|
|
139
|
-
if (start.kind === 'terminal') return start;
|
|
140
|
-
|
|
141
132
|
await enqueueJobExecutionForRunner({
|
|
142
133
|
workspaceId: input.workspaceId,
|
|
143
134
|
workflowRunId: input.workflowRunId,
|
|
@@ -147,30 +138,59 @@ async function markJobExecutionRunningAndEnqueue(
|
|
|
147
138
|
projectId: input.projectId,
|
|
148
139
|
requiredLabels: input.requiredLabels,
|
|
149
140
|
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function markJobExecutionRunning(
|
|
144
|
+
input: JobExecutionOrchestrationInput,
|
|
145
|
+
): Promise<
|
|
146
|
+
| {kind: 'running'; runningVersion: number}
|
|
147
|
+
| {kind: 'terminal'; result: JobExecutionOrchestrationResult}
|
|
148
|
+
> {
|
|
149
|
+
const {newVersion: runningVersion, status} = await setJobExecutionStatus({
|
|
150
|
+
jobExecutionId: input.jobExecutionId,
|
|
151
|
+
status: 'running',
|
|
152
|
+
version: input.executionVersion,
|
|
153
|
+
});
|
|
150
154
|
|
|
155
|
+
const start = jobExecutionStartOutcome({newVersion: runningVersion, status});
|
|
156
|
+
if (start.kind === 'terminal') return start;
|
|
151
157
|
return {kind: 'running', runningVersion};
|
|
152
158
|
}
|
|
153
159
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
160
|
+
interface JobExecutionSignals extends JobExecutionOutcomeSignals {
|
|
161
|
+
claimed: JobClaimedSignalPayload | undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function registerJobExecutionSignalHandlers(
|
|
157
165
|
jobExecutionId: string,
|
|
158
|
-
|
|
159
|
-
):
|
|
160
|
-
let finished: {status: RuntimeCompletionStatus; jobExecutionId?: string | undefined} | undefined;
|
|
161
|
-
let leaseExpired = false;
|
|
166
|
+
signals: JobExecutionSignals,
|
|
167
|
+
): void {
|
|
162
168
|
setHandler(jobFinishedSignal, (payload) => {
|
|
163
169
|
if (payload.jobExecutionId !== undefined && payload.jobExecutionId !== jobExecutionId) return;
|
|
164
|
-
finished ??= payload;
|
|
170
|
+
signals.finished ??= payload;
|
|
165
171
|
});
|
|
166
172
|
setHandler(jobLeaseExpiredSignal, (payload = {}) => {
|
|
167
173
|
if (payload.jobExecutionId !== undefined && payload.jobExecutionId !== jobExecutionId) return;
|
|
168
|
-
leaseExpired = true;
|
|
174
|
+
signals.leaseExpired = true;
|
|
169
175
|
});
|
|
176
|
+
setHandler(jobClaimedSignal, (payload) => {
|
|
177
|
+
if (payload.jobExecutionId !== jobExecutionId) return;
|
|
178
|
+
signals.claimed ??= payload;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
170
181
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
182
|
+
async function waitForJobExecutionSignal(
|
|
183
|
+
signals: JobExecutionSignals,
|
|
184
|
+
deadline: number,
|
|
185
|
+
includeClaim: boolean,
|
|
186
|
+
): Promise<void> {
|
|
187
|
+
await condition(
|
|
188
|
+
() =>
|
|
189
|
+
signals.finished !== undefined ||
|
|
190
|
+
signals.leaseExpired ||
|
|
191
|
+
(includeClaim && signals.claimed !== undefined),
|
|
192
|
+
remainingMs(deadline) ?? 0,
|
|
193
|
+
);
|
|
174
194
|
}
|
|
175
195
|
|
|
176
196
|
interface JobExecutionResolution {
|
|
@@ -286,7 +306,41 @@ async function resolveTimedOutJobExecution({
|
|
|
286
306
|
export async function jobExecutionOrchestration(
|
|
287
307
|
input: JobExecutionOrchestrationInput,
|
|
288
308
|
): Promise<JobExecutionOrchestrationResult> {
|
|
289
|
-
const
|
|
309
|
+
const signals: JobExecutionSignals = {
|
|
310
|
+
finished: undefined,
|
|
311
|
+
leaseExpired: false,
|
|
312
|
+
claimed: undefined,
|
|
313
|
+
};
|
|
314
|
+
// Register every signal before enqueue can block or publish a claim/outcome event. The
|
|
315
|
+
// handlers retain signals that arrive while the enqueue activity is in flight.
|
|
316
|
+
registerJobExecutionSignalHandlers(input.jobExecutionId, signals);
|
|
317
|
+
await enqueueJobExecution(input);
|
|
318
|
+
|
|
319
|
+
const timeoutMs = input.executionTimeoutMs ?? DEFAULT_EXECUTION_MAX_DURATION_MS;
|
|
320
|
+
const deadline = Date.now() + timeoutMs;
|
|
321
|
+
await waitForJobExecutionSignal(signals, deadline, true);
|
|
322
|
+
|
|
323
|
+
// A terminal fact wins over claim even when both signals arrive before the condition
|
|
324
|
+
// resumes. This prevents a late claim from reopening an execution that already finished.
|
|
325
|
+
let resolution = resolveJobExecutionOutcomeSignal(signals);
|
|
326
|
+
if (resolution === 'finished') {
|
|
327
|
+
const {finished} = signals;
|
|
328
|
+
if (finished === undefined) throw new Error('Missing finished signal for finished resolution');
|
|
329
|
+
|
|
330
|
+
return resolveFinishedJobExecution({
|
|
331
|
+
input,
|
|
332
|
+
runningVersion: input.executionVersion,
|
|
333
|
+
status: finished.status,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
if (resolution === 'lease-expired') {
|
|
337
|
+
return resolveLeaseExpiredJobExecution({input, runningVersion: input.executionVersion});
|
|
338
|
+
}
|
|
339
|
+
if (!signals.claimed) {
|
|
340
|
+
return resolveTimedOutJobExecution({input, runningVersion: input.executionVersion});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const running = await markJobExecutionRunning(input);
|
|
290
344
|
if (running.kind === 'terminal') {
|
|
291
345
|
if (input.resolveJobStatus === false) {
|
|
292
346
|
return {status: running.result.status, jobVersion: input.jobVersion};
|
|
@@ -295,16 +349,11 @@ export async function jobExecutionOrchestration(
|
|
|
295
349
|
}
|
|
296
350
|
const {runningVersion} = running;
|
|
297
351
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
// Precedence ladder: a genuinely-finished job is never flipped to failed by a
|
|
302
|
-
// late lease-expiry, so `finished` wins over `leaseExpired`.
|
|
303
|
-
const resolution = resolveJobExecutionOutcomeSignal(signals);
|
|
352
|
+
await waitForJobExecutionSignal(signals, deadline, false);
|
|
353
|
+
resolution = resolveJobExecutionOutcomeSignal(signals);
|
|
304
354
|
if (resolution === 'finished') {
|
|
305
355
|
const {finished} = signals;
|
|
306
356
|
if (finished === undefined) throw new Error('Missing finished signal for finished resolution');
|
|
307
|
-
|
|
308
357
|
return resolveFinishedJobExecution({input, runningVersion, status: finished.status});
|
|
309
358
|
}
|
|
310
359
|
if (resolution === 'lease-expired') {
|
|
@@ -382,7 +382,7 @@ describe('runOrchestration', () => {
|
|
|
382
382
|
const runAttemptStatuses = setRunAttemptStatusCalls().map((c) => c.params.status);
|
|
383
383
|
expect(runAttemptStatuses).toEqual(['running', 'failed']);
|
|
384
384
|
|
|
385
|
-
// A, B, C enqueued
|
|
385
|
+
// A, B, C enqueued: D skipped because B failed
|
|
386
386
|
expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(3);
|
|
387
387
|
const jobStatuses = setJobStatusCalls().map((c) => ({
|
|
388
388
|
id: c.params.jobId,
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
ListenerBufferPeek,
|
|
10
10
|
} from '#db/job-listeners.js';
|
|
11
11
|
import type {RunDag} from '../activities/orchestration-activities.js';
|
|
12
|
-
import {JOB_FINISHED_SIGNAL, JOB_LEASE_EXPIRED_SIGNAL} from '../constants.js';
|
|
12
|
+
import {JOB_CLAIMED_SIGNAL, JOB_FINISHED_SIGNAL, JOB_LEASE_EXPIRED_SIGNAL} from '../constants.js';
|
|
13
13
|
|
|
14
14
|
const TASK_QUEUE = 'test-orchestration';
|
|
15
15
|
const WORKFLOWS_PATH = resolve(import.meta.dirname, '../../../dist/temporal/workflows/index.js');
|
|
@@ -17,7 +17,7 @@ const WORKFLOWS_PATH = resolve(import.meta.dirname, '../../../dist/temporal/work
|
|
|
17
17
|
export {TASK_QUEUE};
|
|
18
18
|
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
|
-
// Shared mutable test config
|
|
20
|
+
// Shared mutable test config: each test sets this before starting a workflow.
|
|
21
21
|
// Activities read from it via closure. Tests run serially within a describe.
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
23
23
|
|
|
@@ -29,8 +29,12 @@ export interface TestConfig {
|
|
|
29
29
|
enqueueError?: string;
|
|
30
30
|
/** If true, enqueueJobExecutionForRunner sends two job-finished signals (for dedup testing) */
|
|
31
31
|
duplicateSignal?: boolean;
|
|
32
|
-
/** If true, enqueueJobExecutionForRunner does nothing
|
|
32
|
+
/** If true, enqueueJobExecutionForRunner does nothing without a signal for timeout testing */
|
|
33
33
|
skipSignal?: boolean;
|
|
34
|
+
/** If true, emit the claim but hold back the terminal outcome (for deadline testing) */
|
|
35
|
+
skipOutcomeSignal?: boolean;
|
|
36
|
+
/** Runner-owned claim timestamp used by the mock claim event */
|
|
37
|
+
claimedAt?: string;
|
|
34
38
|
/** If true, signal job-lease-expired instead of job-finished */
|
|
35
39
|
signalLeaseExpired?: boolean;
|
|
36
40
|
/** If true, signal BOTH job-finished and job-lease-expired (precedence testing) */
|
|
@@ -73,6 +77,14 @@ export interface ActivityCall {
|
|
|
73
77
|
|
|
74
78
|
export let calls: ActivityCall[];
|
|
75
79
|
let versionSeq: number;
|
|
80
|
+
const pendingJobOutcomes = new Map<
|
|
81
|
+
string,
|
|
82
|
+
{
|
|
83
|
+
handle: ReturnType<typeof testEnv.client.workflow.getHandle>;
|
|
84
|
+
status: RuntimeCompletionStatus;
|
|
85
|
+
duplicateSignal: boolean;
|
|
86
|
+
}
|
|
87
|
+
>();
|
|
76
88
|
|
|
77
89
|
function nextVersion(): number {
|
|
78
90
|
return ++versionSeq;
|
|
@@ -81,6 +93,7 @@ function nextVersion(): number {
|
|
|
81
93
|
export function resetCalls(): void {
|
|
82
94
|
calls = [];
|
|
83
95
|
versionSeq = 0;
|
|
96
|
+
pendingJobOutcomes.clear();
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
export function callsNamed(name: string): ActivityCall[] {
|
|
@@ -192,7 +205,7 @@ export async function setupEnv(): Promise<void> {
|
|
|
192
205
|
activities: createMockActivities(),
|
|
193
206
|
});
|
|
194
207
|
|
|
195
|
-
// Run worker in background
|
|
208
|
+
// Run worker in background: it processes tasks until shutdown is called
|
|
196
209
|
workerRunPromise = worker.run();
|
|
197
210
|
workerRunPromise.catch(() => {
|
|
198
211
|
// Suppress unhandled rejection on shutdown
|
|
@@ -242,7 +255,7 @@ function createMockActivities() {
|
|
|
242
255
|
});
|
|
243
256
|
},
|
|
244
257
|
|
|
245
|
-
setJobExecutionStatus: (params: {
|
|
258
|
+
setJobExecutionStatus: async (params: {
|
|
246
259
|
jobExecutionId: string;
|
|
247
260
|
status: string;
|
|
248
261
|
version: number;
|
|
@@ -251,6 +264,24 @@ function createMockActivities() {
|
|
|
251
264
|
calls.push({name: 'setJobExecutionStatus', params});
|
|
252
265
|
const status =
|
|
253
266
|
params.status === 'running' && cfg.runningJobStatus ? cfg.runningJobStatus : params.status;
|
|
267
|
+
|
|
268
|
+
if (params.status === 'running') {
|
|
269
|
+
const outcome = pendingJobOutcomes.get(params.jobExecutionId);
|
|
270
|
+
pendingJobOutcomes.delete(params.jobExecutionId);
|
|
271
|
+
if (outcome && status === 'running') {
|
|
272
|
+
await outcome.handle.signal(JOB_FINISHED_SIGNAL, {
|
|
273
|
+
status: outcome.status,
|
|
274
|
+
jobExecutionId: params.jobExecutionId,
|
|
275
|
+
});
|
|
276
|
+
if (outcome.duplicateSignal) {
|
|
277
|
+
await outcome.handle.signal(JOB_FINISHED_SIGNAL, {
|
|
278
|
+
status: 'failed',
|
|
279
|
+
jobExecutionId: params.jobExecutionId,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
254
285
|
return {newVersion: nextVersion(), status};
|
|
255
286
|
},
|
|
256
287
|
|
|
@@ -285,6 +316,12 @@ function createMockActivities() {
|
|
|
285
316
|
|
|
286
317
|
const status = cfg.jobResults.get(params.jobId) ?? 'succeeded';
|
|
287
318
|
const handle = testEnv.client.workflow.getHandle(`job:${params.jobId}`);
|
|
319
|
+
await handle.signal(JOB_CLAIMED_SIGNAL, {
|
|
320
|
+
jobExecutionId: params.jobExecutionId,
|
|
321
|
+
claimedAt: cfg.claimedAt ?? '2026-06-22T10:05:00.000Z',
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
if (cfg.skipOutcomeSignal) return;
|
|
288
325
|
|
|
289
326
|
if (cfg.signalLeaseExpired) {
|
|
290
327
|
await handle.signal(JOB_LEASE_EXPIRED_SIGNAL, {jobExecutionId: params.jobExecutionId});
|
|
@@ -297,14 +334,14 @@ function createMockActivities() {
|
|
|
297
334
|
return;
|
|
298
335
|
}
|
|
299
336
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
337
|
+
// The terminal outbox is independent from the claim, but the mock delivers it from the
|
|
338
|
+
// pending → running activity. This gives the workflow a deterministic ordering boundary
|
|
339
|
+
// without leaving a polling promise alive after the test finishes.
|
|
340
|
+
pendingJobOutcomes.set(params.jobExecutionId, {
|
|
341
|
+
handle,
|
|
342
|
+
status,
|
|
343
|
+
duplicateSignal: cfg.duplicateSignal ?? false,
|
|
344
|
+
});
|
|
308
345
|
},
|
|
309
346
|
|
|
310
347
|
resolveLeaseExpiredJobExecutionActivity: (params: {
|
|
@@ -5,6 +5,7 @@ interface Project {
|
|
|
5
5
|
workspaceId: string;
|
|
6
6
|
sourceConnectionId: string;
|
|
7
7
|
sourceExternalRepositoryId: string;
|
|
8
|
+
sourceRepositoryOwner: string | null;
|
|
8
9
|
name: string;
|
|
9
10
|
createdAt: Date;
|
|
10
11
|
updatedAt: Date;
|
|
@@ -15,6 +16,7 @@ export const projectFactory = Factory.define<Project>(({sequence}) => ({
|
|
|
15
16
|
workspaceId: crypto.randomUUID(),
|
|
16
17
|
sourceConnectionId: crypto.randomUUID(),
|
|
17
18
|
sourceExternalRepositoryId: `acme/repo-${sequence}`,
|
|
19
|
+
sourceRepositoryOwner: 'acme',
|
|
18
20
|
name: `Project ${sequence}`,
|
|
19
21
|
createdAt: new Date(),
|
|
20
22
|
updatedAt: new Date(),
|