diff --git a/apps/sim/lib/execution/code-placeholders/compiler.test.ts b/apps/sim/lib/execution/code-placeholders/compiler.test.ts index 40c0afde1c0..65fe05ef593 100644 --- a/apps/sim/lib/execution/code-placeholders/compiler.test.ts +++ b/apps/sim/lib/execution/code-placeholders/compiler.test.ts @@ -6,6 +6,11 @@ import { type CodePlaceholderRuntimeBinding, compileCodePlaceholders, } from '@/lib/execution/code-placeholders' +import { + applySourceEdits, + CodePlaceholderCompileError, + CodePlaceholderInvariantError, +} from '@/lib/execution/code-placeholders/shared' import { CodeLanguage } from '@/lib/execution/languages' const installedGlobals = new Set() @@ -1543,3 +1548,29 @@ describe('python true positives survive the dot guard', () => { expect(await directReadNames(code, CodeLanguage.Python)).toEqual(['API_KEY']) }) }) + +describe('compiler failure classification', () => { + it('reports overlapping source edits as a compiler invariant, not a user compile error', () => { + const applyOverlappingEdits = () => + applySourceEdits('return value', [ + { start: 0, end: 6, text: 'yield' }, + { start: 3, end: 9, text: 'x' }, + ]) + + expect(applyOverlappingEdits).toThrow(CodePlaceholderInvariantError) + expect(applyOverlappingEdits).not.toThrow(CodePlaceholderCompileError) + }) + + it('reports an exhausted JavaScript sentinel space as a user compile error', async () => { + /** Every `$xyz$` sentinel a five-character `{{a}}` could take, packed under the code cap. */ + const alphabet = [...'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'] + const payloads = alphabet.flatMap((a) => + alphabet.flatMap((b) => alphabet.map((c) => `${a}${b}${c}`)) + ) + const code = `const taken = '$${payloads.join('$')}$'\nreturn {{a}}` + + await expect( + compileCodePlaceholders({ code, language: CodeLanguage.JavaScript, params: { a: 1 } }) + ).rejects.toBeInstanceOf(CodePlaceholderCompileError) + }) +}) diff --git a/apps/sim/lib/execution/code-placeholders/python.ts b/apps/sim/lib/execution/code-placeholders/python.ts index 2bee46e596f..eb8dc973d0d 100644 --- a/apps/sim/lib/execution/code-placeholders/python.ts +++ b/apps/sim/lib/execution/code-placeholders/python.ts @@ -2,6 +2,7 @@ import { sha256Hex } from '@sim/security/hash' import { applySourceEdits, CodePlaceholderCompileError, + CodePlaceholderInvariantError, createCodePlaceholderCompilationContext, isOffsetInRanges, type SourceEdit, @@ -243,7 +244,7 @@ function createSentinel(code: string): string { const sentinel = `__sim_placeholder_${digest}__` if (!code.includes(sentinel)) return sentinel } - throw new CodePlaceholderCompileError('Unable to allocate a collision-free Python marker') + throw new CodePlaceholderInvariantError('Unable to allocate a collision-free Python marker') } function pythonTriviaGap( diff --git a/apps/sim/lib/execution/code-placeholders/shared.ts b/apps/sim/lib/execution/code-placeholders/shared.ts index 52cd4ea88b4..8390c10b0d1 100644 --- a/apps/sim/lib/execution/code-placeholders/shared.ts +++ b/apps/sim/lib/execution/code-placeholders/shared.ts @@ -27,6 +27,18 @@ export class CodePlaceholderCompileError extends Error { } } +/** + * A broken compiler invariant: Sim's own compilation failed, not the user's code. + * Deliberately not a {@link CodePlaceholderCompileError}, so callers that answer a + * user compile error with a client status treat this as a server failure. + */ +export class CodePlaceholderInvariantError extends Error { + constructor(message: string) { + super(message) + this.name = 'CodePlaceholderInvariantError' + } +} + /** * Scans `{{name}}` placeholders, accepting exactly what `/\{\{([^}]+)\}\}/g` accepts — * a name may contain `{`, because parameter keys are arbitrary strings rather than @@ -270,7 +282,7 @@ export function applySourceEdits(code: string, edits: SourceEdit[]): string { let output = '' for (const edit of sorted) { if (edit.start < cursor || edit.end < edit.start || edit.end > code.length) { - throw new CodePlaceholderCompileError('Overlapping code placeholder transformations') + throw new CodePlaceholderInvariantError('Overlapping code placeholder transformations') } output += code.slice(cursor, edit.start) output += edit.text