Skip to content
215 changes: 215 additions & 0 deletions apps/sim/lib/execution/code-placeholders/compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,169 @@ describe('code placeholder compiler', () => {
}
})

it.each([
'total=$(( {{KEY}} * 2 ))',
'echo "$(( {{KEY}} * 2 ))"',
'total=$[ {{KEY}} * 2 ]',
'echo "$[ {{KEY}} * 2 ]"',
'(( total = {{KEY}} * 2 ))',
'for (( i = 0; i < {{KEY}}; i++ )); do :; done',
'echo "$(( 1 + ({{KEY}} * 2) ))"',
'echo "$(( $(printf %s "{{KEY}}") * 2 ))"',
'echo "$[ values[0] + {{KEY}} ]"',
'cat <<EOF\n$(( {{KEY}} * 2 ))\nEOF',
'cat <<EOF\n$[ {{KEY}} * 2 ]\nEOF',
'values[{{KEY}}]=x',
'values[{{KEY}}]+=x',
'PREFIX=1 values[{{KEY}}]=x',
'if values[{{KEY}}]=x; then :; fi',
'declare -a values[{{KEY}}]=x',
'printf %s done # end of command\nvalues[{{KEY}}]=x',
'[[ 1 -eq {{KEY}} ]]',
'[[ "{{KEY}}" -ne 1 ]]',
'[[ 1 -lt "{{KEY}}" ]]',
'[[ {{KEY}} -le 1 ]]',
'[[ 1 -gt {{KEY}} ]]',
'[[ {{KEY}} -ge 1 ]]',
'[[ "$(printf %s "{{KEY}}")" -eq 1 ]]',
'let "x={{KEY}}"',
'let values[{{KEY}}]=1',
"builtin let 'x={{KEY}}'",
'let "x=$(printf %s "{{KEY}}")"',
'let >/dev/null "x={{KEY}}"',
'echo "$(( $(let "x=1" > /dev/{{KEY}}; printf 1) ))"',
'values[ 1 + {{KEY}} ]=x',
'values[$(printf %s "{{KEY}}")]=x',
'values=([{{KEY}}]=x)',
'values+=([{{KEY}}]=x)',
`echo "\${values[{{KEY}}]}"`,
`echo "\${#values[{{KEY}}]}"`,
`cat <<EOF\n\${values[{{KEY}}]}\nEOF`,
])('rejects shell placeholders that feed arithmetic evaluation: %s', async (code) => {
await expect(
compileCodePlaceholders({
code,
language: CodeLanguage.Shell,
environmentVariables: { KEY: 'x[$(printf unsafe >&2)]' },
})
).rejects.toThrow('is not supported in a shell arithmetic expression')
})

it('keeps arithmetic discovery separate from runtime placeholder rejection', async () => {
await expect(
analyzeCodePlaceholders('echo "$(( {{KEY}} * 2 ))"', CodeLanguage.Shell)
).resolves.toEqual(['KEY'])
const missing = await compileCodePlaceholders({
code: 'echo "$(( {{MISSING}} + 1 ))"',
language: CodeLanguage.Shell,
})
expect(missing.code).toContain('{{MISSING}}')
})

it('restores shell quote contexts after arithmetic and leaves literal arithmetic text alone', async () => {
const compiled = await compileCodePlaceholders({
code: [
'printf "%s\\n" "$(( (2 + 1) << 1 )):$[ 2 + 1 ]:{{KEY}}"',
"printf '%s\\n' '$(( {{KEY}} ))'",
'cat <<EOF',
'(( {{KEY}} ))',
'EOF',
].join('\n'),
language: CodeLanguage.Shell,
environmentVariables: { KEY: 'one two' },
})
expect(executeShell(compiled.code, compiled.bindings)).toBe(
'6:3:one two\n$(( one two ))\n(( one two ))\n'
)
})

it('keeps bracket words and array values outside arithmetic positions', async () => {
const compiled = await compileCodePlaceholders({
code: [
'printf "%s\\n" "values[{{KEY}}]=literal" values[{{KEY}}]',
'values=("{{KEY}}" item[{{KEY}}])',
'values[0]="{{KEY}}"',
`printf "%s\\n" "\${values[@]}"`,
'cat <<EOF',
'values[{{KEY}}]=literal',
'EOF',
].join('\n'),
language: CodeLanguage.Shell,
environmentVariables: { KEY: 'word' },
})
expect(executeShell(compiled.code, compiled.bindings)).toBe(
'values[word]=literal\nvalues[word]\nword\nitem[word]\nvalues[word]=literal\n'
)
})

it('keeps array-shaped command arguments literal', async () => {
const compiled = await compileCodePlaceholders({
code: [
'printf "%s\\n" config[{{KEY}}]=1',
'PREFIX=1 printf "%s\\n" config[{{KEY}}]+=2',
'if true; then printf "%s\\n" config[{{KEY}}]=3; fi',
].join('\n'),
language: CodeLanguage.Shell,
environmentVariables: { KEY: 'word' },
})
expect(executeShell(compiled.code, compiled.bindings)).toBe(
'config[word]=1\nconfig[word]+=2\nconfig[word]=3\n'
)
})

