Skip to content

Commit 41384df

Browse files
committed
remove build files, harden edge cases
1 parent 9e1775e commit 41384df

14 files changed

Lines changed: 400 additions & 300 deletions

File tree

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,67 @@ describe('VariablesBlockHandler', () => {
8181
expect(mockUploadFile).not.toHaveBeenCalled()
8282
})
8383

84+
it('includes unmatched assignments in block output without mutating workflow variables', async () => {
85+
const handler = new VariablesBlockHandler()
86+
const ctx = createContext()
87+
const value = [{ key: 'SIM-1', summary: 'Transient issue' }]
88+
89+
const output = await handler.execute(ctx, createBlock(), {
90+
variables: [
91+
{
92+
variableName: 'transientIssues',
93+
type: 'array',
94+
value,
95+
},
96+
],
97+
})
98+
99+
expect(ctx.workflowVariables).not.toHaveProperty('transientIssues')
100+
expect(output).toEqual({ transientIssues: value })
101+
})
102+
103+
it('keeps special unmatched assignment names as own output fields', async () => {
104+
const handler = new VariablesBlockHandler()
105+
const ctx = createContext()
106+
const value = { polluted: true }
107+
108+
const output = await handler.execute(ctx, createBlock(), {
109+
variables: [
110+
{
111+
variableName: '__proto__',
112+
type: 'object',
113+
value,
114+
},
115+
],
116+
})
117+
118+
expect(Object.hasOwn(output, '__proto__')).toBe(true)
119+
expect(output.__proto__).toEqual(value)
120+
expect(Object.getPrototypeOf(output)).toBe(Object.prototype)
121+
})
122+
123+
it('does not treat inherited prototype keys as existing workflow variable IDs', async () => {
124+
const handler = new VariablesBlockHandler()
125+
const ctx = createContext()
126+
const value = { safe: true }
127+
const originalPrototype = Object.getPrototypeOf(ctx.workflowVariables)
128+
129+
const output = await handler.execute(ctx, createBlock(), {
130+
variables: [
131+
{
132+
variableId: '__proto__',
133+
variableName: 'prototypeAssignment',
134+
type: 'object',
135+
value,
136+
},
137+
],
138+
})
139+
140+
expect(Object.getPrototypeOf(ctx.workflowVariables)).toBe(originalPrototype)
141+
expect(ctx.workflowVariables).not.toHaveProperty('__proto__')
142+
expect(output).toEqual({ prototypeAssignment: value })
143+
})
144+
84145
it('stores oversized array assignments as durable manifests in variables and block output', async () => {
85146
const handler = new VariablesBlockHandler()
86147
const ctx = createContext()

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

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,38 @@ import type { SerializedBlock } from '@/serializer/types'
99

1010
const logger = createLogger('VariablesBlockHandler')
1111

12+
function setOutputValue(output: Record<string, any>, key: string, value: any): void {
13+
Object.defineProperty(output, key, {
14+
value,
15+
enumerable: true,
16+
configurable: true,
17+
writable: true,
18+
})
19+
}
20+
21+
function getWorkflowVariableEntry(
22+
workflowVariables: Record<string, any>,
23+
variableId: string | undefined
24+
): [string, any] | undefined {
25+
if (!variableId || !Object.hasOwn(workflowVariables, variableId)) {
26+
return undefined
27+
}
28+
return [variableId, workflowVariables[variableId]]
29+
}
30+
31+
function setWorkflowVariableEntry(
32+
workflowVariables: Record<string, any>,
33+
id: string,
34+
value: any
35+
): void {
36+
Object.defineProperty(workflowVariables, id, {
37+
value,
38+
enumerable: true,
39+
configurable: true,
40+
writable: true,
41+
})
42+
}
43+
1244
export class VariablesBlockHandler implements BlockHandler {
1345
canHandle(block: SerializedBlock): boolean {
1446
const canHandle = block.metadata?.id === BlockType.VARIABLES
@@ -30,23 +62,21 @@ export class VariablesBlockHandler implements BlockHandler {
3062
const output: Record<string, any> = {}
3163

3264
for (const assignment of assignments) {
33-
const existingEntry = assignment.variableId
34-
? [assignment.variableId, ctx.workflowVariables[assignment.variableId]]
35-
: Object.entries(ctx.workflowVariables).find(
36-
([_, v]) => v.name === assignment.variableName
37-
)
65+
const existingEntry =
66+
getWorkflowVariableEntry(ctx.workflowVariables, assignment.variableId) ??
67+
Object.entries(ctx.workflowVariables).find(([_, v]) => v.name === assignment.variableName)
3868
const value = await this.compactAssignmentValue(ctx, assignment.value)
3969

4070
if (existingEntry?.[1]) {
4171
const [id, variable] = existingEntry
42-
ctx.workflowVariables[id] = {
72+
setWorkflowVariableEntry(ctx.workflowVariables, id, {
4373
...variable,
4474
value,
45-
}
46-
output[assignment.variableName] = value
75+
})
4776
} else {
4877
logger.warn(`Variable "${assignment.variableName}" not found in workflow variables`)
4978
}
79+
setOutputValue(output, assignment.variableName, value)
5080
}
5181

5282
return output

apps/sim/executor/orchestrators/loop.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import {
2222
emitEmptySubflowEvents,
2323
emitSubflowSuccessEvents,
2424
extractBaseBlockId,
25-
resolveArrayInputAsync,
2625
} from '@/executor/utils/subflow-utils'
26+
import { resolveArrayInputAsync } from '@/executor/utils/subflow-utils.server'
2727
import type { VariableResolver } from '@/executor/variables/resolver'
2828
import type { SerializedLoop } from '@/serializer/types'
2929

apps/sim/executor/orchestrators/parallel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import {
1313
emitEmptySubflowEvents,
1414
emitSubflowSuccessEvents,
1515
extractBranchIndex,
16-
resolveArrayInputAsync,
1716
} from '@/executor/utils/subflow-utils'
17+
import { resolveArrayInputAsync } from '@/executor/utils/subflow-utils.server'
1818
import type { VariableResolver } from '@/executor/variables/resolver'
1919
import type { SerializedParallel } from '@/serializer/types'
2020

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it, vi } from 'vitest'
5+
import { filterHiddenOutputKeys } from '@/lib/logs/execution/trace-spans/trace-spans'
6+
import { filterOutputForLog } from '@/executor/utils/output-filter'
7+
8+
vi.mock('@/blocks', () => ({
9+
getBlock: () => undefined,
10+
}))
11+
12+
describe('output filtering', () => {
13+
it('preserves special top-level output keys as own fields', () => {
14+
const rawOutput: Record<string, unknown> = {}
15+
Object.defineProperty(rawOutput, 'constructor', {
16+
value: { safe: true },
17+
enumerable: true,
18+
})
19+
20+
const output = filterOutputForLog('', rawOutput)
21+
22+
expect(Object.hasOwn(output, 'constructor')).toBe(true)
23+
expect(output.constructor).toEqual({ safe: true })
24+
expect(Object.getPrototypeOf(output)).toBe(Object.prototype)
25+
})
26+
27+
it('preserves special nested output keys as own fields', () => {
28+
const nested: Record<string, unknown> = {}
29+
Object.defineProperty(nested, '__proto__', {
30+
value: { safe: true },
31+
enumerable: true,
32+
})
33+
34+
const filtered = filterHiddenOutputKeys({
35+
nested,
36+
}) as { nested: Record<string, unknown> }
37+
38+
expect(Object.hasOwn(filtered.nested, '__proto__')).toBe(true)
39+
expect(filtered.nested.__proto__).toEqual({ safe: true })
40+
expect(Object.getPrototypeOf(filtered.nested)).toBe(Object.prototype)
41+
})
42+
})

apps/sim/executor/utils/output-filter.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@ import { isTriggerBehavior, isTriggerInternalKey } from '@/executor/constants'
66
import type { NormalizedBlockOutput } from '@/executor/types'
77
import type { SerializedBlock } from '@/serializer/types'
88

9+
function setFilteredOutputValue(
10+
output: Record<string, unknown>,
11+
key: string,
12+
value: unknown
13+
): void {
14+
Object.defineProperty(output, key, {
15+
value,
16+
enumerable: true,
17+
configurable: true,
18+
writable: true,
19+
})
20+
}
21+
922
/**
1023
* Filters block output for logging/display purposes.
1124
* Removes internal fields and fields marked with hiddenFromDisplay.
@@ -54,7 +67,7 @@ export function filterOutputForLog(
5467
}
5568

5669
// Recursively filter globally hidden keys from nested objects
57-
filtered[key] = filterHiddenOutputKeys(value)
70+
setFilteredOutputValue(filtered, key, filterHiddenOutputKeys(value))
5871
}
5972

6073
return filtered
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { toError } from '@sim/utils/errors'
2+
import {
3+
isLargeArrayManifest,
4+
LARGE_ARRAY_MANIFEST_MARKER,
5+
materializeLargeArrayManifest,
6+
} from '@/lib/execution/payloads/large-array-manifest'
7+
import { isLargeValueRef, LARGE_VALUE_REF_MARKER } from '@/lib/execution/payloads/large-value-ref'
8+
import { MAX_DURABLE_LARGE_VALUE_BYTES } from '@/lib/execution/payloads/materialization.server'
9+
import { materializeLargeValueRef } from '@/lib/execution/payloads/store'
10+
import { REFERENCE } from '@/executor/constants'
11+
import type { ExecutionContext } from '@/executor/types'
12+
import type { VariableResolver } from '@/executor/variables/resolver'
13+
14+
async function normalizeCollectionValue(ctx: ExecutionContext, value: unknown): Promise<any[]> {
15+
if (Array.isArray(value)) {
16+
return value
17+
}
18+
19+
if (isLargeArrayManifest(value)) {
20+
return materializeLargeArrayManifest(value, {
21+
workspaceId: ctx.workspaceId,
22+
workflowId: ctx.workflowId,
23+
executionId: ctx.executionId,
24+
largeValueExecutionIds: ctx.largeValueExecutionIds,
25+
allowLargeValueWorkflowScope: ctx.allowLargeValueWorkflowScope,
26+
userId: ctx.userId,
27+
maxBytes: MAX_DURABLE_LARGE_VALUE_BYTES,
28+
})
29+
}
30+
31+
if (isLargeValueRef(value)) {
32+
const materialized = await materializeLargeValueRef(value, {
33+
workspaceId: ctx.workspaceId,
34+
workflowId: ctx.workflowId,
35+
executionId: ctx.executionId,
36+
largeValueExecutionIds: ctx.largeValueExecutionIds,
37+
allowLargeValueWorkflowScope: ctx.allowLargeValueWorkflowScope,
38+
userId: ctx.userId,
39+
maxBytes: MAX_DURABLE_LARGE_VALUE_BYTES,
40+
})
41+
if (materialized === undefined) {
42+
throw new Error('Large execution value is unavailable.')
43+
}
44+
return normalizeCollectionValue(ctx, materialized)
45+
}
46+
47+
if (typeof value === 'object' && value !== null) {
48+
if ((value as Record<string, unknown>)[LARGE_ARRAY_MANIFEST_MARKER] === true) {
49+
throw new Error('Invalid large array manifest.')
50+
}
51+
if ((value as Record<string, unknown>)[LARGE_VALUE_REF_MARKER] === true) {
52+
throw new Error('Invalid large value ref.')
53+
}
54+
return Object.entries(value)
55+
}
56+
57+
if (value === null) {
58+
return []
59+
}
60+
61+
throw new Error('Value did not resolve to an array or object')
62+
}
63+
64+
/**
65+
* Resolves loop/parallel collection inputs on the server, including durable
66+
* execution values that cannot be imported into client-reachable utilities.
67+
*/
68+
export async function resolveArrayInputAsync(
69+
ctx: ExecutionContext,
70+
items: any,
71+
resolver: VariableResolver | null
72+
): Promise<any[]> {
73+
if (typeof items !== 'string') {
74+
if (items === null) {
75+
return []
76+
}
77+
if (!Array.isArray(items) && typeof items !== 'object') {
78+
if (!resolver) {
79+
return []
80+
}
81+
try {
82+
const resolved = (await resolver.resolveInputs(ctx, 'subflow_items', { items })).items
83+
return normalizeCollectionValue(ctx, resolved)
84+
} catch (error) {
85+
if (error instanceof Error && error.message.startsWith('Resolved items')) {
86+
throw error
87+
}
88+
throw new Error(`Failed to resolve items: ${toError(error).message}`)
89+
}
90+
}
91+
return normalizeCollectionValue(ctx, items)
92+
}
93+
94+
if (items.startsWith(REFERENCE.START) && items.endsWith(REFERENCE.END) && resolver) {
95+
try {
96+
const resolved = await resolver.resolveSingleReference(ctx, '', items, undefined, {
97+
allowLargeValueRefs: true,
98+
})
99+
return normalizeCollectionValue(ctx, resolved)
100+
} catch (error) {
101+
if (error instanceof Error && error.message.startsWith('Reference "')) {
102+
throw error
103+
}
104+
throw new Error(`Failed to resolve reference "${items}": ${toError(error).message}`)
105+
}
106+
}
107+
108+
try {
109+
const normalized = items.replace(/'/g, '"')
110+
const parsed = JSON.parse(normalized)
111+
return normalizeCollectionValue(ctx, parsed)
112+
} catch (error) {
113+
if (error instanceof Error && error.message.startsWith('Parsed value')) {
114+
throw error
115+
}
116+
throw new Error(`Failed to parse items as JSON: "${items}"`)
117+
}
118+
}

0 commit comments

Comments
 (0)