* fix(#1660): fail-closed frontmatter set of object-list fields instead of silent no-op cmdFrontmatterSet reported {updated:true} even when spliceFrontmatter returned the content unchanged, which happened whenever the new value's extractFrontmatter projection equalled the original's — notably for object-list fields like must_haves, whose {path,provides} items flatten to scalar strings under the lossy parser. Detect a no-op (newContent === content) for a dict-valued field and surface an error directing the user to edit the file directly, instead of silently accepting a no-op set. Scalars and scalar arrays round-trip faithfully, so idempotent sets of those are intentionally NOT flagged (two precision regression tests lock this). Folded into frontmatter-cli.test.cjs. * chore(#1660): backfill changeset pr ref to 1664 * refactor(#1660): extract noOpObjectListSetError as pure tested helper (Stryker coverage) cmdFrontmatterSet is not in Stryker's property/unit test set, so the inline no-op detection added survivors that dropped the frontmatter module below its 62% mutation threshold. Extract the detection into a pure exported helper noOpObjectListSetError and unit-test every branch directly (changed content, scalar, scalar-array, null, dict no-op). cmdFrontmatterSet now calls the helper. Same pattern as the #1572 spliceFrontmatter coverage fix.
This commit is contained in:
@@ -503,10 +503,35 @@ function cmdFrontmatterSet(cwd: string, filePath: string, field: string | undefi
|
||||
try { parsedValue = JSON.parse(value as string); } catch { parsedValue = value; }
|
||||
fm[field as string] = parsedValue as FrontmatterValue;
|
||||
const newContent = spliceFrontmatter(content, fm);
|
||||
// #1660: a no-op set (newContent unchanged) with a dict-valued field means the lossy
|
||||
// frontmatter parser made the new value's projection equal the original's — the change
|
||||
// did not apply (bites object-list fields like must_haves). Detection lives in the pure
|
||||
// exported helper noOpObjectListSetError so the mutation gate (property/unit set) covers
|
||||
// it — the cmd path itself is not in that set.
|
||||
const noOpErr = noOpObjectListSetError(content, newContent, parsedValue);
|
||||
if (noOpErr) {
|
||||
output({ error: noOpErr, field }, raw, undefined);
|
||||
return;
|
||||
}
|
||||
platformWriteSync(fullPath, newContent);
|
||||
output({ updated: true, field, value: parsedValue }, raw, 'true');
|
||||
}
|
||||
|
||||
/**
|
||||
* #1660: detect a frontmatter `set` that would be a silent no-op on a dict-valued field.
|
||||
* Returns an error message when the splice produced no content change but the new value
|
||||
* is a dict (object-list fields like must_haves, whose `{path, provides}` items flatten to
|
||||
* scalar strings under extractFrontmatter so a replacement can deep-equal the original's
|
||||
* projection), else null. Scalars and scalar arrays round-trip faithfully, so idempotent
|
||||
* sets of those are intentionally NOT flagged. Pure and unit-tested directly (the cmd path
|
||||
* is not in Stryker's property/unit set, so the detection must be testable in isolation).
|
||||
*/
|
||||
function noOpObjectListSetError(originalContent: string, newContent: string, parsedValue: unknown): string | null {
|
||||
if (newContent !== originalContent) return null;
|
||||
if (parsedValue === null || typeof parsedValue !== 'object' || Array.isArray(parsedValue)) return null;
|
||||
return 'frontmatter set had no effect — the supplied value is equivalent to the existing field under the frontmatter parser, which cannot faithfully round-trip object-list fields like must_haves. Edit the file directly.';
|
||||
}
|
||||
|
||||
function cmdFrontmatterMerge(cwd: string, filePath: string, data: string | undefined, raw: boolean): void {
|
||||
if (!filePath || !data) { error('file and data required'); }
|
||||
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
||||
@@ -543,6 +568,7 @@ export = {
|
||||
parseFrontmatter: extractFrontmatter,
|
||||
reconstructFrontmatter,
|
||||
spliceFrontmatter,
|
||||
noOpObjectListSetError,
|
||||
parseMustHavesBlock,
|
||||
FRONTMATTER_SCHEMAS,
|
||||
cmdFrontmatterGet,
|
||||
|
||||
Reference in New Issue
Block a user