GeezerVison is showing github.com — fix(v2): derive the log and run status enums from the persisted statu… · simstudioai/sim@6931ea6
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: apps/docs/openapi-v2-logs.json +20-4Lines changed: 20 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 640 | 640 | }, | 641 | 641 | "status": { | 642 | 642 | "type": "string", | 643 | | - "enum": ["pending", "running", "redacting", "completed", "failed", "cancelled"], | 644 | | - "description": "Current execution status. `redacting` is transient while run output is scrubbed." | | 643 | + "enum": [ | | 644 | + "pending", | | 645 | + "running", | | 646 | + "paused", | | 647 | + "redacting", | | 648 | + "completed", | | 649 | + "failed", | | 650 | + "cancelled" | | 651 | + ], | | 652 | + "description": "Current execution status. `redacting` is transient while run output is scrubbed. `paused` is reported when a resume attempt did not run to completion and the run is waiting to be resumed again." | 645 | 653 | }, | 646 | 654 | "level": { | 647 | 655 | "type": "string", | | 1028 | 1036 | }, | 1029 | 1037 | "status": { | 1030 | 1038 | "type": "string", | 1031 | | - "enum": ["pending", "running", "redacting", "completed", "failed", "cancelled"], | 1032 | | - "description": "Current execution status. `redacting` is transient while run output is scrubbed." | | 1039 | + "enum": [ | | 1040 | + "pending", | | 1041 | + "running", | | 1042 | + "paused", | | 1043 | + "redacting", | | 1044 | + "completed", | | 1045 | + "failed", | | 1046 | + "cancelled" | | 1047 | + ], | | 1048 | + "description": "Current execution status. `redacting` is transient while run output is scrubbed. `paused` is reported when a resume attempt did not run to completion and the run is waiting to be resumed again." | 1033 | 1049 | }, | 1034 | 1050 | "level": { | 1035 | 1051 | "type": "string", | |
Collapse file Copy file name to clipboardExpand all lines: apps/docs/openapi-v2-workflows.json +5-5Lines changed: 5 additions & 5 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 3923 | 3923 | "enum": [ | 3924 | 3924 | "pending", | 3925 | 3925 | "running", | | 3926 | + "paused", | 3926 | 3927 | "redacting", | 3927 | 3928 | "completed", | 3928 | 3929 | "failed", | 3929 | | - "cancelled", | 3930 | | - "paused" | | 3930 | + "cancelled" | 3931 | 3931 | ], | 3932 | | - "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed." | | 3932 | + "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` is reported while the run is waiting at a human-in-the-loop pause point." | 3933 | 3933 | }, | 3934 | 3934 | "trigger": { | 3935 | 3935 | "type": "string", | | 4063 | 4063 | "enum": [ | 4064 | 4064 | "pending", | 4065 | 4065 | "running", | | 4066 | + "paused", | 4066 | 4067 | "redacting", | 4067 | 4068 | "completed", | 4068 | 4069 | "failed", | 4069 | 4070 | "cancelled", | 4070 | | - "paused", | 4071 | 4071 | "queued" | 4072 | 4072 | ], | 4073 | | - "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed." | | 4073 | + "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` is reported while the run is waiting at a human-in-the-loop pause point." | 4074 | 4074 | }, | 4075 | 4075 | "trigger": { | 4076 | 4076 | "anyOf": [ | |
Collapse file Copy file name to clipboardExpand all lines: apps/sim/app/api/v2/logs/[runId]/route.test.ts +15Lines changed: 15 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -94,6 +94,21 @@ describe('GET /api/v2/logs/[runId]', () => { | 94 | 94 | }) | 95 | 95 | }) | 96 | 96 |
| | 97 | + it('serves a run whose persisted status is paused', async () => { | | 98 | + mocks.execute.mockResolvedValue({ | | 99 | + log: { ...log, status: 'paused' }, | | 100 | + workflowFolderPath: '/agents', | | 101 | + executionData: { traceSpans: [], finalOutput: null }, | | 102 | + }) | | 103 | + | | 104 | + const response = await GET(new NextRequest('http://localhost:3000/api/v2/logs/run-1'), { | | 105 | + params: Promise.resolve({ runId: 'run-1' }), | | 106 | + }) | | 107 | + | | 108 | + expect(response.status).toBe(200) | | 109 | + expect((await response.json()).data).toMatchObject({ runId: 'run-1', status: 'paused' }) | | 110 | + }) | | 111 | + | 97 | 112 | it('conceals canonical workspace authorization as log not-found', async () => { | 98 | 113 | mocks.execute.mockRejectedValueOnce(new NoWorkspaceAccessError()) | 99 | 114 |
| |
Collapse file Copy file name to clipboardExpand all lines: apps/sim/app/api/v2/logs/route.test.ts +18Lines changed: 18 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -98,6 +98,24 @@ describe('GET /api/v2/logs', () => { | 98 | 98 | }) | 99 | 99 | }) | 100 | 100 |
| | 101 | + it('serves a run whose persisted status is paused', async () => { | | 102 | + mocks.execute.mockResolvedValue({ | | 103 | + items: [{ log: { ...log, status: 'paused' }, executionData: null }], | | 104 | + nextCursor: null, | | 105 | + includeFullDetails: false, | | 106 | + includeFinalOutput: false, | | 107 | + includeTraceSpans: false, | | 108 | + }) | | 109 | + | | 110 | + const response = await GET( | | 111 | + new NextRequest(`http://localhost:3000/api/v2/logs?workspaceId=${WORKSPACE_ID}`) | | 112 | + ) | | 113 | + const body = await response.json() | | 114 | + | | 115 | + expect(response.status).toBe(200) | | 116 | + expect(body.data[0]).toMatchObject({ runId: 'run-1', status: 'paused' }) | | 117 | + }) | | 118 | + | 101 | 119 | it('rejects malformed cursors after admission and before protected reads', async () => { | 102 | 120 | const response = await GET( | 103 | 121 | new NextRequest( | |
Collapse file Copy file name to clipboard +26Lines changed: 26 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +import { describe, expect, it } from 'vitest' | | 2 | +import { v2LogStatusSchema } from '@/lib/api/contracts/v2/logs' | | 3 | +import { PERSISTED_WORKFLOW_EXECUTION_STATUSES } from '@/lib/logs/types' | | 4 | + | | 5 | +/** | | 6 | + * Both log endpoints pass `workflow_execution_logs.status` through verbatim — unlike the | | 7 | + * run endpoints there is no `paused` overlay and no `queued` — so any drift between the | | 8 | + * reported enum and the persisted list 500s a whole page of results. | | 9 | + */ | | 10 | +describe('v2 log status schema', () => { | | 11 | + it('publishes exactly the persisted statuses', () => { | | 12 | + expect(v2LogStatusSchema.options).toEqual([ | | 13 | + 'pending', | | 14 | + 'running', | | 15 | + 'paused', | | 16 | + 'redacting', | | 17 | + 'completed', | | 18 | + 'failed', | | 19 | + 'cancelled', | | 20 | + ]) | | 21 | + }) | | 22 | + | | 23 | + it('stays derived from the persisted status list', () => { | | 24 | + expect(v2LogStatusSchema.options).toEqual([...PERSISTED_WORKFLOW_EXECUTION_STATUSES]) | | 25 | + }) | | 26 | +}) |
Collapse file Copy file name to clipboardExpand all lines: apps/sim/lib/api/contracts/v2/logs.ts +9-22Lines changed: 9 additions & 22 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -10,7 +10,7 @@ import { | 10 | 10 | v2FolderPathSchema, | 11 | 11 | v2TimestampSchema, | 12 | 12 | } from '@/lib/api/contracts/v2/shared' | 13 | | -import type { PersistedWorkflowExecutionStatus } from '@/lib/logs/types' | | 13 | +import { PERSISTED_WORKFLOW_EXECUTION_STATUSES } from '@/lib/logs/types' | 14 | 14 |
| 15 | 15 | /** | 16 | 16 | * v2 logs contracts. The query schemas are reused verbatim from v1 (the request | @@ -23,29 +23,16 @@ const v2LogCostSchema = z | 23 | 23 | .nullable() | 24 | 24 | .describe('Cost charged for the run, or null when unavailable.') | 25 | 25 | /** | 26 | | - * Every status the execution logger can persist, including the transient | 27 | | - * `redacting` state written while a finished run's output is scrubbed. The | 28 | | - * column is free text, so a value missing here fails the response parse and | 29 | | - * turns a single row into a 500 for the whole page. `_ExhaustiveLogStatus` | 30 | | - * makes a future addition to the persisted union a compile error instead. | | 26 | + * Both log endpoints pass `workflow_execution_logs.status` through verbatim, so the | | 27 | + * reported set is exactly the persisted set — a value missing here fails the response | | 28 | + * parse, and because list validation is whole-page one such row turns an entire page | | 29 | + * into a 500. | 31 | 30 | */ | 32 | | -const V2_LOG_STATUSES = [ | 33 | | - 'pending', | 34 | | - 'running', | 35 | | - 'redacting', | 36 | | - 'completed', | 37 | | - 'failed', | 38 | | - 'cancelled', | 39 | | -] as const satisfies readonly PersistedWorkflowExecutionStatus[] | 40 | | - | 41 | | -type AssertNever<T extends never> = T | 42 | | -type _ExhaustiveLogStatus = AssertNever< | 43 | | - Exclude<PersistedWorkflowExecutionStatus, (typeof V2_LOG_STATUSES)[number]> | 44 | | -> | 45 | | - | 46 | 31 | export const v2LogStatusSchema = z | 47 | | - .enum(V2_LOG_STATUSES) | 48 | | - .describe('Current execution status. `redacting` is transient while run output is scrubbed.') | | 32 | + .enum(PERSISTED_WORKFLOW_EXECUTION_STATUSES) | | 33 | + .describe( | | 34 | + 'Current execution status. `redacting` is transient while run output is scrubbed. `paused` is reported when a resume attempt did not run to completion and the run is waiting to be resumed again.' | | 35 | + ) | 49 | 36 |
| 50 | 37 | /** Execution `files` is a per-run jsonb array of attachment metadata. */ | 51 | 38 | const v2LogFilesSchema = z | |
Collapse file Copy file name to clipboardExpand all lines: apps/sim/lib/api/contracts/v2/workflow-run-status.test.ts +34-21Lines changed: 34 additions & 21 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -4,36 +4,49 @@ import { | 4 | 4 | v2WorkflowRunStatusFilterSchema, | 5 | 5 | v2WorkflowRunStatusValueSchema, | 6 | 6 | } from '@/lib/api/contracts/v2/workflows' | 7 | | -import type { PersistedWorkflowExecutionStatus } from '@/lib/logs/types' | | 7 | +import { PERSISTED_WORKFLOW_EXECUTION_STATUSES } from '@/lib/logs/types' | 8 | 8 |
| 9 | 9 | /** | 10 | | - * The runtime mirror of the persisted union. `satisfies` keeps it honest against | 11 | | - * `PersistedWorkflowExecutionStatus`, and the `AssertNever` gate in the contract keeps | 12 | | - * that union honest against the reported enums, so a status added to the execution logger | 13 | | - * fails compilation in both places before it can 500 a response parse. | | 10 | + * Both run endpoints report `workflow_execution_logs.status`, overlaid with `paused` from | | 11 | + * `paused_executions`, so every reported value lands in the persisted set. These tests | | 12 | + * guard the two ways that can break: the derivation being replaced by a hand-maintained | | 13 | + * list again, and a status being added to the persisted set without anyone confirming it | | 14 | + * belongs on the public wire (and regenerating the OpenAPI specs). | 14 | 15 | */ | 15 | | -const PERSISTED_STATUSES = [ | 16 | | - 'pending', | 17 | | - 'running', | 18 | | - 'redacting', | 19 | | - 'completed', | 20 | | - 'failed', | 21 | | - 'cancelled', | 22 | | -] as const satisfies readonly PersistedWorkflowExecutionStatus[] | 23 | | - | 24 | 16 | describe('v2 workflow run status schemas', () => { | 25 | | - it.each(PERSISTED_STATUSES)('reports the persisted status %s on both run endpoints', (status) => { | 26 | | - expect(v2WorkflowRunListStatusValueSchema.parse(status)).toBe(status) | 27 | | - expect(v2WorkflowRunStatusValueSchema.parse(status)).toBe(status) | | 17 | + it('publishes exactly the persisted statuses on the run list', () => { | | 18 | + expect(v2WorkflowRunListStatusValueSchema.options).toEqual([ | | 19 | + 'pending', | | 20 | + 'running', | | 21 | + 'paused', | | 22 | + 'redacting', | | 23 | + 'completed', | | 24 | + 'failed', | | 25 | + 'cancelled', | | 26 | + ]) | 28 | 27 | }) | 29 | 28 |
| 30 | | - it('reports the paused overlay on both run endpoints', () => { | 31 | | - expect(v2WorkflowRunListStatusValueSchema.parse('paused')).toBe('paused') | 32 | | - expect(v2WorkflowRunStatusValueSchema.parse('paused')).toBe('paused') | | 29 | + it('stays derived from the persisted status list', () => { | | 30 | + expect(v2WorkflowRunListStatusValueSchema.options).toEqual([ | | 31 | + ...PERSISTED_WORKFLOW_EXECUTION_STATUSES, | | 32 | + ]) | | 33 | + expect(v2WorkflowRunStatusValueSchema.options).toEqual([ | | 34 | + ...PERSISTED_WORKFLOW_EXECUTION_STATUSES, | | 35 | + 'queued', | | 36 | + ]) | 33 | 37 | }) | 34 | 38 |
| 35 | 39 | it('reports queued only where the job queue is consulted', () => { | 36 | | - expect(v2WorkflowRunStatusValueSchema.parse('queued')).toBe('queued') | | 40 | + expect(v2WorkflowRunStatusValueSchema.options).toEqual([ | | 41 | + 'pending', | | 42 | + 'running', | | 43 | + 'paused', | | 44 | + 'redacting', | | 45 | + 'completed', | | 46 | + 'failed', | | 47 | + 'cancelled', | | 48 | + 'queued', | | 49 | + ]) | 37 | 50 | expect(v2WorkflowRunListStatusValueSchema.safeParse('queued').success).toBe(false) | 38 | 51 | }) | 39 | 52 |
| |
Collapse file Copy file name to clipboardExpand all lines: apps/sim/lib/api/contracts/v2/workflows.ts +11-32Lines changed: 11 additions & 32 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -36,7 +36,7 @@ import { | 36 | 36 | workflowIdParamsSchema, | 37 | 37 | } from '@/lib/api/contracts/workflows' | 38 | 38 | import { MAX_WORKFLOW_EXECUTION_TIMEOUT_SECONDS } from '@/lib/billing/execution-timeout-defaults' | 39 | | -import type { PersistedWorkflowExecutionStatus } from '@/lib/logs/types' | | 39 | +import { PERSISTED_WORKFLOW_EXECUTION_STATUSES } from '@/lib/logs/types' | 40 | 40 |
| 41 | 41 | export const V2_WORKFLOW_RUN_ID_HEADER = 'X-Run-Id' | 42 | 42 |
| @@ -982,48 +982,27 @@ export const v2ResumeWorkflowContract = defineRouteContract({ | 982 | 982 | }, | 983 | 983 | }) | 984 | 984 |
| 985 | | -/** | 986 | | - * Every status the execution logger can persist into `workflow_execution_logs.status`, | 987 | | - * including the transient `redacting` state written while a finished run's output is | 988 | | - * scrubbed. The column is free text and both run endpoints pass it straight through, so | 989 | | - * a value missing here fails the response parse — and because list validation is | 990 | | - * whole-page, one such row turns an entire page into a 500. `_ExhaustiveRunStatus` makes | 991 | | - * a future addition to the persisted union a compile error instead. | 992 | | - */ | 993 | | -const V2_PERSISTED_RUN_STATUSES = [ | 994 | | - 'pending', | 995 | | - 'running', | 996 | | - 'redacting', | 997 | | - 'completed', | 998 | | - 'failed', | 999 | | - 'cancelled', | 1000 | | -] as const satisfies readonly PersistedWorkflowExecutionStatus[] | 1001 | | - | 1002 | | -type AssertNever<T extends never> = T | 1003 | | -type _ExhaustiveRunStatus = AssertNever< | 1004 | | - Exclude<PersistedWorkflowExecutionStatus, (typeof V2_PERSISTED_RUN_STATUSES)[number]> | 1005 | | -> | | 985 | +const RUN_STATUS_DESCRIPTION = | | 986 | + 'Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` is reported while the run is waiting at a human-in-the-loop pause point.' | 1006 | 987 |
| 1007 | 988 | /** | 1008 | | - * The list projection overlays `paused` onto the persisted status whenever the run has a | 1009 | | - * `paused` or `partially_resumed` row in `paused_executions`. It cannot report `queued`: | 1010 | | - * a run that is still only in the job queue has no log row to list. | | 989 | + * The list projection passes `workflow_execution_logs.status` through except where it | | 990 | + * overlays `paused` for a run holding a `paused` or `partially_resumed` row in | | 991 | + * `paused_executions`. Both branches land in the persisted set, so the reported enum is | | 992 | + * derived from it — a value missing here fails the response parse, and because list | | 993 | + * validation is whole-page one such row turns an entire page into a 500. `queued` is not | | 994 | + * reportable: a run still only in the job queue has no log row to list. | 1011 | 995 | */ | 1012 | | -const V2_WORKFLOW_RUN_LIST_STATUSES = [...V2_PERSISTED_RUN_STATUSES, 'paused'] as const | 1013 | | - | 1014 | | -const RUN_STATUS_DESCRIPTION = | 1015 | | - 'Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed.' | 1016 | | - | 1017 | 996 | export const v2WorkflowRunListStatusValueSchema = z | 1018 | | - .enum(V2_WORKFLOW_RUN_LIST_STATUSES) | | 997 | + .enum(PERSISTED_WORKFLOW_EXECUTION_STATUSES) | 1019 | 998 | .describe(RUN_STATUS_DESCRIPTION) | 1020 | 999 |
| 1021 | 1000 | /** | 1022 | 1001 | * The single-run read additionally consults the async job queue by deterministic job id, | 1023 | 1002 | * so a run accepted but not yet started reports `queued` rather than 404. | 1024 | 1003 | */ | 1025 | 1004 | export const v2WorkflowRunStatusValueSchema = z | 1026 | | - .enum([...V2_WORKFLOW_RUN_LIST_STATUSES, 'queued']) | | 1005 | + .enum([...PERSISTED_WORKFLOW_EXECUTION_STATUSES, 'queued']) | 1027 | 1006 | .describe(RUN_STATUS_DESCRIPTION) | 1028 | 1007 |
| 1029 | 1008 | /** | |
Collapse file Copy file name to clipboardExpand all lines: apps/sim/lib/logs/types.ts +24-6Lines changed: 24 additions & 6 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -204,13 +204,31 @@ export interface WorkflowExecutionLog { | 204 | 204 | createdAt: string | 205 | 205 | } | 206 | 206 |
| | 207 | +/** | | 208 | + * Every value written into `workflow_execution_logs.status`. The column is free text and | | 209 | + * one writer sets it through a raw `sql` CASE Drizzle cannot type-check, so this list — | | 210 | + * not the column type — is the only source of truth. API contracts that pass the column | | 211 | + * through derive their enums from it, so adding a status here widens the public wire; the | | 212 | + * contract tests fail until that widening is reviewed and the OpenAPI specs regenerated. | | 213 | + * | | 214 | + * `redacting` is transient while a finished run's output is scrubbed. `paused` is written | | 215 | + * only by `PauseResumeManager.markResumeAttemptFailed`, when a resume attempt does not run | | 216 | + * to completion — it failed admission, the run buffer was unavailable, the resume job could | | 217 | + * not be enqueued, or the attempt was cancelled. An ordinary human-in-the-loop pause | | 218 | + * persists `pending`. | | 219 | + */ | | 220 | +export const PERSISTED_WORKFLOW_EXECUTION_STATUSES = [ | | 221 | + 'pending', | | 222 | + 'running', | | 223 | + 'paused', | | 224 | + 'redacting', | | 225 | + 'completed', | | 226 | + 'failed', | | 227 | + 'cancelled', | | 228 | +] as const | | 229 | + | 207 | 230 | export type PersistedWorkflowExecutionStatus = | 208 | | - | 'running' | 209 | | - | 'pending' | 210 | | - | 'completed' | 211 | | - | 'failed' | 212 | | - | 'cancelled' | 213 | | - | 'redacting' | | 231 | + (typeof PERSISTED_WORKFLOW_EXECUTION_STATUSES)[number] | 214 | 232 |
| 215 | 233 | export interface CompletedWorkflowExecutionLog extends WorkflowExecutionLog { | 216 | 234 | persistedStatus: PersistedWorkflowExecutionStatus | |
Collapse file Copy file name to clipboardExpand all lines: packages/db/schema.ts +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -411,7 +411,7 @@ export const workflowExecutionLogs = pgTable( | 411 | 411 | ), | 412 | 412 |
| 413 | 413 | level: text('level').notNull(), // 'info' | 'error' | 414 | | - status: text('status').notNull().default('running'), // 'running' | 'pending' | 'completed' | 'failed' | 'cancelled' | | 414 | + status: text('status').notNull().default('running'), // see PERSISTED_WORKFLOW_EXECUTION_STATUSES in apps/sim/lib/logs/types.ts | 415 | 415 | trigger: text('trigger').notNull(), // 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' | 416 | 416 |
| 417 | 417 | startedAt: timestamp('started_at').notNull(), | |
|