Skip to content

Commit 7d2fe33

Browse files
committed
fix(workflows): keep validating literal entries of an oversized selector value
Review round 2. The length cap skipped the whole field, so an oversized list of plain literal ids lost validation it previously had. Literals never needed tokenization, so the cap was broader than the cost it was there to bound. It now gives up only the reference-aware split: past the cap the value is split plainly, and its entries are classified and validated as usual, since they are short enough that the tokenizer's per-candidate cost does not apply (1MB of literal ids across 30000 entries measures ~9ms). Only an individual entry past the cap is skipped, where there is no cheap way to tell a literal from a dynamic binding.
1 parent 3ee8395 commit 7d2fe33

2 files changed

Lines changed: 43 additions & 14 deletions

File tree

apps/sim/lib/workflows/editing/validation.test.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,9 +1304,7 @@ describe('collectUnresolvedReferences', () => {
13041304
expect(refs).toHaveLength(0)
13051305
})
13061306

1307-
it('skips an oversized selector value instead of tokenizing it', async () => {
1308-
// Reference detection tokenizes the whole string and the tokenizer is superlinear in
1309-
// candidate count, so an implausible value is skipped rather than parsed.
1307+
it('skips a single oversized entry, which cannot be classified cheaply', async () => {
13101308
mockValidateSelectorIds.mockResolvedValue({ valid: [], invalid: ['x'] })
13111309
const state = {
13121310
blocks: {
@@ -1324,6 +1322,28 @@ describe('collectUnresolvedReferences', () => {
13241322
expect(refs).toHaveLength(0)
13251323
})
13261324

1325+
it('still validates the literal entries of an oversized list', async () => {
1326+
// The cap gives up reference-aware splitting, not validation: the entries are still short,
1327+
// so each is classified and the literals are still checked.
1328+
mockValidateSelectorIds.mockResolvedValue({ valid: [], invalid: ['kb_missing'] })
1329+
const padding = Array.from({ length: 400 }, (_, index) => `kb_${'x'.repeat(30)}${index}`)
1330+
const value = [...padding, '<start.kbId>', 'kb_missing'].join(',')
1331+
expect(value.length).toBeGreaterThan(10_000)
1332+
const state = {
1333+
blocks: {
1334+
kb1: { type: 'knowledge', name: 'KB', subBlocks: { knowledgeBaseId: { value } } },
1335+
},
1336+
}
1337+
const startedAt = performance.now()
1338+
const refs = await collectUnresolvedReferences(state, CTX)
1339+
1340+
expect(performance.now() - startedAt).toBeLessThan(1000)
1341+
const [, ids] = mockValidateSelectorIds.mock.calls[0]
1342+
expect(ids).toContain('kb_missing')
1343+
expect(ids).not.toContain('<start.kbId>')
1344+
expect(refs).toHaveLength(1)
1345+
})
1346+
13271347
it('still validates a plain literal id (the guard must not over-skip)', async () => {
13281348
mockValidateSelectorIds.mockResolvedValue({ valid: [], invalid: ['kb_missing'] })
13291349
const state = {

apps/sim/lib/workflows/editing/validation.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,11 +1083,13 @@ interface SelectorFieldToValidate {
10831083
}
10841084

10851085
/**
1086-
* A selector value is an id, or a short comma-separated list of them. Reference detection
1087-
* tokenizes the whole string and `findWorkflowReferenceTokens` is superlinear in candidate count,
1088-
* so an oversized value is both implausible and expensive on a write path that admits megabytes.
1089-
* Past this the field is skipped rather than tokenized: the lint is advisory, so declining to
1090-
* check is the safe direction.
1086+
* Longest selector string reference detection will tokenize.
1087+
*
1088+
* `findWorkflowReferenceTokens` parses the whole string and is superlinear in candidate count, so
1089+
* an oversized value is expensive on a write path that admits megabytes. Past this a value is
1090+
* split plainly instead: its entries are still short, so each one is classified and validated as
1091+
* usual, and only the comma-inside-a-reference protection is given up. An individual ENTRY past
1092+
* the cap is skipped, since there is no cheap way to tell a literal from a dynamic binding.
10911093
*/
10921094
const MAX_SELECTOR_VALUE_LENGTH = 10_000
10931095

@@ -1144,25 +1146,32 @@ function collectSelectorFields(
11441146

11451147
const subBlockValue = blockData.subBlocks?.[subBlockConfig.id]?.value
11461148
if (!subBlockValue) continue
1147-
if (typeof subBlockValue === 'string' && subBlockValue.length > MAX_SELECTOR_VALUE_LENGTH) {
1148-
continue
1149-
}
1149+
1150+
const isOversized = (entry: unknown) =>
1151+
typeof entry === 'string' && entry.length > MAX_SELECTOR_VALUE_LENGTH
11501152

11511153
// Handle comma-separated values for multi-select
11521154
let values: string | string[] = subBlockValue
11531155
if (typeof subBlockValue === 'string' && subBlockValue.includes(',')) {
1154-
values = splitOutsideReferences(subBlockValue)
1156+
values = isOversized(subBlockValue)
1157+
? subBlockValue
1158+
.split(',')
1159+
.map((entry: string) => entry.trim())
1160+
.filter(Boolean)
1161+
: splitOutsideReferences(subBlockValue)
11551162
}
11561163

11571164
// A dynamically bound value only acquires its id at execution time, so a static
11581165
// id-existence check cannot evaluate it. Filtered per entry rather than on the whole
11591166
// string, because a multi-select can mix literal ids with dynamic ones: testing
11601167
// `<a.b>,kb_real,<c.d>` as a whole would drop `kb_real` along with the references.
11611168
if (Array.isArray(values)) {
1162-
const literalValues = values.filter((entry) => !containsReference(entry))
1169+
const literalValues = values.filter(
1170+
(entry) => !isOversized(entry) && !containsReference(entry)
1171+
)
11631172
if (literalValues.length === 0) continue
11641173
values = literalValues
1165-
} else if (containsReference(values)) {
1174+
} else if (isOversized(values) || containsReference(values)) {
11661175
continue
11671176
}
11681177

0 commit comments

Comments
 (0)