Skip to content

Commit a811c12

Browse files
fix(ui): quote YAML scalars containing embedded newlines (#7820)
yamlScalar decided a value needs quoting from value.trim() !== value plus a special-character regex. trim() only strips edge whitespace, and the regex omitted line breaks, so a value with an embedded newline and no other flagged character (e.g. "claude 3.7\nignore_findings true") was emitted as a raw multi-line scalar -- a stray top-level line that breaks the generated .loopover.yml, violating the file's "a partial form never produces invalid output" invariant. Same incomplete-guard bug class as the csv-export fix. Add \n and \r to the needsQuote regex so any line-break-containing value goes through the existing JSON.stringify quoting path. Adds embedded-newline and carriage-return regression cases to config-generator-yaml.test.ts. Closes #7786 Co-authored-by: jaytbarimbao-collab <300663773+jaytbarimbao-collab@users.noreply.github.com>
1 parent 9c9188b commit a811c12

2 files changed

Lines changed: 26 additions & 3 deletions

File tree

apps/loopover-ui/src/lib/config-generator-yaml.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,24 @@ describe("formStateToYaml", () => {
7676
[HEADER, "gate:", " aiReview:", ` model: ${JSON.stringify(" claude ")}`].join("\n"),
7777
);
7878
});
79+
80+
// Regression (#7786): an embedded newline is not caught by trim() (which only strips edges) and is
81+
// none of the flagged special chars, so the old guard emitted it as a raw multi-line scalar that
82+
// broke the generated .loopover.yml with a stray top-level line.
83+
it("quotes a model value containing an embedded newline", () => {
84+
const model = "claude 3.7\nignore_findings true";
85+
const output = formStateToYaml({ gate: { aiReview: { model } } });
86+
expect(output).toBe(
87+
[HEADER, "gate:", " aiReview:", ` model: ${JSON.stringify(model)}`].join("\n"),
88+
);
89+
// The serialized value must stay on a single line -- no stray line beyond the 4 expected ones.
90+
expect(output.split("\n")).toHaveLength(4);
91+
});
92+
93+
it("quotes a model value containing a carriage return", () => {
94+
const model = "claude\r3.7";
95+
expect(formStateToYaml({ gate: { aiReview: { model } } })).toBe(
96+
[HEADER, "gate:", " aiReview:", ` model: ${JSON.stringify(model)}`].join("\n"),
97+
);
98+
});
7999
});

apps/loopover-ui/src/lib/config-generator-yaml.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ export type GeneratorFormState = {
2525
const YAML_HEADER = "# .loopover.yml — generated by the config generator";
2626

2727
/** Plain-scalar YAML value needs quoting when it would otherwise be ambiguous (a YAML special
28-
* character, leading/trailing whitespace, or the empty string). Model names are free text, so this
29-
* is a real correctness need, not defensive-for-its-own-sake. */
28+
* character, an embedded line break, leading/trailing whitespace, or the empty string). Model names
29+
* are free text, so this is a real correctness need, not defensive-for-its-own-sake. `value.trim()`
30+
* only catches whitespace at the edges, so an embedded `\n`/`\r` must be flagged explicitly (#7786)
31+
* or `formStateToYaml` would emit it as a raw, file-breaking multi-line scalar. */
3032
function yamlScalar(value: string): string {
31-
const needsQuote = value === "" || value.trim() !== value || /[:#[\]{}&*!|>'"%@`,]/.test(value);
33+
const needsQuote =
34+
value === "" || value.trim() !== value || /[\n\r:#[\]{}&*!|>'"%@`,]/.test(value);
3235
return needsQuote ? JSON.stringify(value) : value;
3336
}
3437

0 commit comments

Comments
 (0)