From 7720e79d79ddc7c60cf3adcd92b87ae17a251336 Mon Sep 17 00:00:00 2001 From: MohammadYusif Date: Thu, 28 May 2026 03:14:57 +0300 Subject: [PATCH] fix: use function replacement in expandPrompt to prevent dollar-sign corruption (#50) String.prototype.replaceAll interprets special patterns like $$, $&, $`, and $' in the replacement string, silently rewriting those sequences when variable values contain them. Switch to the function form `() => value` which is never pattern- interpreted, so values are always inserted verbatim. Add five regression tests in expandTemplate covering $$, $&, $`, $', and mixed dollar-sign sequences across multiple variables. --- src/util/__test__/expand-prompt.test.ts | 40 +++++++++++++++++++++++++ src/util/expand-prompt.ts | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/util/__test__/expand-prompt.test.ts b/src/util/__test__/expand-prompt.test.ts index 5010199..412576f 100644 --- a/src/util/__test__/expand-prompt.test.ts +++ b/src/util/__test__/expand-prompt.test.ts @@ -145,4 +145,44 @@ describe('expandTemplate', () => { const result = await expandPrompt('No placeholders here.', tempDir, {}); expect(result).toBe('No placeholders here.'); }); + + it('preserves $$ in a variable value verbatim', async () => { + const result = await expandPrompt('Price: {{cost}}', tempDir, { + cost: '$$100', + }); + expect(result).toBe('Price: $$100'); + }); + + it('preserves $& in a variable value verbatim', async () => { + const result = await expandPrompt('Body: {{body}}', tempDir, { + body: 'foo $& bar', + }); + expect(result).toBe('Body: foo $& bar'); + }); + + it('preserves $` in a variable value verbatim', async () => { + const result = await expandPrompt('Body: {{body}}', tempDir, { + body: 'before $` after', + }); + expect(result).toBe('Body: before $` after'); + }); + + it("preserves $' in a variable value verbatim", async () => { + const result = await expandPrompt('Body: {{body}}', tempDir, { + body: "before $' after", + }); + expect(result).toBe("Body: before $' after"); + }); + + it('preserves mixed dollar-sign sequences across multiple variables', async () => { + const result = await expandPrompt( + 'Cost: {{cost}}, body: {{body}}', + tempDir, + { + cost: '$$100', + body: 'regex $& match', + }, + ); + expect(result).toBe('Cost: $$100, body: regex $& match'); + }); }); diff --git a/src/util/expand-prompt.ts b/src/util/expand-prompt.ts index a6556ae..07c7556 100644 --- a/src/util/expand-prompt.ts +++ b/src/util/expand-prompt.ts @@ -22,7 +22,7 @@ export async function expandPrompt( let result = await expandIncludes(template, basePath); for (const [key, value] of Object.entries(variables)) { - result = result.replaceAll(`{{${key}}}`, value); + result = result.replaceAll(`{{${key}}}`, () => value); } return result;