Skip to content

Commit 25c1978

Browse files
fix(table): preserve waiting and resume provenance
1 parent 6b4d97f commit 25c1978

4 files changed

Lines changed: 108 additions & 10 deletions

File tree

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,16 @@ describe('resolveCellRender waiting precedence', () => {
7676
})
7777
).toEqual({ kind: 'value', text: 'kept workflow result' })
7878
})
79+
80+
it('shows Waiting for an empty ordinary workflow output with unmet dependencies', () => {
81+
expect(
82+
resolveCellRender({
83+
value: '',
84+
exec: undefined,
85+
column: COLUMN,
86+
waitingOnLabels: ['Dependency'],
87+
isEnrichmentOutput: false,
88+
})
89+
).toEqual({ kind: 'waiting', labels: ['Dependency'] })
90+
})
7991
})

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,17 @@ export function resolveCellRender({
7979
if (inFlight && blockRunning) return { kind: 'running' }
8080

8181
/**
82-
* A mapped-input edit can invalidate an earlier enrichment result without
83-
* deleting its stored output. Once no attempt is in flight, the actionable
84-
* Waiting state must win over that stale value. Active reruns still keep
85-
* showing the previous value until their replacement lands.
82+
* Empty workflow outputs show their unmet dependencies. Enrichment input
83+
* edits can additionally invalidate a result without deleting its stored
84+
* output, so Waiting must win over that stale value once no attempt is in
85+
* flight. Active reruns still show the previous value until replacement.
8686
*/
87-
if (isEnrichmentOutput && !inFlight && waitingOnLabels && waitingOnLabels.length > 0) {
87+
if (
88+
!inFlight &&
89+
(isEnrichmentOutput || isEmpty) &&
90+
waitingOnLabels &&
91+
waitingOnLabels.length > 0
92+
) {
8893
return { kind: 'waiting', labels: waitingOnLabels }
8994
}
9095

apps/sim/background/resume-execution.test.ts

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ const {
1313
mockSnapshotFromJson,
1414
mockCreateResumeAttemptTimeoutController,
1515
mockIsTimedOut,
16+
mockWithCascadeLock,
17+
mockGetRowById,
18+
mockGetTableById,
19+
mockCreateWorkflowCellProgressWriter,
20+
mockWriteWorkflowGroupState,
1621
} = vi.hoisted(() => ({
1722
mockTask: vi.fn((config) => config),
1823
mockGetPausedExecutionById: vi.fn(),
@@ -21,6 +26,11 @@ const {
2126
mockSnapshotFromJson: vi.fn(),
2227
mockCreateResumeAttemptTimeoutController: vi.fn(),
2328
mockIsTimedOut: vi.fn(() => false),
29+
mockWithCascadeLock: vi.fn(),
30+
mockGetRowById: vi.fn(),
31+
mockGetTableById: vi.fn(),
32+
mockCreateWorkflowCellProgressWriter: vi.fn(),
33+
mockWriteWorkflowGroupState: vi.fn(),
2434
}))
2535

2636
vi.mock('@trigger.dev/sdk', () => ({ task: mockTask, timeout: { None: 'none' } }))
@@ -29,9 +39,17 @@ vi.mock('@/lib/billing/core/billing-attribution', () => ({
2939
assertBillingAttributionSnapshot: vi.fn((value) => value),
3040
}))
3141

32-
vi.mock('@/lib/table/cascade-lock', () => ({ withCascadeLock: vi.fn() }))
42+
vi.mock('@/lib/table/cascade-lock', () => ({ withCascadeLock: mockWithCascadeLock }))
3343
vi.mock('@/lib/table/deps', () => ({ isExecCancelled: vi.fn(() => false) }))
3444

45+
vi.mock('@/lib/table/rows/service', () => ({ getRowById: mockGetRowById }))
46+
vi.mock('@/lib/table/service', () => ({ getTableById: mockGetTableById }))
47+
vi.mock('@/lib/table/cell-write', () => ({
48+
buildCancelledExecution: vi.fn(),
49+
createWorkflowCellProgressWriter: mockCreateWorkflowCellProgressWriter,
50+
writeWorkflowGroupState: mockWriteWorkflowGroupState,
51+
}))
52+
3553
vi.mock('@/lib/table/workflow-columns', () => ({
3654
findCellContextByExecutionId: mockFindCellContextByExecutionId,
3755
}))
@@ -70,7 +88,7 @@ const payload: ResumeExecutionPayload = {
7088
parentExecutionId: 'parent-execution-1',
7189
}
7290

73-
describe('executeResumeJob terminal errors', () => {
91+
describe('executeResumeJob', () => {
7492
beforeEach(() => {
7593
vi.clearAllMocks()
7694
mockGetPausedExecutionById.mockResolvedValue({
@@ -93,6 +111,21 @@ describe('executeResumeJob terminal errors', () => {
93111
})
94112
mockFindCellContextByExecutionId.mockResolvedValue(null)
95113
mockIsTimedOut.mockReturnValue(false)
114+
mockWithCascadeLock.mockImplementation(
115+
async (_tableId: string, _rowId: string, _ownerId: string, run: () => Promise<unknown>) => ({
116+
status: 'acquired',
117+
result: await run(),
118+
})
119+
)
120+
mockCreateWorkflowCellProgressWriter.mockReturnValue({
121+
onBlockComplete: vi.fn(),
122+
finish: vi.fn(),
123+
getBlockErrors: vi.fn(() => ({})),
124+
getPendingDataPatch: vi.fn(() => undefined),
125+
getEventOutputs: vi.fn(() => ({})),
126+
getPendingSecretProvenance: vi.fn(() => undefined),
127+
})
128+
mockWriteWorkflowGroupState.mockResolvedValue('wrote')
96129
})
97130

98131
it('rethrows the original core-finalized resume error', async () => {
@@ -155,4 +188,47 @@ describe('executeResumeJob terminal errors', () => {
155188
message: 'Execution timed out after 5 seconds',
156189
})
157190
})
191+
192+
it('preserves manual-run provenance across a paused cell resume', async () => {
193+
mockFindCellContextByExecutionId.mockResolvedValue({
194+
tableId: 'table-1',
195+
tableName: 'Table',
196+
rowId: 'row-1',
197+
groupId: 'group-1',
198+
workspaceId: 'workspace-1',
199+
workflowId: 'workflow-1',
200+
})
201+
mockGetRowById.mockResolvedValue({
202+
executions: {
203+
'group-1': {
204+
status: 'pending',
205+
executionId: payload.parentExecutionId,
206+
jobId: `paused-${payload.parentExecutionId}`,
207+
workflowId: 'workflow-1',
208+
isManualRun: true,
209+
error: null,
210+
},
211+
},
212+
})
213+
mockGetTableById.mockResolvedValue({
214+
schema: {
215+
columns: [],
216+
workflowGroups: [{ id: 'group-1', workflowId: 'workflow-1', outputs: [] }],
217+
},
218+
})
219+
mockStartResumeExecution.mockResolvedValue({
220+
success: true,
221+
status: 'paused',
222+
output: undefined,
223+
})
224+
225+
await executeResumeJob(payload)
226+
227+
expect(mockWriteWorkflowGroupState).toHaveBeenCalledWith(
228+
expect.objectContaining({ isManualRun: true }),
229+
expect.objectContaining({
230+
executionState: expect.objectContaining({ status: 'pending' }),
231+
})
232+
)
233+
})
158234
})

apps/sim/background/resume-execution.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export async function executeResumeJob(payload: ResumeExecutionPayload, signal?:
101101
// at the pause boundary.
102102
const { findCellContextByExecutionId } = await import('@/lib/table/workflow-columns')
103103
const cellContext = await findCellContextByExecutionId(parentExecutionId)
104+
let cellIsManualRun: boolean | undefined
104105

105106
// A paused/awaiting table cell that was cancelled by "Stop all" must not
106107
// resume — the cancel write is authoritative (matches the cell-write guard
@@ -113,7 +114,8 @@ export async function executeResumeJob(payload: ResumeExecutionPayload, signal?:
113114
cellContext.rowId,
114115
cellContext.workspaceId
115116
)
116-
if (isExecCancelled(cellRow?.executions?.[cellContext.groupId])) {
117+
const cellExecution = cellRow?.executions?.[cellContext.groupId]
118+
if (isExecCancelled(cellExecution)) {
117119
logger.info('Skipping resume — table cell cancelled', {
118120
tableId: cellContext.tableId,
119121
rowId: cellContext.rowId,
@@ -130,10 +132,11 @@ export async function executeResumeJob(payload: ResumeExecutionPayload, signal?:
130132
executedAt: new Date().toISOString(),
131133
}
132134
}
135+
cellIsManualRun = cellExecution?.isManualRun
133136
}
134137

135138
const writers = cellContext
136-
? await buildResumeCellWriters(cellContext, parentExecutionId)
139+
? await buildResumeCellWriters(cellContext, parentExecutionId, cellIsManualRun)
137140
: null
138141

139142
// No cell context → plain resume, no lock, no cascade continuation.
@@ -261,7 +264,8 @@ async function buildResumeCellWriters(
261264
groupId: string
262265
workflowId: string
263266
},
264-
parentExecutionId: string
267+
parentExecutionId: string,
268+
isManualRun: boolean | undefined
265269
): Promise<CellWriters | null> {
266270
const { getTableById } = await import('@/lib/table/service')
267271
const { buildCancelledExecution, createWorkflowCellProgressWriter, writeWorkflowGroupState } =
@@ -284,6 +288,7 @@ async function buildResumeCellWriters(
284288
workspaceId: cellContext.workspaceId,
285289
groupId: cellContext.groupId,
286290
executionId: parentExecutionId,
291+
isManualRun,
287292
requestId: `wfgrp-resume-${parentExecutionId}`,
288293
table,
289294
}

0 commit comments

Comments
 (0)