Skip to content

Commit 41ea290

Browse files
fix(workflows): resume legacy execution snapshots
1 parent a377050 commit 41ea290

2 files changed

Lines changed: 107 additions & 4 deletions

File tree

apps/sim/executor/execution/snapshot.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ describe('ExecutionSnapshot', () => {
5454
expect(() =>
5555
ExecutionSnapshot.fromJSON(
5656
JSON.stringify({
57+
version: 1,
5758
metadata: { ...metadata, principal: { version: 99, principal: {} } },
5859
workflow: { blocks: [] },
5960
input: {},
@@ -70,6 +71,7 @@ describe('ExecutionSnapshot', () => {
7071
expect(() =>
7172
ExecutionSnapshot.fromJSON(
7273
JSON.stringify({
74+
version: 1,
7375
metadata: metadataWithoutPrincipal,
7476
workflow: { blocks: [] },
7577
input: {},
@@ -79,4 +81,58 @@ describe('ExecutionSnapshot', () => {
7981
)
8082
).toThrow('Execution snapshot metadata is missing its principal')
8183
})
84+
85+
it('restores the recorded session user from a legacy pause snapshot', () => {
86+
const { principal: _principal, ...legacyMetadata } = metadata
87+
const restored = ExecutionSnapshot.fromJSON(
88+
JSON.stringify({
89+
metadata: { ...legacyMetadata, sessionUserId: 'session-user-1' },
90+
workflow: { blocks: [] },
91+
input: {},
92+
workflowVariables: {},
93+
selectedOutputs: [],
94+
})
95+
)
96+
97+
expect(restored.metadata.principal).toEqual({
98+
kind: 'session',
99+
userId: 'session-user-1',
100+
sessionId: 'legacy-paused-execution',
101+
})
102+
})
103+
104+
it('restores actorless legacy pause snapshots as internal system executions', () => {
105+
const { principal: _principal, ...legacyMetadata } = metadata
106+
const restored = ExecutionSnapshot.fromJSON(
107+
JSON.stringify({
108+
metadata: legacyMetadata,
109+
workflow: { blocks: [] },
110+
input: {},
111+
workflowVariables: {},
112+
selectedOutputs: [],
113+
})
114+
)
115+
116+
expect(restored.metadata.principal).toEqual({
117+
kind: 'system',
118+
serviceId: 'internal',
119+
workspaceId: 'workspace-1',
120+
workflowId: 'workflow-1',
121+
})
122+
})
123+
124+
it('rejects unsupported execution snapshot versions', () => {
125+
expect(() =>
126+
ExecutionSnapshot.fromJSON(
127+
JSON.stringify({
128+
version: 2,
129+
metadata,
130+
workflow: { blocks: [] },
131+
input: {},
132+
workflowVariables: {},
133+
selectedOutputs: [],
134+
})
135+
)
136+
).toThrow('Unsupported execution snapshot version 2')
137+
})
82138
})

apps/sim/executor/execution/snapshot.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,43 @@
1-
import { parsePrincipal, serializePrincipal } from '@sim/auth/principal'
1+
import {
2+
parsePrincipal,
3+
serializePrincipal,
4+
type WorkflowExecutionPrincipal,
5+
} from '@sim/auth/principal'
26
import { normalizeStringArray } from '@/lib/core/utils/arrays'
37
import { normalizeWorkflowVariables } from '@/lib/core/utils/records'
48
import type { ExecutionMetadata, SerializableExecutionState } from '@/executor/execution/types'
59

10+
const EXECUTION_SNAPSHOT_VERSION = 1
11+
const LEGACY_PAUSE_SESSION_ID = 'legacy-paused-execution'
12+
13+
function requireLegacyMetadataString(
14+
metadata: Record<string, unknown>,
15+
field: 'workflowId' | 'workspaceId'
16+
): string {
17+
const value = metadata[field]
18+
if (typeof value !== 'string' || !value.trim()) {
19+
throw new Error(`Legacy execution snapshot metadata ${field} must be a non-empty string`)
20+
}
21+
return value
22+
}
23+
24+
/** Restores only identity that the pre-principal snapshot format recorded unambiguously. */
25+
function parseLegacyPrincipal(metadata: Record<string, unknown>): WorkflowExecutionPrincipal {
26+
const workflowId = requireLegacyMetadataString(metadata, 'workflowId')
27+
const workspaceId = requireLegacyMetadataString(metadata, 'workspaceId')
28+
if (metadata.sessionUserId !== undefined) {
29+
if (typeof metadata.sessionUserId !== 'string' || !metadata.sessionUserId.trim()) {
30+
throw new Error('Legacy execution snapshot metadata sessionUserId must be a non-empty string')
31+
}
32+
return {
33+
kind: 'session',
34+
userId: metadata.sessionUserId,
35+
sessionId: LEGACY_PAUSE_SESSION_ID,
36+
}
37+
}
38+
return { kind: 'system', serviceId: 'internal', workspaceId, workflowId }
39+
}
40+
641
export class ExecutionSnapshot {
742
public readonly metadata: ExecutionMetadata
843
public readonly workflow: any
@@ -29,6 +64,7 @@ export class ExecutionSnapshot {
2964

3065
toJSON(): string {
3166
return JSON.stringify({
67+
version: EXECUTION_SNAPSHOT_VERSION,
3268
metadata: {
3369
...this.metadata,
3470
principal: serializePrincipal(this.metadata.principal),
@@ -51,12 +87,23 @@ export class ExecutionSnapshot {
5187
throw new Error('Execution snapshot metadata must be an object')
5288
}
5389
const serializedMetadata = parsed.metadata as Record<string, unknown>
54-
if (serializedMetadata.principal === undefined) {
55-
throw new Error('Execution snapshot metadata is missing its principal')
90+
let principal: WorkflowExecutionPrincipal
91+
if (parsed.version === EXECUTION_SNAPSHOT_VERSION) {
92+
if (serializedMetadata.principal === undefined) {
93+
throw new Error('Execution snapshot metadata is missing its principal')
94+
}
95+
principal = parsePrincipal(serializedMetadata.principal)
96+
} else if (parsed.version === undefined) {
97+
if (serializedMetadata.principal !== undefined) {
98+
throw new Error('Unversioned execution snapshots cannot contain a principal')
99+
}
100+
principal = parseLegacyPrincipal(serializedMetadata)
101+
} else {
102+
throw new Error(`Unsupported execution snapshot version ${String(parsed.version)}`)
56103
}
57104
const metadata = {
58105
...serializedMetadata,
59-
principal: parsePrincipal(serializedMetadata.principal),
106+
principal,
60107
} as ExecutionMetadata
61108
return new ExecutionSnapshot(
62109
metadata,

0 commit comments

Comments
 (0)