From f5f7b371569e2bc7aa3c06ca04f3818c5140304e Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 23 Oct 2026 00:33:00 -0700 Subject: [PATCH 1/3] fix(workflow-tests): keep the error of a mock that throws A mock that throws reaches the server as { runId, callId, error }. The reply schema tried the output branch first, and its optional `output` matched, stripping `error`: the block failed with "A mocked block must return an object" instead of the thrown message, so a workflow that branches on its error text behaved differently under test than live. The error branch is now tried first. --- apps/sim/lib/workflow-tests/protocol.test.ts | 28 ++++++++++++++++++++ apps/sim/lib/workflow-tests/protocol.ts | 7 ++++- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 apps/sim/lib/workflow-tests/protocol.test.ts diff --git a/apps/sim/lib/workflow-tests/protocol.test.ts b/apps/sim/lib/workflow-tests/protocol.test.ts new file mode 100644 index 00000000000..a8d22eea948 --- /dev/null +++ b/apps/sim/lib/workflow-tests/protocol.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest' +import { testReplyArgsSchema } from '@/lib/workflow-tests/protocol' + +describe('testReplyArgsSchema', () => { + it('keeps the error of a mock that threw', () => { + // The sandbox sends no `output` key for a thrown mock; an optional `output` must not claim it. + expect( + testReplyArgsSchema.parse({ runId: 'r', callId: 'c', error: '409 version conflict' }) + ).toEqual({ + runId: 'r', + callId: 'c', + error: '409 version conflict', + }) + }) + + it('keeps a mock output, including a falsy one', () => { + expect(testReplyArgsSchema.parse({ runId: 'r', callId: 'c', output: { ok: true } })).toEqual({ + runId: 'r', + callId: 'c', + output: { ok: true }, + }) + expect(testReplyArgsSchema.parse({ runId: 'r', callId: 'c', output: null })).toEqual({ + runId: 'r', + callId: 'c', + output: null, + }) + }) +}) diff --git a/apps/sim/lib/workflow-tests/protocol.ts b/apps/sim/lib/workflow-tests/protocol.ts index c8871149726..096fb506ca7 100644 --- a/apps/sim/lib/workflow-tests/protocol.ts +++ b/apps/sim/lib/workflow-tests/protocol.ts @@ -40,9 +40,14 @@ export const testStartArgsSchema = z.object({ targets: z.array(testTargetSchema).max(MAX_TARGETS), }) +/** + * A mock's answer: its output, or the error it threw. The error branch is tried first, because + * `output: z.unknown()` also accepts an object with no `output`, which would turn a thrown mock + * into an empty output and lose its message. + */ export const testReplyArgsSchema = z.union([ - z.object({ runId: z.string().min(1), callId: z.string().min(1), output: z.unknown() }), z.object({ runId: z.string().min(1), callId: z.string().min(1), error: z.string().max(10_000) }), + z.object({ runId: z.string().min(1), callId: z.string().min(1), output: z.unknown() }), ]) export const testNextArgsSchema = z.object({ runId: z.string().min(1) }) From 3294a9cffae0cfbe2b11d1aec266ba8ac186c261 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 23 Oct 2026 01:35:52 -0700 Subject: [PATCH 2/3] fix(workflow-tests): never read a mock error past the cap as an empty output An error longer than the 10,000-character cap failed the error branch and fell through to the output branch, which still read it as an empty output. The output branch is now strict, so such a reply is refused instead. --- apps/sim/lib/workflow-tests/protocol.test.ts | 5 +++++ apps/sim/lib/workflow-tests/protocol.ts | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/sim/lib/workflow-tests/protocol.test.ts b/apps/sim/lib/workflow-tests/protocol.test.ts index a8d22eea948..d1b3d665730 100644 --- a/apps/sim/lib/workflow-tests/protocol.test.ts +++ b/apps/sim/lib/workflow-tests/protocol.test.ts @@ -25,4 +25,9 @@ describe('testReplyArgsSchema', () => { output: null, }) }) + + it('never reads an error past the cap as an empty output', () => { + const reply = { runId: 'r', callId: 'c', error: 'x'.repeat(10_001) } + expect(testReplyArgsSchema.safeParse(reply).success).toBe(false) + }) }) diff --git a/apps/sim/lib/workflow-tests/protocol.ts b/apps/sim/lib/workflow-tests/protocol.ts index 096fb506ca7..b5618e2e870 100644 --- a/apps/sim/lib/workflow-tests/protocol.ts +++ b/apps/sim/lib/workflow-tests/protocol.ts @@ -47,7 +47,10 @@ export const testStartArgsSchema = z.object({ */ export const testReplyArgsSchema = z.union([ z.object({ runId: z.string().min(1), callId: z.string().min(1), error: z.string().max(10_000) }), - z.object({ runId: z.string().min(1), callId: z.string().min(1), output: z.unknown() }), + // Strict, so a reply carrying an `error` (one past the cap included) is never read as an output. + z + .object({ runId: z.string().min(1), callId: z.string().min(1), output: z.unknown() }) + .strict(), ]) export const testNextArgsSchema = z.object({ runId: z.string().min(1) }) From ae22a382861ba23c9ed56567047b66b915eb94e1 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 23 Oct 2026 01:48:22 -0700 Subject: [PATCH 3/3] test(workflow-tests): drop the case that restates the reply schema --- apps/sim/lib/workflow-tests/protocol.test.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/apps/sim/lib/workflow-tests/protocol.test.ts b/apps/sim/lib/workflow-tests/protocol.test.ts index d1b3d665730..72b23d6d016 100644 --- a/apps/sim/lib/workflow-tests/protocol.test.ts +++ b/apps/sim/lib/workflow-tests/protocol.test.ts @@ -13,19 +13,6 @@ describe('testReplyArgsSchema', () => { }) }) - it('keeps a mock output, including a falsy one', () => { - expect(testReplyArgsSchema.parse({ runId: 'r', callId: 'c', output: { ok: true } })).toEqual({ - runId: 'r', - callId: 'c', - output: { ok: true }, - }) - expect(testReplyArgsSchema.parse({ runId: 'r', callId: 'c', output: null })).toEqual({ - runId: 'r', - callId: 'c', - output: null, - }) - }) - it('never reads an error past the cap as an empty output', () => { const reply = { runId: 'r', callId: 'c', error: 'x'.repeat(10_001) } expect(testReplyArgsSchema.safeParse(reply).success).toBe(false)