Skip to content

Commit 9ac12a2

Browse files
authored
fix(workflow-tests): keep the error of a mock that throws (#9333)
* 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. * 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. * test(workflow-tests): drop the case that restates the reply schema
1 parent 9482fb3 commit 9ac12a2

2 files changed

Lines changed: 29 additions & 1 deletion

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { testReplyArgsSchema } from '@/lib/workflow-tests/protocol'
3+
4+
describe('testReplyArgsSchema', () => {
5+
it('keeps the error of a mock that threw', () => {
6+
// The sandbox sends no `output` key for a thrown mock; an optional `output` must not claim it.
7+
expect(
8+
testReplyArgsSchema.parse({ runId: 'r', callId: 'c', error: '409 version conflict' })
9+
).toEqual({
10+
runId: 'r',
11+
callId: 'c',
12+
error: '409 version conflict',
13+
})
14+
})
15+
16+
it('never reads an error past the cap as an empty output', () => {
17+
const reply = { runId: 'r', callId: 'c', error: 'x'.repeat(10_001) }
18+
expect(testReplyArgsSchema.safeParse(reply).success).toBe(false)
19+
})
20+
})

‎apps/sim/lib/workflow-tests/protocol.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,17 @@ export const testStartArgsSchema = z.object({
4040
targets: z.array(testTargetSchema).max(MAX_TARGETS),
4141
})
4242

43+
/**
44+
* A mock's answer: its output, or the error it threw. The error branch is tried first, because
45+
* `output: z.unknown()` also accepts an object with no `output`, which would turn a thrown mock
46+
* into an empty output and lose its message.
47+
*/
4348
export const testReplyArgsSchema = z.union([
44-
z.object({ runId: z.string().min(1), callId: z.string().min(1), output: z.unknown() }),
4549
z.object({ runId: z.string().min(1), callId: z.string().min(1), error: z.string().max(10_000) }),
50+
// Strict, so a reply carrying an `error` (one past the cap included) is never read as an output.
51+
z
52+
.object({ runId: z.string().min(1), callId: z.string().min(1), output: z.unknown() })
53+
.strict(),
4654
])
4755

4856
export const testNextArgsSchema = z.object({ runId: z.string().min(1) })

0 commit comments

Comments
 (0)