|
3 | 3 | * PostgreSQL: the pause's reviewer content is stored and read back with the run's resolved |
4 | 4 | * secret redacted, and the run detail withholds that content on the same terms as the run's |
5 | 5 | * output, while the pause's input fields stay readable. The workspace's paused-run list shows |
6 | | - * each point on exactly the same terms. |
| 6 | + * each point on exactly the same terms. A run whose resumed execution failed reads failed, not |
| 7 | + * paused, once no point is left to resume. |
7 | 8 | */ |
8 | 9 | import { db } from '@sim/db' |
9 | 10 | import { |
@@ -32,6 +33,7 @@ import { encryptSecret } from '@/lib/core/security/encryption' |
32 | 33 | import { executeWorkflowOperation } from '@/lib/workflows/application/execute-workflow' |
33 | 34 | import { listPausedRuns } from '@/lib/workflows/application/list-paused-runs' |
34 | 35 | import { readWorkflowRun } from '@/lib/workflows/application/read-workflow-run' |
| 36 | +import { listWorkflowExecutions } from '@/lib/workflows/executor/execution-queries' |
35 | 37 | import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager' |
36 | 38 | import type { PausePoint } from '@/executor/types' |
37 | 39 |
|
@@ -475,3 +477,55 @@ describe("the workspace's paused-run list", () => { |
475 | 477 | expect(second.runs[0].pausedAt.getTime()).toBeLessThanOrEqual(first.runs[0].pausedAt.getTime()) |
476 | 478 | }) |
477 | 479 | }) |
| 480 | + |
| 481 | +describe('a run whose resumed execution failed', () => { |
| 482 | + /** What a failed resume leaves behind: the log failed, its point failed, the record still open. */ |
| 483 | + async function settleAsFailed(runId: string) { |
| 484 | + const [row] = await db |
| 485 | + .select({ id: pausedExecutions.id, pausePoints: pausedExecutions.pausePoints }) |
| 486 | + .from(pausedExecutions) |
| 487 | + .where(eq(pausedExecutions.executionId, runId)) |
| 488 | + const points = row.pausePoints as Record<string, PausePoint> |
| 489 | + await db |
| 490 | + .update(pausedExecutions) |
| 491 | + .set({ |
| 492 | + pausePoints: Object.fromEntries( |
| 493 | + Object.entries(points).map(([id, point]) => [id, { ...point, resumeStatus: 'failed' }]) |
| 494 | + ), |
| 495 | + }) |
| 496 | + .where(eq(pausedExecutions.id, row.id)) |
| 497 | + await db |
| 498 | + .update(workflowExecutionLogs) |
| 499 | + .set({ status: 'failed', level: 'error', endedAt: new Date() }) |
| 500 | + .where(eq(workflowExecutionLogs.executionId, runId)) |
| 501 | + } |
| 502 | + |
| 503 | + async function listedStatus(runId: string) { |
| 504 | + const rows = await listWorkflowExecutions({ workflowId, limit: 100, order: 'desc' }) |
| 505 | + return rows.data.find((row) => row.executionId === runId)?.status |
| 506 | + } |
| 507 | + |
| 508 | + it('reads failed in the run detail and the run list once no point can be resumed', async () => { |
| 509 | + const runId = await pausedRun() |
| 510 | + await settleAsFailed(runId) |
| 511 | + |
| 512 | + const run = await readRun(owner, runId, { includeOutput: false }) |
| 513 | + expect(run).toMatchObject({ status: 'failed', paused: null, pausePoints: null }) |
| 514 | + expect(await listedStatus(runId)).toBe('failed') |
| 515 | + }) |
| 516 | + |
| 517 | + it('still reads paused while a point is waiting, even though the log has finished', async () => { |
| 518 | + const runId = await pausedRun() |
| 519 | + await db |
| 520 | + .update(workflowExecutionLogs) |
| 521 | + .set({ status: 'failed', level: 'error', endedAt: new Date() }) |
| 522 | + .where(eq(workflowExecutionLogs.executionId, runId)) |
| 523 | + |
| 524 | + const run = await readRun(owner, runId, { includeOutput: false }) |
| 525 | + expect(run.status).toBe('paused') |
| 526 | + expect(run.pausePoints).toEqual([ |
| 527 | + expect.objectContaining({ contextId: 'approval', status: 'paused' }), |
| 528 | + ]) |
| 529 | + expect(await listedStatus(runId)).toBe('paused') |
| 530 | + }) |
| 531 | +}) |
0 commit comments