Skip to content

Commit dd9748f

Browse files
fix(custom-blocks): surface a cancelled child as cancelled, not a generic failure
1 parent 3d2cac2 commit dd9748f

3 files changed

Lines changed: 38 additions & 4 deletions

File tree

apps/sim/executor/errors/boundary.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export type CustomBlockErrorType =
1414
* the exhausted limit is their org's, not a foreign publisher's.
1515
*/
1616
| 'usage_limit'
17+
/** The child run was cancelled (the invoking run aborted or was cancelled). */
18+
| 'cancelled'
1719
| 'execution_failed'
1820

1921
/** What a failed custom block tells its consumer. Leaks nothing about the source run. */

apps/sim/executor/handlers/workflow/workflow-handler.test.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,17 +1160,35 @@ describe('WorkflowBlockHandler', () => {
11601160
})
11611161

11621162
it('records a cancelled child through the cancellation path', async () => {
1163-
// Cancellation is reported on `ExecutionResult.status`, not on metadata.
1163+
// Production shape: the engine reports cancellation as `success: false`
1164+
// plus `status: 'cancelled'` on the ExecutionResult (never on metadata).
11641165
mockExecutorExecute.mockResolvedValue({
1165-
success: true,
1166+
success: false,
11661167
output: {},
11671168
status: 'cancelled',
11681169
})
11691170

1170-
await handler.execute(customBlockContext(), customBlock(), {})
1171+
await handler.execute(customBlockContext(), customBlock(), {}).catch(() => {})
11711172

11721173
expect(mockSafeCompleteWithCancellation).toHaveBeenCalledTimes(1)
11731174
expect(mockSafeComplete).not.toHaveBeenCalled()
1175+
// Already finalized as cancelled — must not be re-completed as an error.
1176+
expect(mockSafeCompleteWithError).not.toHaveBeenCalled()
1177+
})
1178+
1179+
it('tells the consumer a cancellation was a cancellation, not a generic failure', async () => {
1180+
mockExecutorExecute.mockResolvedValue({
1181+
success: false,
1182+
output: {},
1183+
status: 'cancelled',
1184+
})
1185+
1186+
const error = await handler
1187+
.execute(customBlockContext(), customBlock(), {})
1188+
.catch((e: any) => e)
1189+
1190+
expect(error.consumerFacing.errorType).toBe('cancelled')
1191+
expect(error.message).toBe('Custom block execution was cancelled')
11741192
})
11751193

11761194
it('records the real failure on the child log and hides it from the consumer', async () => {

apps/sim/executor/handlers/workflow/workflow-handler.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,8 @@ export class WorkflowBlockHandler implements BlockHandler {
253253
let childExecutionId: string | undefined
254254
let childSession: LoggingSession | undefined
255255
let childSessionStarted = false
256+
/** Set once the child's session reached a terminal state, so the catch doesn't re-complete it. */
257+
let childSessionFinalized = false
256258
let childCancellation: { signal: AbortSignal; dispose: () => void } | undefined
257259
try {
258260
// A custom block runs the source's latest deployment; if the source has been
@@ -599,13 +601,25 @@ export class WorkflowBlockHandler implements BlockHandler {
599601

600602
if (childSession && childSessionStarted) {
601603
await this.finalizeChildSession(childSession, executionResult, duration, childWorkflowInput)
604+
childSessionFinalized = true
602605
}
603606

604607
logger.info(`Child workflow ${childWorkflowName} completed in ${Math.round(duration)}ms`, {
605608
success: executionResult.success,
606609
hasLogs: (executionResult.logs?.length ?? 0) > 0,
607610
})
608611

612+
// A cancelled run comes back as `success: false`, so without this it would
613+
// fall through to `mapChildOutputToParent` and reach the consumer as a
614+
// generic `execution_failed`. Classify it instead — the message names
615+
// nothing about the source, so it crosses the boundary verbatim.
616+
if (isCustomBlock && executionResult.status === 'cancelled') {
617+
throw new BoundarySafeError({
618+
errorType: 'cancelled',
619+
message: 'Custom block execution was cancelled',
620+
})
621+
}
622+
609623
// A custom block's spans never reach the parent — they belong to the child's
610624
// own log row in the source workspace — so don't build them here at all.
611625
const childTraceSpans = isCustomBlock
@@ -636,7 +650,7 @@ export class WorkflowBlockHandler implements BlockHandler {
636650

637651
// The child's own log row records the real failure in the source workspace,
638652
// so the publisher sees what the consumer deliberately cannot.
639-
if (childSession && childSessionStarted) {
653+
if (childSession && childSessionStarted && !childSessionFinalized) {
640654
await this.failChildSession(childSession, error)
641655
}
642656

0 commit comments

Comments
 (0)