Skip to content

Commit 4f2deb3

Browse files
committed
fix(executor): name the exact field when a Function block reference holds a large value
1 parent 9703b07 commit 4f2deb3

2 files changed

Lines changed: 208 additions & 131 deletions

File tree

‎apps/sim/executor/variables/resolver.test.ts‎

Lines changed: 123 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,47 @@ async function evaluateResolvedCondition(
286286
}
287287
}
288288

289+
function createOffloadEnv(language: string, producerOutput: Record<string, unknown>) {
290+
const { block, ctx } = createResolver(language)
291+
const producer = createBlock('producer', 'Producer', BlockType.API)
292+
const state = new ExecutionState()
293+
state.setBlockOutput('producer', producerOutput)
294+
const workflow: SerializedWorkflow = {
295+
version: '1',
296+
blocks: [producer, block],
297+
connections: [],
298+
loops: {},
299+
parallels: {},
300+
}
301+
const resolver = new VariableResolver(workflow, {}, state)
302+
const durableCtx = {
303+
...ctx,
304+
blockStates: state.getBlockStates(),
305+
workspaceId: 'workspace-1',
306+
workflowId: 'workflow-1',
307+
executionId: 'execution-1',
308+
largeValueKeys: [] as string[],
309+
} as ExecutionContext
310+
return { block, resolver, durableCtx }
311+
}
312+
313+
function largeRef(size: number) {
314+
return {
315+
__simLargeValueRef: true,
316+
version: 1,
317+
id: 'lv_ABCDEFGHIJKL',
318+
kind: 'string',
319+
size,
320+
key: 'execution/workspace-1/workflow-1/execution-1/large-value-lv_ABCDEFGHIJKL.json',
321+
executionId: 'execution-1',
322+
}
323+
}
324+
325+
function resolveAgainstProducer(language: string, output: Record<string, unknown>, code: string) {
326+
const { block, resolver, durableCtx } = createOffloadEnv(language, output)
327+
return resolver.resolveInputsForFunctionBlock(durableCtx, 'function', { code }, block)
328+
}
329+
289330
describe('VariableResolver function block inputs', () => {
290331
it('inlines only structurally inert condition literals and defers the rest to the compiler', async () => {
291332
const { ctx, resolver } = createResolver()
@@ -924,116 +965,100 @@ describe('VariableResolver function block inputs', () => {
924965
})
925966

926967
it('fails whole large value refs for Function runtimes without lazy helpers', async () => {
927-
const { block, ctx } = createResolver('python')
928-
const state = new ExecutionState()
929-
state.setBlockOutput('producer', {
930-
result: {
931-
__simLargeValueRef: true,
932-
version: 1,
933-
id: 'lv_ABCDEFGHIJKL',
934-
kind: 'object',
935-
size: 12 * 1024 * 1024,
936-
key: 'execution/workspace-1/workflow-1/execution-1/large-value-lv_ABCDEFGHIJKL.json',
937-
executionId: 'execution-1',
938-
},
939-
})
940-
const workflow: SerializedWorkflow = {
941-
version: '1',
942-
blocks: [createBlock('producer', 'Producer', BlockType.API), block],
943-
connections: [],
944-
loops: {},
945-
parallels: {},
946-
}
947-
const largeResolver = new VariableResolver(workflow, {}, state)
948-
const largeCtx = {
949-
...ctx,
950-
blockStates: state.getBlockStates(),
951-
} as ExecutionContext
952-
953968
await expect(
954-
largeResolver.resolveInputsForFunctionBlock(
955-
largeCtx,
956-
'function',
957-
{ code: 'return <Producer.result>' },
958-
block
969+
resolveAgainstProducer(
970+
'python',
971+
{ result: largeRef(12 * 1024 * 1024) },
972+
'return <Producer.result>'
959973
)
960-
).rejects.toThrow('This execution value is too large to inline')
974+
).rejects.toThrow(
975+
'<Producer.result> is too large to pass inline (12.0 MB). Select a smaller field, or read it in JavaScript without imports.'
976+
)
961977
})
962978

963979
it('fails whole large value refs for JavaScript with imports', async () => {
964-
const { block, ctx } = createResolver('javascript')
965-
const state = new ExecutionState()
966-
state.setBlockOutput('producer', {
967-
result: {
968-
__simLargeValueRef: true,
969-
version: 1,
970-
id: 'lv_ABCDEFGHIJKL',
971-
kind: 'object',
972-
size: 12 * 1024 * 1024,
973-
key: 'execution/workspace-1/workflow-1/execution-1/large-value-lv_ABCDEFGHIJKL.json',
974-
executionId: 'execution-1',
975-
},
976-
})
977-
const workflow: SerializedWorkflow = {
978-
version: '1',
979-
blocks: [createBlock('producer', 'Producer', BlockType.API), block],
980-
connections: [],
981-
loops: {},
982-
parallels: {},
983-
}
984-
const largeResolver = new VariableResolver(workflow, {}, state)
985-
const largeCtx = {
986-
...ctx,
987-
blockStates: state.getBlockStates(),
988-
} as ExecutionContext
989-
990980
await expect(
991-
largeResolver.resolveInputsForFunctionBlock(
992-
largeCtx,
993-
'function',
994-
{ code: "import x from 'x'\nreturn <Producer.result>" },
995-
block
981+
resolveAgainstProducer(
982+
'javascript',
983+
{ result: largeRef(12 * 1024 * 1024) },
984+
"import x from 'x'\nreturn <Producer.result>"
996985
)
997-
).rejects.toThrow('This execution value is too large to inline')
986+
).rejects.toThrow(
987+
'<Producer.result> is too large to pass inline (12.0 MB). Select a smaller field, or read it in JavaScript without imports.'
988+
)
998989
})
999990

1000-
it('fails nested large value refs for JavaScript instead of leaking ref markers', async () => {
1001-
const { block, ctx } = createResolver('javascript')
1002-
const state = new ExecutionState()
1003-
state.setBlockOutput('producer', {
1004-
result: {
1005-
rows: {
1006-
__simLargeValueRef: true,
1007-
version: 1,
1008-
id: 'lv_ABCDEFGHIJKL',
1009-
kind: 'array',
1010-
size: 12 * 1024 * 1024,
1011-
key: 'execution/workspace-1/workflow-1/execution-1/large-value-lv_ABCDEFGHIJKL.json',
1012-
executionId: 'execution-1',
1013-
},
1014-
},
991+
describe('nested large values', () => {
992+
it('names the nested field instead of leaking ref markers', async () => {
993+
await expect(
994+
resolveAgainstProducer(
995+
'javascript',
996+
{ result: { rows: largeRef(12 * 1024 * 1024) } },
997+
'return <Producer.result>.rows.length'
998+
)
999+
).rejects.toThrow(
1000+
'<Producer.result> contains a 12.0 MB value at <Producer.result.rows>. Reference that field directly.'
1001+
)
10151002
})
1016-
const workflow: SerializedWorkflow = {
1017-
version: '1',
1018-
blocks: [createBlock('producer', 'Producer', BlockType.API), block],
1019-
connections: [],
1020-
loops: {},
1021-
parallels: {},
1022-
}
1023-
const largeResolver = new VariableResolver(workflow, {}, state)
1024-
const largeCtx = {
1025-
...ctx,
1026-
blockStates: state.getBlockStates(),
1027-
} as ExecutionContext
10281003

1029-
await expect(
1030-
largeResolver.resolveInputsForFunctionBlock(
1031-
largeCtx,
1032-
'function',
1033-
{ code: 'return <Producer.result>.rows.length' },
1034-
block
1004+
it('names the array index of a large value nested in an array', async () => {
1005+
await expect(
1006+
resolveAgainstProducer(
1007+
'javascript',
1008+
{ contents: [largeRef(10.5 * 1024 * 1024)] },
1009+
'return <Producer.contents>[0].slice(0, 10)'
1010+
)
1011+
).rejects.toThrow(
1012+
'<Producer.contents> contains a 10.5 MB value at <Producer.contents[0]>. Reference that field directly.'
10351013
)
1036-
).rejects.toThrow('This execution value contains nested large values')
1014+
})
1015+
1016+
it('names a nested array manifest without descending into its chunks', async () => {
1017+
const chunk = largeRef(9 * 1024 * 1024)
1018+
const manifest = {
1019+
__simLargeArrayManifest: true,
1020+
version: 2,
1021+
kind: 'array',
1022+
totalCount: 2,
1023+
chunkCount: 1,
1024+
byteSize: chunk.size,
1025+
chunks: [{ ref: chunk, count: 2, byteSize: chunk.size }],
1026+
preview: [],
1027+
}
1028+
await expect(
1029+
resolveAgainstProducer(
1030+
'javascript',
1031+
{ result: { items: manifest } },
1032+
'return <Producer.result>.items.length'
1033+
)
1034+
).rejects.toThrow(
1035+
'<Producer.result> contains a 9.0 MB value at <Producer.result.items>. Reference that field directly.'
1036+
)
1037+
})
1038+
1039+
it('does not suggest a direct reference where it could not be loaded either', async () => {
1040+
await expect(
1041+
resolveAgainstProducer(
1042+
'python',
1043+
{ contents: [largeRef(10.5 * 1024 * 1024)] },
1044+
'return <Producer.contents>'
1045+
)
1046+
).rejects.toThrow(
1047+
'<Producer.contents> contains a 10.5 MB value at <Producer.contents[0]>. Select a smaller field, or read it in JavaScript without imports.'
1048+
)
1049+
})
1050+
1051+
it('omits a field reference that a key cannot be written as', async () => {
1052+
await expect(
1053+
resolveAgainstProducer(
1054+
'javascript',
1055+
{ result: { 'report.txt': largeRef(10.5 * 1024 * 1024) } },
1056+
'return <Producer.result>'
1057+
)
1058+
).rejects.toThrow(
1059+
'<Producer.result> contains a 10.5 MB value. Reference the field you need directly.'
1060+
)
1061+
})
10371062
})
10381063

10391064
it('breaks JavaScript string literals around quoted block references', async () => {
@@ -1148,30 +1173,6 @@ describe('VariableResolver function block inputs', () => {
11481173
describe('VariableResolver function context overflow offload', () => {
11491174
const REF_KEY = 'execution/workspace-1/workflow-1/execution-1/large-value-lv_ABCDEFGHIJKL.json'
11501175

1151-
function createOffloadEnv(language: string, producerOutput: Record<string, unknown>) {
1152-
const { block, ctx } = createResolver(language)
1153-
const producer = createBlock('producer', 'Producer', BlockType.API)
1154-
const state = new ExecutionState()
1155-
state.setBlockOutput('producer', producerOutput)
1156-
const workflow: SerializedWorkflow = {
1157-
version: '1',
1158-
blocks: [producer, block],
1159-
connections: [],
1160-
loops: {},
1161-
parallels: {},
1162-
}
1163-
const resolver = new VariableResolver(workflow, {}, state)
1164-
const durableCtx = {
1165-
...ctx,
1166-
blockStates: state.getBlockStates(),
1167-
workspaceId: 'workspace-1',
1168-
workflowId: 'workflow-1',
1169-
executionId: 'execution-1',
1170-
largeValueKeys: [] as string[],
1171-
} as ExecutionContext
1172-
return { block, resolver, durableCtx }
1173-
}
1174-
11751176
beforeEach(() => {
11761177
mockStoreLargeValue.mockReset()
11771178
mockStoreLargeValue.mockResolvedValue({

0 commit comments

Comments
 (0)