it('preserves string operands beside numeric shell comparisons', async () => {
const compiled = await compileCodePlaceholders({
code: [
'[[ "{{KEY}}" == word && 1 -eq 1 ]] && printf "%s\\n" "{{KEY}}"',
'[[ 2 -gt 1 && ( "{{KEY}}" == word || "{{KEY}}" == "-eq" ) ]] && printf "%s\\n" "{{KEY}}"',
].join('\n'),
language: CodeLanguage.Shell,
environmentVariables: { KEY: 'word' },
})
expect(executeShell(compiled.code, compiled.bindings)).toBe('word\nword\n')
})

it('ends builtin arithmetic context at the next shell command', async () => {
const compiled = await compileCodePlaceholders({
code: [
'let "x=1"; printf "%s\\n" "{{KEY}}"',
'let "x=2" # end of arithmetic',
'printf "%s\\n" "{{KEY}}"',
].join('\n'),
language: CodeLanguage.Shell,
environmentVariables: { KEY: 'two words' },
})
expect(executeShell(compiled.code, compiled.bindings)).toBe('two words\ntwo words\n')
})

it.each(['> /dev/{{KEY}}', '> "/dev/{{KEY}}"', '> "$(printf /dev/%s "{{KEY}}")"'])(
'keeps let redirection targets out of arithmetic context: %s',
async (redirect) => {
const compiled = await compileCodePlaceholders({
code: `let "x=1" ${redirect}; printf "%s\\n" "$x"`,
language: CodeLanguage.Shell,
environmentVariables: { KEY: 'null' },
})
expect(executeShell(compiled.code, compiled.bindings)).toBe('1\n')
}
)

it.each([
['unquoted', "cat <<EOF >/dev/null\nToday's report\nEOF"],
['quoted', "cat <<'EOF' >/dev/null\nToday's report\nEOF"],
['double quote in prose', 'cat <<EOF >/dev/null\nSay "hello\nEOF'],
['literal backtick', "cat <<'EOF' >/dev/null\nAn unmatched `\nEOF"],
['multiple bodies', "cat <<ONE <<'TWO' >/dev/null\nToday's report\nONE\nSay \"hello\nTWO"],
])('preserves quoted shell values after a %s heredoc', async (_name, prefix) => {
const value = 'one two $(printf unsafe)'
const compiled = await compileCodePlaceholders({
code: `${prefix}\nprintf "<%s>\\n" "{{KEY}}"`,
language: CodeLanguage.Shell,
environmentVariables: { KEY: value },
})
expect(executeShell(compiled.code, compiled.bindings)).toBe(`<${value}>\n`)
})

it('renders quoted shell heredocs nested in double-quoted command substitutions', async () => {
const compiled = await compileCodePlaceholders({
code: [
Expand Down Expand Up @@ -1261,6 +1424,58 @@ describe('direct environment reads in shell', () => {
expect(unquoted.resolvedSecretNames).toEqual(['API_KEY'])
})

it.each(['EOF', "'EOF'"])(
'records direct shell reads after a %s heredoc without lexing its prose',
async (delimiter) => {
const compiled = await compileCodePlaceholders({
code: `cat <<${delimiter} >/dev/null\nToday's report\nEOF\necho "$API_KEY"`,
language: CodeLanguage.Shell,
environmentVariables: { API_KEY: 'synthetic-value' },
})
expect(executeShell(compiled.code, [{ name: 'API_KEY', value: 'synthetic-value' }])).toBe(
'synthetic-value\n'
)
expect(compiled.resolvedSecretNames).toEqual(['API_KEY'])
}
)

it.each(["Today's $API_KEY", '# $API_KEY'])(
'records direct shell reads in expanding heredoc prose: %s',
async (body) => {
const compiled = await compileCodePlaceholders({
code: `cat <<EOF\n${body}\nEOF`,
language: CodeLanguage.Shell,
environmentVariables: { API_KEY: 'synthetic-value' },
})
expect(
executeShell(compiled.code, [{ name: 'API_KEY', value: 'synthetic-value' }])
).toContain('synthetic-value')
expect(compiled.resolvedSecretNames).toEqual(['API_KEY'])
}
)

it('skips heredoc delimiters and literal bodies but keeps reads on the header line', async () => {
const compiled = await compileCodePlaceholders({
code: [
'cat <<$DELIMITER; echo "$HEADER"',
'body',
'$DELIMITER',
"cat <<'EOF'",
"Today's $LITERAL",
'EOF',
'echo "$AFTER"',
].join('\n'),
language: CodeLanguage.Shell,
environmentVariables: {
DELIMITER: 'unused-delimiter',
HEADER: 'header-value',
LITERAL: 'unused-literal',
AFTER: 'after-value',
},
})
expect(compiled.resolvedSecretNames).toEqual(['HEADER', 'AFTER'])
})

it('ignores a shell variable that is not a configured secret', async () => {
const compiled = await compileCodePlaceholders({
code: 'echo "$PATH $HOME"',
Expand Down
Loading
Loading