Skip to content

Commit 0c79a18

Browse files
committed
Fix lint
1 parent 64b3472 commit 0c79a18

22 files changed

Lines changed: 717 additions & 367 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: 96 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',
@@ -875,6 +883,94 @@ describe('workflow execute async route', () => {
875883
expect(mockExecuteWorkflowCore).not.toHaveBeenCalled()
876884
})
877885

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

0 commit comments

Comments
 (0)