|
| 1 | +import { describe, it, expect } from 'vitest'; |
| 2 | +import { parse as parseYaml } from 'yaml'; |
| 3 | +import { escapeYamlValue } from '../../../src/core/command-generation/yaml.js'; |
| 4 | + |
| 5 | +/** |
| 6 | + * Parses a single-key YAML document and returns the round-tripped value. |
| 7 | + * |
| 8 | + * @param value - The raw string to escape and round-trip through YAML. |
| 9 | + * @returns The value as read back by a real YAML parser. |
| 10 | + */ |
| 11 | +function roundTrip(value: string): unknown { |
| 12 | + const doc = `key: ${escapeYamlValue(value)}\n`; |
| 13 | + return parseYaml(doc).key; |
| 14 | +} |
| 15 | + |
| 16 | +describe('command-generation/yaml escapeYamlValue', () => { |
| 17 | + it('returns the value unquoted when no special characters are present', () => { |
| 18 | + expect(escapeYamlValue('Enter explore mode for thinking')).toBe( |
| 19 | + 'Enter explore mode for thinking' |
| 20 | + ); |
| 21 | + }); |
| 22 | + |
| 23 | + it('quotes values containing a colon', () => { |
| 24 | + expect(escapeYamlValue('Fix: regression')).toBe('"Fix: regression"'); |
| 25 | + }); |
| 26 | + |
| 27 | + it('escapes embedded double quotes', () => { |
| 28 | + expect(escapeYamlValue('Fix the "auth" feature')).toBe( |
| 29 | + '"Fix the \\"auth\\" feature"' |
| 30 | + ); |
| 31 | + }); |
| 32 | + |
| 33 | + it('escapes backslashes before other characters', () => { |
| 34 | + expect(escapeYamlValue('path\\to:thing')).toBe('"path\\\\to:thing"'); |
| 35 | + }); |
| 36 | + |
| 37 | + it('escapes line feeds', () => { |
| 38 | + expect(escapeYamlValue('Line 1\nLine 2')).toBe('"Line 1\\nLine 2"'); |
| 39 | + }); |
| 40 | + |
| 41 | + it('escapes carriage returns', () => { |
| 42 | + // Regression: \r is detected as needing quoting but was previously left |
| 43 | + // as a literal CR inside the double-quoted scalar. |
| 44 | + expect(escapeYamlValue('Line 1\rLine 2')).toBe('"Line 1\\rLine 2"'); |
| 45 | + }); |
| 46 | + |
| 47 | + it('escapes CRLF sequences', () => { |
| 48 | + expect(escapeYamlValue('Line 1\r\nLine 2')).toBe('"Line 1\\r\\nLine 2"'); |
| 49 | + }); |
| 50 | + |
| 51 | + it('quotes values with leading or trailing whitespace', () => { |
| 52 | + expect(escapeYamlValue(' leading')).toBe('" leading"'); |
| 53 | + expect(escapeYamlValue('trailing ')).toBe('"trailing "'); |
| 54 | + }); |
| 55 | + |
| 56 | + describe('round-trips through a real YAML parser', () => { |
| 57 | + const cases: Array<[string, string]> = [ |
| 58 | + ['plain', 'Enter explore mode'], |
| 59 | + ['colon', 'Fix: regression in parser'], |
| 60 | + ['double quotes', 'Fix the "auth" feature'], |
| 61 | + ['backslash', 'path\\to\\thing'], |
| 62 | + ['line feed', 'Line 1\nLine 2'], |
| 63 | + ['carriage return', 'Line 1\rLine 2'], |
| 64 | + ['crlf', 'Line 1\r\nLine 2'], |
| 65 | + ['mixed special', 'a: "b"\r\n#c\\d'], |
| 66 | + ]; |
| 67 | + |
| 68 | + for (const [label, value] of cases) { |
| 69 | + it(`preserves the value: ${label}`, () => { |
| 70 | + expect(roundTrip(value)).toBe(value); |
| 71 | + }); |
| 72 | + } |
| 73 | + }); |
| 74 | +}); |
0 commit comments