GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — fix(api): widen the cancel-execution reason contract to what the rout… · simstudioai/sim@a1a05c6

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • apps/sim
    • app/api/workflows/[id]/executions/[executionId]/cancel
      • route.test.ts
      • route.ts
    • lib
      • api/contracts
        • logs.ts
        • workflows.test.ts
        • workflows.ts
      • execution
        • cancel-workflow-execution.ts
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.test.ts‎

Copy file name to clipboardExpand all lines: apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.test.ts
+18-1Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,24 @@ vi.mock('@/lib/execution/event-buffer', () => ({
112112
}),
113113
}))
114114

115-
import { POST } from './route'
115+
import { cancelWorkflowExecutionContract } from '@/lib/api/contracts/workflows'
116+
import { POST as cancelExecution } from './route'
117+
118+
/**
119+
* Drives the route and validates every success body against the contract that
120+
* `requestJson` enforces on the client. The route builds its responses by hand
121+
* rather than through a declarative builder, so nothing else checks that the
122+
* two agree — and a `reason` the contract omits makes the client throw on a
123+
* cancellation that actually applied. Routing every case in this suite through
124+
* here covers each `NextResponse.json` shape the route can return.
125+
*/
126+
const POST = async (...args: Parameters<typeof cancelExecution>) => {
127+
const response = await cancelExecution(...args)
128+
if (response.status < 400) {
129+
cancelWorkflowExecutionContract.response.schema.parse(await response.clone().json())
130+
}
131+
return response
132+
}
116133

