Skip to content

Commit 3c87849

Browse files
authored
fix(workflows): stop flagging whole-field references in JSON fields and correct the quoting advice (#8837)
* fix(workflows): stop flagging whole-field references in JSON fields and correct the quoting advice * fix(workflows): name JSON control characters in the quoting advice
1 parent e89bc65 commit 3c87849

2 files changed

Lines changed: 101 additions & 5 deletions

File tree

‎apps/sim/lib/workflows/editing/json-literal-refs.test.ts‎

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,20 @@ const MOCK_BLOCKS = vi.hoisted(
6363
],
6464
outputs: {},
6565
},
66+
response: {
67+
type: 'response',
68+
category: 'blocks',
69+
subBlocks: [
70+
{ id: 'dataMode', type: 'dropdown' },
71+
{
72+
id: 'data',
73+
type: 'code',
74+
language: 'json',
75+
condition: { field: 'dataMode', value: 'json' },
76+
},
77+
],
78+
outputs: {},
79+
},
6680
function: {
6781
type: 'function',
6882
category: 'blocks',
@@ -135,7 +149,74 @@ describe('collectUnquotedJsonStringReferences', () => {
135149
value: ['<start.order_id>'],
136150
})
137151
expect(findings[0]?.reason).toMatch(/^unquoted-json-string: /)
138-
expect(findings[0]?.reason).toContain('"<start.order_id>"')
152+
})
153+
154+
/**
155+
* Quoting is only safe for text without a double quote, backslash, or control
156+
* character, since the text is inserted raw; free text such as a model reply has
157+
* to be built into JSON by a Function block instead.
158+
*/
159+
it('advises quoting only for plain text and a Function block for free text', () => {
160+
const [finding] = collectUnquotedJsonStringReferences(
161+
graph({
162+
start: START,
163+
insert: insertRow('{"order_id": <start.order_id>}'),
164+
})
165+
)
166+
expect(finding?.reason).toContain('"<start.order_id>"')
167+
expect(finding?.reason).toMatch(
168+
/double quote, backslash, line break, or other control character/
169+
)
170+
expect(finding?.reason).toMatch(/Function block/)
171+
expect(finding?.reason).not.toMatch(/Quote each one/)
172+
})
173+
174+
/**
175+
* A field that is exactly one reference is the referenced value itself: the
176+
* Response block returns text it cannot parse as-is and the API block sends a
177+
* text body raw, while quoting it would turn JSON text into a JSON string.
178+
*/
179+
it('accepts a field made only of string references, with no JSON around them', () => {
180+
const findings = collectUnquotedJsonStringReferences(
181+
graph({
182+
writer: { type: 'agent', name: 'Writer' },
183+
reply: {
184+
type: 'response',
185+
name: 'Reply',
186+
subBlocks: { dataMode: { value: 'json' }, data: { value: '<writer.content>' } },
187+
},
188+
call: {
189+
type: 'api',
190+
name: 'Post',
191+
subBlocks: { body: { value: ' <writer.content>\n' } },
192+
},
193+
signed: {
194+
type: 'api',
195+
name: 'Post Signed',
196+
subBlocks: { body: { value: '<writer.content>\n\n<writer.content>' } },
197+
},
198+
})
199+
)
200+
expect(findings).toHaveLength(0)
201+
})
202+
203+
it('still flags a string reference that sits inside JSON text around it', () => {
204+
const findings = collectUnquotedJsonStringReferences(
205+
graph({
206+
writer: { type: 'agent', name: 'Writer' },
207+
reply: {
208+
type: 'response',
209+
name: 'Reply',
210+
subBlocks: { dataMode: { value: 'json' }, data: { value: '[<writer.content>]' } },
211+
},
212+
call: {
213+
type: 'api',
214+
name: 'Post',
215+
subBlocks: { body: { value: '{"note": <writer.content>}' } },
216+
},
217+
})
218+
)
219+
expect(findings.map((finding) => finding.blockId)).toEqual(['reply', 'call'])
139220
})
140221

141222
it('flags every unquoted string reference in the field once, including agent text', () => {

‎apps/sim/lib/workflows/editing/lint.ts‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -758,13 +758,26 @@ export function collectDanglingBlockOutputReferences(
758758
* `block.path` bodies of the reference tokens in JSON text that sit outside
759759
* every string literal, tokenized with the runtime's own reference scanner.
760760
* Tokens inside a literal, escaped quotes included, are already strings in the
761-
* parsed document.
761+
* parsed document. A field made only of references has no JSON around them:
762+
* the block receives the referenced text itself (Response returns text it
763+
* cannot parse as-is, API sends it as a raw body), and quoting would only turn
764+
* JSON text into a JSON string, so none of its tokens are reported.
762765
*/
763766
function unquotedJsonReferenceTokens(json: string): string[] {
767+
const tokens = findWorkflowReferenceTokens(json)
768+
let outsideTokens = ''
769+
let previousEnd = 0
770+
for (const token of tokens) {
771+
outsideTokens += json.slice(previousEnd, token.start)
772+
previousEnd = token.end
773+
}
774+
outsideTokens += json.slice(previousEnd)
775+
if (outsideTokens.trim() === '') return []
776+
764777
const unquoted: string[] = []
765778
let cursor = 0
766779
let inString = false
767-
for (const token of findWorkflowReferenceTokens(json)) {
780+
for (const token of tokens) {
768781
for (; cursor < token.start; cursor++) {
769782
const char = json[cursor]
770783
if (inString && char === '\\') cursor++
@@ -784,7 +797,9 @@ function unquotedJsonReferenceTokens(json: string): string[] {
784797
* Outside Function code a reference is replaced by its raw text, so
785798
* `{"id": <start.order_id>}` becomes `{"id": ord-1}` and the block fails to
786799
* parse it at run time, while lint, deploy, and every earlier run of a draft
787-
* that never reached the block stay clean. Only references whose declared
800+
* that never reached the block stay clean. No JSON-aware escaping exists
801+
* outside Function code, so quoting is only safe for text that can never hold a
802+
* quote, backslash, or control character. Only references whose declared
788803
* output type is `string` are reported: numbers, booleans, and objects already
789804
* resolve to JSON values, and an undeclared type cannot be judged here.
790805
*/
@@ -835,7 +850,7 @@ export function collectUnquotedJsonStringReferences(
835850
field,
836851
value,
837852
kind: 'block-output',
838-
reason: `unquoted-json-string: these references resolve to text, which is inserted without quotes, so the field is not valid JSON at run time unless the text is itself JSON. Quote each one, e.g. "${value[0]}".`,
853+
reason: `unquoted-json-string: these references resolve to text, which is inserted raw, so the field is not valid JSON at run time unless the text is itself JSON. Quoting, e.g. "${value[0]}", works only for text that never contains a double quote, backslash, line break, or other control character, such as an id. For free text such as a model reply, build the JSON in a Function block, which reads references as values, and set this field to only that block's result reference.`,
839854
})
840855
}
841856
}

0 commit comments

Comments
 (0)