Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 82 additions & 1 deletion apps/sim/lib/workflows/editing/json-literal-refs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@ const MOCK_BLOCKS = vi.hoisted(
],
outputs: {},
},
response: {
type: 'response',
category: 'blocks',
subBlocks: [
{ id: 'dataMode', type: 'dropdown' },
{
id: 'data',
type: 'code',
language: 'json',
condition: { field: 'dataMode', value: 'json' },
},
],
outputs: {},
},
function: {
type: 'function',
category: 'blocks',
Expand Down Expand Up @@ -135,7 +149,74 @@ describe('collectUnquotedJsonStringReferences', () => {
value: ['<start.order_id>'],
})
expect(findings[0]?.reason).toMatch(/^unquoted-json-string: /)
expect(findings[0]?.reason).toContain('"<start.order_id>"')
})

/**
* Quoting is only safe for text without a double quote, backslash, or control
* character, since the text is inserted raw; free text such as a model reply has
* to be built into JSON by a Function block instead.
*/
it('advises quoting only for plain text and a Function block for free text', () => {
const [finding] = collectUnquotedJsonStringReferences(
graph({
start: START,
insert: insertRow('{"order_id": <start.order_id>}'),
})
)
expect(finding?.reason).toContain('"<start.order_id>"')
expect(finding?.reason).toMatch(
/double quote, backslash, line break, or other control character/
)
expect(finding?.reason).toMatch(/Function block/)
expect(finding?.reason).not.toMatch(/Quote each one/)
})

/**
* A field that is exactly one reference is the referenced value itself: the
* Response block returns text it cannot parse as-is and the API block sends a
* text body raw, while quoting it would turn JSON text into a JSON string.
*/
it('accepts a field made only of string references, with no JSON around them', () => {
const findings = collectUnquotedJsonStringReferences(
graph({
writer: { type: 'agent', name: 'Writer' },
reply: {
type: 'response',
name: 'Reply',
subBlocks: { dataMode: { value: 'json' }, data: { value: '<writer.content>' } },
},
call: {
type: 'api',
name: 'Post',
subBlocks: { body: { value: ' <writer.content>\n' } },
},
signed: {
type: 'api',
name: 'Post Signed',
subBlocks: { body: { value: '<writer.content>\n\n<writer.content>' } },
},
})
)
expect(findings).toHaveLength(0)
})

it('still flags a string reference that sits inside JSON text around it', () => {
const findings = collectUnquotedJsonStringReferences(
graph({
writer: { type: 'agent', name: 'Writer' },
reply: {
type: 'response',
name: 'Reply',
subBlocks: { dataMode: { value: 'json' }, data: { value: '[<writer.content>]' } },
},
call: {
type: 'api',
name: 'Post',
subBlocks: { body: { value: '{"note": <writer.content>}' } },
},
})
)
expect(findings.map((finding) => finding.blockId)).toEqual(['reply', 'call'])
})

it('flags every unquoted string reference in the field once, including agent text', () => {
Expand Down
23 changes: 19 additions & 4 deletions apps/sim/lib/workflows/editing/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -758,13 +758,26 @@ export function collectDanglingBlockOutputReferences(
* `block.path` bodies of the reference tokens in JSON text that sit outside
* every string literal, tokenized with the runtime's own reference scanner.
* Tokens inside a literal, escaped quotes included, are already strings in the
* parsed document.
* parsed document. A field made only of references has no JSON around them:
* the block receives the referenced text itself (Response returns text it
* cannot parse as-is, API sends it as a raw body), and quoting would only turn
* JSON text into a JSON string, so none of its tokens are reported.
*/
function unquotedJsonReferenceTokens(json: string): string[] {
const tokens = findWorkflowReferenceTokens(json)
let outsideTokens = ''
let previousEnd = 0
for (const token of tokens) {
outsideTokens += json.slice(previousEnd, token.start)
previousEnd = token.end
}
outsideTokens += json.slice(previousEnd)
if (outsideTokens.trim() === '') return []
Comment thread
waleedlatif1 marked this conversation as resolved.

const unquoted: string[] = []
let cursor = 0
let inString = false
for (const token of findWorkflowReferenceTokens(json)) {
for (const token of tokens) {
for (; cursor < token.start; cursor++) {
const char = json[cursor]
if (inString && char === '\\') cursor++
Expand All @@ -784,7 +797,9 @@ function unquotedJsonReferenceTokens(json: string): string[] {
* Outside Function code a reference is replaced by its raw text, so
* `{"id": <start.order_id>}` becomes `{"id": ord-1}` and the block fails to
* parse it at run time, while lint, deploy, and every earlier run of a draft
* that never reached the block stay clean. Only references whose declared
* that never reached the block stay clean. No JSON-aware escaping exists
* outside Function code, so quoting is only safe for text that can never hold a
* quote, backslash, or control character. Only references whose declared
* output type is `string` are reported: numbers, booleans, and objects already
* resolve to JSON values, and an undeclared type cannot be judged here.
*/
Expand Down Expand Up @@ -835,7 +850,7 @@ export function collectUnquotedJsonStringReferences(
field,
value,
kind: 'block-output',
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]}".`,
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.`,
})
}
}
Expand Down
Loading