117134
const makeRequest = () =>
118135
new NextRequest('http://localhost/api/workflows/wf-1/executions/ex-1/cancel', {
Collapse file

‎apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.ts‎

Copy file name to clipboardExpand all lines: apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.ts
+23-8Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import { toError } from '@sim/utils/errors'
66
import { sleep } from '@sim/utils/helpers'
77
import { and, eq, inArray } from 'drizzle-orm'
88
import { type NextRequest, NextResponse } from 'next/server'
9-
import { cancelWorkflowExecutionContract } from '@/lib/api/contracts/workflows'
9+
import {
10+
type CancelWorkflowExecutionResponse,
11+
cancelWorkflowExecutionContract,
12+
} from '@/lib/api/contracts/workflows'
1013
import { parseRequest } from '@/lib/api/server'
1114
import { WORKSPACE_KEY_SCOPE_DENIED } from '@/lib/api-key/policy-messages'
1215
import { checkHybridAuth } from '@/lib/auth/hybrid'
@@ -34,6 +37,18 @@ const logger = createLogger('CancelExecutionAPI')
3437
const PAUSED_CANCELLATION_DB_ATTEMPTS = 3
3538
const PAUSED_CANCELLATION_DB_RETRY_MS = 200
3639

40+
/**
41+
* Builds the single success shape this route returns. The route hand-builds its
42+
* responses rather than going through a declarative builder, so nothing else
43+
* checks that they satisfy the contract the client validates against — and
44+
* several outcomes the route resolves itself (a still-queued run, an
45+
* already-cancelled run) ride on `success: true`, where a rejected body turns a
46+
* cancellation that worked into a client-side failure.
47+
*/
48+
function cancellationOutcome(body: CancelWorkflowExecutionResponse) {
49+
return NextResponse.json(body)
50+
}
51+
3752
async function cancelQueuedExecutionJobs(
3853
workflowId: string,
3954
executionId: string,
@@ -397,7 +412,7 @@ export const POST = withRouteHandler(
397412
workspaceId ? { groups: { workspace: workspaceId } } : undefined
398413
)
399414

400-
return NextResponse.json({
415+
return cancellationOutcome({
401416
success: true,
402417
executionId,
403418
redisAvailable: cancellation.reason !== 'redis_unavailable',
@@ -524,7 +539,7 @@ export const POST = withRouteHandler(
524539
const pausedReconciliationSucceeded =
525540
exactStopSatisfied &&
526541
(!hasPausedCancellation || (cancellationEventPublished && pausedCancelled))
527-
return NextResponse.json({
542+
return cancellationOutcome({
528543
success: pausedReconciliationSucceeded,
529544
executionId,
530545
redisAvailable: requiresCancellationEvent ? cancellationEventPublished : true,
@@ -597,7 +612,7 @@ export const POST = withRouteHandler(
597612
})
598613
})
599614
await clearStopSignalMarkers(stopSummary)
600-
return NextResponse.json({
615+
return cancellationOutcome({
601616
success: false,
602617
executionId,
603618
redisAvailable: stopSummary.cancellation.reason !== 'redis_unavailable',
@@ -658,7 +673,7 @@ export const POST = withRouteHandler(
658673
})
659674
})
660675
await clearStopSignalMarkers(stopSummary)
661-
return NextResponse.json({
676+
return cancellationOutcome({
662677
success: false,
663678
executionId,
664679
redisAvailable: stopSummary.cancellation.reason !== 'redis_unavailable',
@@ -669,7 +684,7 @@ export const POST = withRouteHandler(
669684
})
670685
}
671686
} else if (!effectivePausedCancellationPath) {
672-
return NextResponse.json({
687+
return cancellationOutcome({
673688
success: false,
674689
executionId,
675690
redisAvailable: stopSummary.cancellation.reason !== 'redis_unavailable',
@@ -801,7 +816,7 @@ export const POST = withRouteHandler(
801816
await PauseResumeManager.clearPausedCancellationIntent(executionId, workflowId)
802817
}
803818
}
804-
return NextResponse.json({
819+
return cancellationOutcome({
805820
success: false,
806821
executionId,
807822
redisAvailable: stopSummary.cancellation.reason !== 'redis_unavailable',
@@ -963,7 +978,7 @@ export const POST = withRouteHandler(
963978
? 'queue_cancelled'
964979
: stopSummary.cancellation.reason
965980

966-
return NextResponse.json({
981+
return cancellationOutcome({
967982
success,
968983
executionId,
969984
redisAvailable:
Collapse file

‎apps/sim/lib/api/contracts/logs.ts‎

Copy file name to clipboardExpand all lines: apps/sim/lib/api/contracts/logs.ts
-27Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { z } from 'zod'
22
import { userFileSchema } from '@/lib/api/contracts/primitives'
33
import { defineRouteContract } from '@/lib/api/contracts/types'
4-
import { cancelWorkflowExecutionReasonSchema } from '@/lib/api/contracts/workflows'
54

65
const comparisonOperatorSchema = z.enum(['=', '>', '<', '>=', '<=', '!='])
76

@@ -13,11 +12,6 @@ export const executionIdParamsSchema = z.object({
1312
executionId: z.string().min(1),
1413
})
1514

16-
export const cancelWorkflowExecutionParamsSchema = z.object({
17-
id: z.string().min(1, 'Invalid workflow ID'),
18-
executionId: z.string().min(1, 'Invalid execution ID'),
19-
})
20-
2115
const logFilterQuerySchema = z.object({
2216
workspaceId: z.string(),
2317
level: z.string().optional(),
@@ -353,21 +347,10 @@ export const triggersQuerySchema = z.object({
353347
})
354348
export type TriggersQuery = z.output<typeof triggersQuerySchema>
355349

356-
export const cancelWorkflowExecutionResponseSchema = z.object({
357-
success: z.boolean(),
358-
executionId: z.string(),
359-
redisAvailable: z.boolean(),
360-
durablyRecorded: z.boolean(),
361-
locallyAborted: z.boolean(),
362-
pausedCancelled: z.boolean(),
363-
reason: cancelWorkflowExecutionReasonSchema,
364-
})
365-
366350
export type SegmentStats = z.output<typeof segmentStatsSchema>
367351
export type WorkflowStats = z.output<typeof workflowStatsSchema>
368352
export type DashboardStatsResponse = z.output<typeof dashboardStatsResponseSchema>
369353
export type ExecutionSnapshotData = z.output<typeof executionSnapshotDataSchema>
370-
export type CancelWorkflowExecutionResponse = z.output<typeof cancelWorkflowExecutionResponseSchema>
371354

372355
export const listLogsContract = defineRouteContract({
373356
method: 'GET',
@@ -424,13 +407,3 @@ export const getExecutionSnapshotContract = defineRouteContract({
424407
schema: executionSnapshotDataSchema,
425408
},
426409
})
427-
428-
export const cancelWorkflowExecutionContract = defineRouteContract({
429-
method: 'POST',
430-
path: '/api/workflows/[id]/executions/[executionId]/cancel',
431-
params: cancelWorkflowExecutionParamsSchema,
432-
response: {
433-
mode: 'json',
434-
schema: cancelWorkflowExecutionResponseSchema,
435-
},
436-
})
Collapse file

‎apps/sim/lib/api/contracts/workflows.test.ts‎

Copy file name to clipboardExpand all lines: apps/sim/lib/api/contracts/workflows.test.ts
+21Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { describe, expect, it } from 'vitest'
22
import {
3+
cancelWorkflowExecutionReasonSchema,
34
executeWorkflowBodySchema,
45
getWorkflowResponseDataSchema,
6+
internalCancelWorkflowExecutionReasonSchema,
57
updateWorkflowBodySchema,
68
workflowListItemSchema,
79
} from '@/lib/api/contracts/workflows'
@@ -127,4 +129,23 @@ describe('workflow contracts', () => {
127129
expect(forkPolicySchema.parse({ forkSyncExcluded: true }).forkSyncExcluded).toBe(true)
128130
expect(forkPolicySchema.parse({}).forkSyncExcluded).toBe(false)
129131
})
132+
133+
/**
134+
* The v2 cancel endpoint presents `cancelWorkflowRun`'s result unchanged, and
135+
* that use case delegates wholly to the cancellation service — so it cannot
136+
* emit the outcomes the internal route resolves for itself. Folding those into
137+
* the service enum would publish four reasons v2 never returns, because the
138+
* v2 contract documents this enum value by value.
139+
*/
140+
it('keeps internal-only cancellation reasons out of the enum v2 publishes', () => {
141+
for (const reason of [
142+
'queue_cancelled',
143+
'already_cancelled',
144+
'active_resume_signal_failed',
145+
'cancellation_not_finalized',
146+
]) {
147+
expect(internalCancelWorkflowExecutionReasonSchema.options).toContain(reason)
148+
expect(cancelWorkflowExecutionReasonSchema.options).not.toContain(reason)
149+
}
150+
})
130151
})
Collapse file

‎apps/sim/lib/api/contracts/workflows.ts‎

Copy file name to clipboardExpand all lines: apps/sim/lib/api/contracts/workflows.ts
+25-6Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -661,11 +661,12 @@ export const workflowExecutionStatusQuerySchema = z.object({
661661
})
662662

663663
/**
664-
* Full cancellation-outcome vocabulary — mirrors
665-
* `CancelWorkflowExecutionReason` in `lib/execution/cancel-workflow-execution`
666-
* (contracts stay import-clean of server modules). The paused-HITL path emits
667-
* the two `paused_*` values; a narrower copy of this enum previously lived in
668-
* `contracts/logs.ts` and made the client reject those responses.
664+
* Cancellation outcomes produced by the cancellation service, and so the whole
665+
* vocabulary the public v2 endpoint can return — `cancelWorkflowRun` delegates
666+
* its outcome to that service. Mirrors `CancelWorkflowExecutionReason` in
667+
* `lib/execution/cancel-workflow-execution` (contracts stay import-clean of
668+
* server modules). Keeping the internal route's extra outcomes out of here is
669+
* what stops the published v2 schema advertising reasons v2 cannot emit.
669670
*/
670671
export const cancelWorkflowExecutionReasonSchema = z.enum([
671672
'recorded',
@@ -675,16 +676,34 @@ export const cancelWorkflowExecutionReasonSchema = z.enum([
675676
'paused_database_cancel_failed',
676677
])
677678

679+
/**
680+
* The internal route's vocabulary. It resolves four outcomes before the service
681+
* is ever reached: `queue_cancelled` (the run was still queued, so no execution
682+
* log row existed), `already_cancelled` (reconciling a run already cancelled),
683+
* and the two stop-signal failures. Several ride on `success: true` responses,
684+
* so validating them against the service enum makes `requestJson` reject
685+
* cancellations that genuinely applied.
686+
*/
687+
export const internalCancelWorkflowExecutionReasonSchema = z.enum([
688+
...cancelWorkflowExecutionReasonSchema.options,
689+
'queue_cancelled',
690+
'already_cancelled',
691+
'active_resume_signal_failed',
692+
'cancellation_not_finalized',
693+
])
694+
678695
const cancelWorkflowExecutionResponseSchema = z.object({
679696
success: z.boolean(),
680697
executionId: z.string(),
681698
redisAvailable: z.boolean(),
682699
durablyRecorded: z.boolean(),
683700
locallyAborted: z.boolean(),
684701
pausedCancelled: z.boolean(),
685-
reason: cancelWorkflowExecutionReasonSchema.optional(),
702+
reason: internalCancelWorkflowExecutionReasonSchema.optional(),
686703
})
687704

705+
export type CancelWorkflowExecutionResponse = z.output<typeof cancelWorkflowExecutionResponseSchema>
706+
688707
const resumeWorkflowExecutionContextResponseSchema = z
689708
.object({
690709
status: z.enum(['queued', 'started']).optional(),
Collapse file

‎apps/sim/lib/execution/cancel-workflow-execution.ts‎

Copy file name to clipboardExpand all lines: apps/sim/lib/execution/cancel-workflow-execution.ts
+5-2Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,12 @@ async function cancelActiveWorkflowJob(executionId: string): Promise<boolean> {
4141
}
4242

4343
/**
44-
* Cancellation outcome vocabulary. `recorded`/`redis_unavailable`/
44+
* Cancellation outcome vocabulary produced by this service, and so the whole
45+
* vocabulary the public v2 endpoint can return. `recorded`/`redis_unavailable`/
4546
* `redis_write_failed` come from the Redis record step; the two `paused_*`
46-
* values from the paused-HITL path.
47+
* values from the paused-HITL path. The internal cancel route resolves further
48+
* outcomes on top of these — see `internalCancelWorkflowExecutionReasonSchema`
49+
* in `lib/api/contracts/workflows`.
4750
*/
4851
export type CancelWorkflowExecutionReason =
4952
| 'recorded'

0 commit comments

Comments
0 (0)
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.