|
| 1 | +/** The failure message a browser-settled Copilot workflow tool reads back, against real PostgreSQL. */ |
| 2 | +import { db } from '@sim/db' |
| 3 | +import { user, workflow, workflowExecutionSnapshots, workspace } from '@sim/db/schema' |
| 4 | +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' |
| 5 | +import { generateId } from '@sim/utils/id' |
| 6 | +import { eq } from 'drizzle-orm' |
| 7 | +import { afterAll, beforeAll, describe, expect, it } from 'vitest' |
| 8 | +import { LoggingSession } from '@/lib/logs/execution/logging-session' |
| 9 | +import type { TraceSpan, WorkflowState } from '@/lib/logs/types' |
| 10 | +import { getTrustedWorkflowToolExecution } from '@/lib/workflows/executor/execution-state' |
| 11 | +import type { SerializableExecutionState } from '@/executor/execution/types' |
| 12 | +import { emptyResolvedSecretTraceProvenance } from '@/executor/utils/resolved-secret-trace-registry' |
| 13 | + |
| 14 | +const ids = { |
| 15 | + owner: `trusted-tool-failure-${generateId()}`, |
| 16 | + workspace: generateId(), |
| 17 | + workflow: generateId(), |
| 18 | +} |
| 19 | + |
| 20 | +const workflowState: WorkflowState = { |
| 21 | + blocks: { |
| 22 | + start: { |
| 23 | + id: 'start', |
| 24 | + type: 'starter', |
| 25 | + name: 'Start', |
| 26 | + position: { x: 0, y: 0 }, |
| 27 | + subBlocks: {}, |
| 28 | + outputs: {}, |
| 29 | + enabled: true, |
| 30 | + }, |
| 31 | + }, |
| 32 | + edges: [], |
| 33 | + loops: {}, |
| 34 | + parallels: {}, |
| 35 | +} |
| 36 | + |
| 37 | +function executionState(): SerializableExecutionState { |
| 38 | + return { |
| 39 | + blockStates: {}, |
| 40 | + executedBlocks: ['start'], |
| 41 | + blockLogs: [], |
| 42 | + decisions: { router: {}, condition: {} }, |
| 43 | + completedLoops: [], |
| 44 | + activeExecutionPath: [], |
| 45 | + resolvedSecretTraceProvenance: emptyResolvedSecretTraceProvenance(), |
| 46 | + } |
| 47 | +} |
| 48 | + |
| 49 | +/** Starts a run bound to a Copilot tool call, the way the browser settlement path launches one. */ |
| 50 | +async function startBoundRun() { |
| 51 | + const executionId = generateId() |
| 52 | + const toolCallId = generateId() |
| 53 | + const session = new LoggingSession(ids.workflow, executionId, 'manual', 'trusted-tool-failure') |
| 54 | + session.setTrustedExecutionCorrelation({ |
| 55 | + executionId, |
| 56 | + requestId: 'trusted-tool-failure', |
| 57 | + source: 'workflow', |
| 58 | + workflowId: ids.workflow, |
| 59 | + triggerType: 'copilot', |
| 60 | + copilotToolCallId: toolCallId, |
| 61 | + }) |
| 62 | + expect( |
| 63 | + await session.safeStart({ userId: ids.owner, workspaceId: ids.workspace, workflowState }) |
| 64 | + ).toBe(true) |
| 65 | + return { session, executionId, toolCallId } |
| 66 | +} |
| 67 | + |
| 68 | +function readBack(run: { executionId: string; toolCallId: string }) { |
| 69 | + return getTrustedWorkflowToolExecution(run.executionId, ids.workflow, run.toolCallId) |
| 70 | +} |
| 71 | + |
| 72 | +beforeAll(async () => { |
| 73 | + const now = new Date() |
| 74 | + await db.insert(user).values({ |
| 75 | + id: ids.owner, |
| 76 | + name: 'Trusted Tool Failure', |
| 77 | + email: `${ids.owner}@trusted-tool-failure.test`, |
| 78 | + emailVerified: true, |
| 79 | + createdAt: now, |
| 80 | + updatedAt: now, |
| 81 | + }) |
| 82 | + await insertWorkspaceFixture(db, { |
| 83 | + id: ids.workspace, |
| 84 | + name: 'Trusted Tool Failure', |
| 85 | + ownerId: ids.owner, |
| 86 | + billedAccountUserId: ids.owner, |
| 87 | + }) |
| 88 | + await db.insert(workflow).values({ |
| 89 | + id: ids.workflow, |
| 90 | + userId: ids.owner, |
| 91 | + workspaceId: ids.workspace, |
| 92 | + name: 'Trusted Tool Failure', |
| 93 | + lastSynced: now, |
| 94 | + createdAt: now, |
| 95 | + updatedAt: now, |
| 96 | + }) |
| 97 | +}, 30_000) |
| 98 | + |
| 99 | +afterAll(async () => { |
| 100 | + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspace)) |
| 101 | + await db |
| 102 | + .delete(workflowExecutionSnapshots) |
| 103 | + .where(eq(workflowExecutionSnapshots.workflowId, ids.workflow)) |
| 104 | + await db.delete(user).where(eq(user.id, ids.owner)) |
| 105 | +}) |
| 106 | + |
| 107 | +describe('trusted workflow tool execution failure message', () => { |
| 108 | + it('returns the message a run that threw was finalized with', async () => { |
| 109 | + const run = await startBoundRun() |
| 110 | + |
| 111 | + await run.session.safeCompleteWithError({ |
| 112 | + error: { message: 'Fetch Orders: Request failed with status 404' }, |
| 113 | + executionState: executionState(), |
| 114 | + }) |
| 115 | + |
| 116 | + await expect(readBack(run)).resolves.toMatchObject({ |
| 117 | + status: 'failed', |
| 118 | + contentAvailable: true, |
| 119 | + error: 'Fetch Orders: Request failed with status 404', |
| 120 | + }) |
| 121 | + }) |
| 122 | + |
| 123 | + it('returns no message for a run whose failed block left its own output', async () => { |
| 124 | + const run = await startBoundRun() |
| 125 | + const now = new Date().toISOString() |
| 126 | + const failedSpan: TraceSpan = { |
| 127 | + id: 'span-1', |
| 128 | + name: 'Check', |
| 129 | + type: 'function', |
| 130 | + duration: 1, |
| 131 | + startTime: now, |
| 132 | + endTime: now, |
| 133 | + status: 'error', |
| 134 | + output: { error: 'boom' }, |
| 135 | + } |
| 136 | + |
| 137 | + await run.session.safeComplete({ |
| 138 | + finalOutput: { error: 'a field the workflow returned' }, |
| 139 | + traceSpans: [failedSpan], |
| 140 | + executionState: executionState(), |
| 141 | + }) |
| 142 | + |
| 143 | + const result = await readBack(run) |
| 144 | + expect(result).toMatchObject({ status: 'failed', contentAvailable: true }) |
| 145 | + expect(result).not.toHaveProperty('error') |
| 146 | + }) |
| 147 | +}) |
0 commit comments