Skip to content

Commit 78e9c87

Browse files
committed
fix(workflows): preserve cancellation scope assertions
1 parent df833ef commit 78e9c87

9 files changed

Lines changed: 73 additions & 3 deletions

File tree

apps/sim/app/api/v2/workflows/[workflowId]/runs/[runId]/cancel/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const POST = defineV2JsonRoute({
1313
operation: workflowOperations.cancelRun,
1414
rateLimit: v2RateLimits.publicApi,
1515
errorPolicy: v2WorkflowErrorPolicies.cancelRun,
16-
mapInput: ({ params }) => ({ runId: params.runId }),
16+
mapInput: ({ params }) => ({ workflowId: params.workflowId, runId: params.runId }),
1717
useCase: cancelWorkflowRun,
1818
present: (result) => ({
1919
data: {

apps/sim/app/api/v2/workflows/[workflowId]/runs/[runId]/route.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ describe('v2 run detail and cancel adapters', () => {
359359
})
360360
expect(mocks.cancel).toHaveBeenCalledWith({
361361
principal,
362-
input: { runId: 'run-1' },
362+
input: { workflowId: 'workflow-1', runId: 'run-1' },
363363
request: expect.anything(),
364364
})
365365
expect(v2RouteMocks.operationRate).toHaveBeenCalledTimes(2)
@@ -417,7 +417,7 @@ describe('v2 run detail and cancel adapters', () => {
417417
expect(response.status).toBe(200)
418418
expect(mocks.cancel).toHaveBeenCalledWith({
419419
principal: personalPrincipal,
420-
input: { runId: 'run-1' },
420+
input: { workflowId: 'workflow-1', runId: 'run-1' },
421421
request: expect.anything(),
422422
})
423423
})

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ describe('POST /api/workflows/[id]/executions/[executionId]/cancel', () => {
6464
expect(mocks.cancel).toHaveBeenCalledWith({
6565
principal,
6666
input: {
67+
workflowId: 'workflow-1',
6768
runId: 'execution-1',
6869
abortSignal: expect.any(AbortSignal),
6970
},

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const POST = defineInternalJsonRoute({
1919
}),
2020
errorPolicy: internalWorkflowErrorPolicies.concealRunAuthorization,
2121
mapInput: ({ params }, { request }) => ({
22+
workflowId: params.id,
2223
runId: params.executionId,
2324
abortSignal: request.signal,
2425
}),

apps/sim/lib/copilot/tools/handlers/workflow/mutations.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ describe('workflow mutation Copilot adapters', () => {
198198
}),
199199
{
200200
runId: 'execution-1',
201+
assertedWorkspaceId: 'workspace-1',
201202
}
202203
)
203204
})

apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ export async function executeCancelWorkflowRun(
300300
)
301301
const result = await executeCopilotWorkflowUseCase(context, cancelWorkflowRun, {
302302
runId: executionId,
303+
assertedWorkspaceId: context.workspaceId,
303304
...(context.abortSignal ? { abortSignal: context.abortSignal } : {}),
304305
})
305306

apps/sim/lib/workflows/application/cancel-run.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import { resolveActiveWorkflowRunApplicationContext } from '@/lib/workflows/appl
1010
import { workflowOperations } from '@/lib/workflows/application/operations'
1111

1212
export interface CancelWorkflowRunInput {
13+
workflowId?: string
1314
runId: string
15+
assertedWorkspaceId?: string
1416
abortSignal?: AbortSignal
1517
}
1618

@@ -19,6 +21,8 @@ export const cancelWorkflowRun = defineAuthorizedWorkflowUseCase({
1921
resolveContext: ({ input }: { input: CancelWorkflowRunInput }) =>
2022
resolveActiveWorkflowRunApplicationContext({
2123
runId: input.runId,
24+
assertedWorkflowId: input.workflowId,
25+
assertedWorkspaceId: input.assertedWorkspaceId,
2226
}),
2327
async execute({ principal, context, input }) {
2428
const attribution = resolvePrincipalAttribution(principal, {

apps/sim/lib/workflows/application/context.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,47 @@ describe('workflow application contexts', () => {
144144
})
145145
})
146146

147+
it('resolves the canonical workflow from an execution ID without a workflow assertion', async () => {
148+
queueCanonicalBindings({ log: 'workflow-1' })
149+
queueTableRows(schemaMock.workflow, [
150+
{
151+
workflowId: 'workflow-1',
152+
workflow: { id: 'workflow-1', name: 'Canonical workflow' },
153+
workspaceId: 'workspace-1',
154+
},
155+
])
156+
157+
await expect(
158+
resolveActiveWorkflowRunApplicationContext({
159+
runId: 'run-1',
160+
assertedWorkspaceId: 'workspace-1',
161+
})
162+
).resolves.toMatchObject({
163+
runId: 'run-1',
164+
workflowId: 'workflow-1',
165+
workspaceId: 'workspace-1',
166+
})
167+
})
168+
169+
it('conceals a workspace mismatch for an execution-only lookup', async () => {
170+
queueCanonicalBindings({ log: 'workflow-1' })
171+
queueTableRows(schemaMock.workflow, [
172+
{
173+
workflowId: 'workflow-1',
174+
workflow: { id: 'workflow-1', name: 'Canonical workflow' },
175+
workspaceId: 'workspace-1',
176+
},
177+
])
178+
179+
await expect(
180+
resolveActiveWorkflowRunApplicationContext({
181+
runId: 'run-1',
182+
assertedWorkspaceId: 'workspace-2',
183+
})
184+
).rejects.toMatchObject({ code: 'not_found', message: 'Workflow not found' })
185+
expect(mocks.loadWorkspace).not.toHaveBeenCalled()
186+
})
187+
147188
it('binds live execution authority to the deployment version stored on its durable log', async () => {
148189
queueTableRows(schemaMock.workflowExecutionLogs, [
149190
{

apps/sim/lib/workflows/application/workflow-run-control.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,27 @@ describe('workflow run-control application use cases', () => {
140140
}
141141
)
142142

143+
it('derives cancellation workflow scope from the execution ID when no workflow is asserted', async () => {
144+
const result = await cancelWorkflowRun.execute({
145+
principal: principals[0].principal,
146+
input: { runId: 'parent-run-1', assertedWorkspaceId: 'workspace-1' },
147+
})
148+
149+
expect(mocks.resolveRunContext).toHaveBeenCalledWith({
150+
runId: 'parent-run-1',
151+
assertedWorkflowId: undefined,
152+
assertedWorkspaceId: 'workspace-1',
153+
})
154+
expect(mocks.cancel).toHaveBeenCalledWith(
155+
expect.objectContaining({
156+
executionId: 'parent-run-1',
157+
workflowId: 'workflow-1',
158+
workspaceId: 'workspace-1',
159+
})
160+
)
161+
expect(result).toMatchObject({ workflowId: 'workflow-1', workspaceId: 'workspace-1' })
162+
})
163+
143164
it.each(principals)(
144165
'authorizes $principal.kind resume and preserves the parent/new run distinction',
145166
async ({ principal, actorUserId }) => {

0 commit comments

Comments
 (0)