diff --git a/src/pipeline/template.test.ts b/src/pipeline/template.test.ts index 6253994c8..46b7c152f 100644 --- a/src/pipeline/template.test.ts +++ b/src/pipeline/template.test.ts @@ -100,6 +100,21 @@ describe('evalExpr', () => { it('applies join filter', () => { expect(evalExpr('item.tags | join(,)', { item: { tags: ['a', 'b', 'c'] } })).toBe('a,b,c'); }); + it('applies replace filter', () => { + expect(evalExpr("item.x | replace('foo','bar')", { item: { x: 'foo baz' } })).toBe('bar baz'); + }); + it('preserves commas in the replace filter replacement value', () => { + expect(evalExpr("item.x | replace('a','x,y')", { item: { x: 'a' } })).toBe('x,y'); + }); + it('replaces all occurrences with the replace filter', () => { + expect(evalExpr("item.x | replace(' ','_')", { item: { x: 'a b c' } })).toBe('a_b_c'); + }); + it('passes non-string values through the replace filter unchanged', () => { + expect(evalExpr('item.x | replace(1,2)', { item: { x: 42 } })).toBe(42); + }); + it('returns the value unchanged when replace has a single arg', () => { + expect(evalExpr("item.x | replace('a')", { item: { x: 'abc' } })).toBe('abc'); + }); it('applies upper filter', () => { expect(evalExpr('item.name | upper', { item: { name: 'hello' } })).toBe('HELLO'); }); diff --git a/src/pipeline/template.ts b/src/pipeline/template.ts index fb7ed66c5..bc93160b7 100644 --- a/src/pipeline/template.ts +++ b/src/pipeline/template.ts @@ -100,8 +100,16 @@ function applyFilter(filterExpr: string, value: unknown): unknown { } case 'replace': { if (typeof value !== 'string') return value; - const parts = rawArgs?.split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')) ?? []; - return parts.length >= 2 ? value.replaceAll(parts[0], parts[1]) : value; + if (rawArgs == null) return value; + // Split on the FIRST comma only so the replacement value may contain commas, + // e.g. replace('a', 'x,y') -> 'x,y'. (A literal comma needle is unsupported by + // design: the comma is the argument separator.) + const ci = rawArgs.indexOf(','); + if (ci === -1) return value; + const strip = (s: string) => s.trim().replace(/^['"]|['"]$/g, ''); + const oldVal = strip(rawArgs.slice(0, ci)); + const newVal = strip(rawArgs.slice(ci + 1)); + return value.replaceAll(oldVal, newVal); } case 'keys': return value && typeof value === 'object' ? Object.keys(value) : value;