Skip to content

Commit e3d1226

Browse files
committed
Fix lint
1 parent 117fe31 commit e3d1226

23 files changed

Lines changed: 716 additions & 368 deletions

File tree

apps/sim/app/api/copilot/confirm/route.test.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ describe('Copilot Confirm API Route', () => {
459459
getAsyncToolCall.mockResolvedValue({
460460
...existingRow,
461461
toolName: 'run_workflow',
462-
args: { workflowId: 'workflow-1' },
462+
args: { workflowId: 'workflow-1', async: true },
463463
status: 'running',
464464
claimedBy: null,
465465
})
@@ -469,6 +469,7 @@ describe('Copilot Confirm API Route', () => {
469469
toolCallId: 'tool-call-123',
470470
status: 'error',
471471
message: 'untrusted client detail',
472+
data: { code: 'ASYNC_WORKFLOW_DEPLOYMENT_STALE' },
472473
})
473474
)
474475

@@ -477,14 +478,47 @@ describe('Copilot Confirm API Route', () => {
477478
expect(completeAsyncToolCall).toHaveBeenCalledWith({
478479
toolCallId: 'tool-call-123',
479480
status: 'failed',
480-
result: { success: false, workflowId: 'workflow-1' },
481-
error: 'Workflow execution failed.',
481+
result: {
482+
success: false,
483+
workflowId: 'workflow-1',
484+
code: 'ASYNC_WORKFLOW_DEPLOYMENT_STALE',
485+
error: 'Async execution requires the current workflow to match its deployed version',
486+
},
487+
error: 'Async execution requires the current workflow to match its deployed version',
482488
})
483489
expect(JSON.stringify(publishToolConfirmation.mock.calls)).not.toContain(
484490
'untrusted client detail'
485491
)
486492
})
487493

494+
it('discards an unknown async workflow preflight failure', async () => {
495+
getAsyncToolCall.mockResolvedValue({
496+
...existingRow,
497+
toolName: 'run_workflow',
498+
args: { workflowId: 'workflow-1', async: true },
499+
status: 'running',
500+
claimedBy: null,
501+
})
502+
503+
const response = await POST(
504+
createMockPostRequest({
505+
toolCallId: 'tool-call-123',
506+
status: 'error',
507+
message: 'untrusted client detail',
508+
data: { code: 'UNTRUSTED_CLIENT_CODE' },
509+
})
510+
)
511+
512+
expect(response.status).toBe(200)
513+
expect(completeAsyncToolCall).toHaveBeenCalledWith({
514+
toolCallId: 'tool-call-123',
515+
status: 'failed',
516+
result: { success: false, workflowId: 'workflow-1' },
517+
error: 'Workflow execution failed.',
518+
})
519+
expect(JSON.stringify(publishToolConfirmation.mock.calls)).not.toContain('untrusted')
520+
})
521+
488522
it('downgrades an unverifiable success from a stale client to a structural failure', async () => {
489523
getAsyncToolCall.mockResolvedValue({
490524
...existingRow,

apps/sim/app/api/copilot/confirm/route.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { isBrowserToolName } from '@sim/browser-protocol'
22
import { createLogger } from '@sim/logger'
33
import { isTerminalToolName } from '@sim/terminal-protocol'
44
import { getErrorMessage, toError } from '@sim/utils/errors'
5+
import { isPlainRecord } from '@sim/utils/object'
56
import { type NextRequest, NextResponse } from 'next/server'
67
import { copilotConfirmContract } from '@/lib/api/contracts/copilot'
78
import { parseRequest, validationErrorResponse } from '@/lib/api/server'
@@ -38,7 +39,9 @@ import {
3839
sealClientToolCompletion,
3940
} from '@/lib/copilot/request/tools/client-completion-seal.server'
4041
import {
42+
type AsyncWorkflowDeploymentError,
4143
createStructuralWorkflowToolCompletionData,
44+
getAsyncWorkflowDeploymentError,
4245
getWorkflowToolCompletionExecutionId,
4346
getWorkflowToolCompletionMessage,
4447
getWorkflowToolConfirmationStatus,
@@ -278,6 +281,7 @@ export const POST = withRouteHandler((req: NextRequest) => {
278281

279282
let effectiveStatus = status
280283
let executionId = submittedExecutionId
284+
let deploymentError: AsyncWorkflowDeploymentError | undefined
281285

282286
if (isWorkflowTool) {
283287
const claimedExecutionId = getClaimedWorkflowExecutionId(existing.claimedBy)
@@ -329,16 +333,28 @@ export const POST = withRouteHandler((req: NextRequest) => {
329333
} else {
330334
executionId = undefined
331335
}
336+
337+
if (
338+
effectiveStatus === ASYNC_TOOL_CONFIRMATION_STATUS.error &&
339+
executionId === undefined &&
340+
existing.toolName === 'run_workflow' &&
341+
isPlainRecord(existing.args) &&
342+
existing.args.async === true
343+
) {
344+
deploymentError = getAsyncWorkflowDeploymentError(data)
345+
}
332346
}
333347

334348
span.setAttribute(TraceAttr.ToolConfirmationStatus, effectiveStatus)
335349
const projected = isWorkflowTool
336350
? {
337-
message: getWorkflowToolCompletionMessage(effectiveStatus),
351+
message:
352+
deploymentError?.message ?? getWorkflowToolCompletionMessage(effectiveStatus),
338353
data: createStructuralWorkflowToolCompletionData(
339354
effectiveStatus,
340355
workflowId,
341-
executionId
356+
executionId,
357+
deploymentError
342358
),
343359
}
344360
: {

apps/sim/app/api/workflows/[id]/execute/route.async.test.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
2929
import { AsyncJobEnqueueError } from '@/lib/core/async-jobs/types'
3030
import { getRemainingExecutionMs } from '@/lib/core/execution-limits'
3131
import { INTERNAL_EXECUTION_DEADLINE_HEADER } from '@/lib/execution/execution-deadline-header'
32+
import { WORKFLOW_NOT_DEPLOYED_CODE } from '@/lib/execution/preprocessing'
3233
import {
3334
PRIVATE_SECRET_PROVENANCE_BUNDLE_V1,
3435
PRIVATE_SECRET_PROVENANCE_FIELD,
@@ -39,6 +40,7 @@ const {
3940
mockAssertBillingAttributionSnapshot,
4041
mockClaimExecutionId,
4142
mockClaimWorkflowToolExecution,
43+
mockCheckNeedsRedeployment,
4244
mockEnqueue,
4345
mockExecuteWorkflowJob,
4446
mockExecuteWorkflowCore,
@@ -67,6 +69,7 @@ const {
6769
}),
6870
mockClaimExecutionId: vi.fn(),
6971
mockClaimWorkflowToolExecution: vi.fn(),
72+
mockCheckNeedsRedeployment: vi.fn(),
7073
mockEnqueue: vi.fn().mockResolvedValue('job-123'),
7174
mockExecuteWorkflowJob: vi.fn(),
7275
mockExecuteWorkflowCore: vi.fn(),
@@ -118,6 +121,10 @@ vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)
118121

119122
vi.mock('@/lib/execution/preprocessing', () => executionPreprocessingMock)
120123

124+
vi.mock('@/app/api/workflows/utils', () => ({
125+
checkNeedsRedeployment: mockCheckNeedsRedeployment,
126+
}))
127+
121128
vi.mock('@/lib/workflows/persistence/utils', () => workflowsPersistenceUtilsMock)
122129

123130
vi.mock('@/lib/workflows/executor/execution-core', () => ({
@@ -415,6 +422,7 @@ describe('workflow execute async route', () => {
415422
toolCallId: 'copilot-tool-1',
416423
claimedBy: 'workflow:execution-123',
417424
})
425+
mockCheckNeedsRedeployment.mockResolvedValue(false)
418426
mockHasDurableExecutionOwner.mockResolvedValue(false)
419427
mockGetAsyncToolCall.mockReset().mockResolvedValue({
420428
toolCallId: 'copilot-tool-1',
@@ -873,6 +881,93 @@ describe('workflow execute async route', () => {
873881
expect(mockExecuteWorkflowCore).not.toHaveBeenCalled()
874882
})
875883

884+
it('queues a bound Copilot workflow execution asynchronously', async () => {
885+
const request = createBoundCopilotExecutionRequest({
886+
stream: false,
887+
triggerBlockId: 'trigger-async',
888+
})
889+
request.headers.set('X-Execution-Mode', 'async')
890+
891+
const response = await POST(request, {
892+
params: Promise.resolve({ id: 'workflow-1' }),
893+
})
894+
895+
expect(response.status).toBe(202)
896+
expect(mockClaimWorkflowToolExecution).toHaveBeenCalledWith('copilot-tool-1', 'execution-123')
897+
expect(mockPreprocessExecution).toHaveBeenCalledWith(
898+
expect.objectContaining({ checkDeployment: true, executionType: 'async' })
899+
)
900+
expect(loggingSessionMockFns.mockSetTrustedExecutionCorrelation).toHaveBeenCalledWith({
901+
executionId: 'execution-123',
902+
requestId: 'req-12345678',
903+
source: 'workflow',
904+
workflowId: 'workflow-1',
905+
triggerType: 'copilot',
906+
copilotToolCallId: 'copilot-tool-1',
907+
})
908+
expect(mockEnqueue).toHaveBeenCalledWith(
909+
'workflow-execution',
910+
expect.objectContaining({
911+
executionId: 'execution-123',
912+
triggerBlockId: 'trigger-async',
913+
correlation: expect.objectContaining({ copilotToolCallId: 'copilot-tool-1' }),
914+
}),
915+
expect.any(Object)
916+
)
917+
})
918+
919+
it('rejects a bound async run when the deployed workflow is stale', async () => {
920+
mockCheckNeedsRedeployment.mockResolvedValueOnce(true)
921+
const request = createBoundCopilotExecutionRequest({ stream: false })
922+
request.headers.set('X-Execution-Mode', 'async')
923+
924+
const response = await POST(request, {
925+
params: Promise.resolve({ id: 'workflow-1' }),
926+
})
927+
928+
expect(response.status).toBe(409)
929+
await expect(response.json()).resolves.toEqual({
930+
error: 'Async execution requires the current workflow to match its deployed version',
931+
code: 'ASYNC_WORKFLOW_DEPLOYMENT_STALE',
932+
})
933+
expect(mockClaimWorkflowToolExecution).toHaveBeenCalledWith('copilot-tool-1', 'execution-123')
934+
expect(mockReleaseExecutionSlot).toHaveBeenCalledWith('execution-123')
935+
expect(mockReleaseWorkflowToolExecutionClaim).toHaveBeenCalledWith(
936+
'copilot-tool-1',
937+
'execution-123'
938+
)
939+
expect(mockEnqueue).not.toHaveBeenCalled()
940+
})
941+
942+
it('rejects a bound async run when the workflow has not been deployed', async () => {
943+
mockPreprocessExecution.mockResolvedValueOnce({
944+
success: false,
945+
error: {
946+
message: 'Workflow is not deployed',
947+
statusCode: 403,
948+
code: WORKFLOW_NOT_DEPLOYED_CODE,
949+
},
950+
})
951+
const request = createBoundCopilotExecutionRequest({ stream: false })
952+
request.headers.set('X-Execution-Mode', 'async')
953+
954+
const response = await POST(request, {
955+
params: Promise.resolve({ id: 'workflow-1' }),
956+
})
957+
958+
expect(response.status).toBe(403)
959+
await expect(response.json()).resolves.toEqual({
960+
error: 'Async execution requires the workflow to be deployed first',
961+
code: 'ASYNC_WORKFLOW_DEPLOYMENT_MISSING',
962+
})
963+
expect(mockReleaseWorkflowToolExecutionClaim).toHaveBeenCalledWith(
964+
'copilot-tool-1',
965+
'execution-123'
966+
)
967+
expect(mockEnqueue).not.toHaveBeenCalled()
968+
expect(mockCheckNeedsRedeployment).not.toHaveBeenCalled()
969+
})
970+
876971
it.each([
877972
[
878973
'cancelled',

0 commit comments

Comments
 (0)