From 385445804ab45605a47edcb996ebbc026e2f0a43 Mon Sep 17 00:00:00 2001 From: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Mon, 5 Oct 2026 02:59:35 -0400 Subject: [PATCH] fix(workflows): retain container presence in export shape comparisons (#523) Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com> --- src/domain/javascriptExportShapeVariants.ts | 22 ++++- .../javascriptExportShapeComparison.test.ts | 81 +++++++++++++++++++ 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/src/domain/javascriptExportShapeVariants.ts b/src/domain/javascriptExportShapeVariants.ts index 58cee07734..9db1e888d9 100644 --- a/src/domain/javascriptExportShapeVariants.ts +++ b/src/domain/javascriptExportShapeVariants.ts @@ -251,8 +251,8 @@ const diffPair = ( const rightFields = new Map(right.fields.map((field) => [field.path, field])); const paths = uniqueSorted([...leftFields.keys(), ...rightFields.keys()]); return paths.flatMap((path) => { - const leftField = leftFields.get(path); - const rightField = rightFields.get(path); + const leftField = fieldAtPath(left, leftFields, path); + const rightField = fieldAtPath(right, rightFields, path); const status = fieldChangeStatus({ leftShape: left, rightShape: right, @@ -282,6 +282,24 @@ const diffPair = ( }); }; +const fieldAtPath = ( + shape: Shape, + fields: ReadonlyMap, + path: string, +): Field | undefined => { + const field = fields.get(path); + if (field !== undefined) return field; + if (!shape.property_coverage.some((coverage) => coverage.path === path)) + return undefined; + return { + path, + state: "unknown", + value: null, + reason: + "A static container is retained at this path, but no primitive field value is projected.", + }; +}; + interface FieldChangeInput { readonly leftShape: Shape; readonly rightShape: Shape; diff --git a/tests/boundary/filesystem/javascriptExportShapeComparison.test.ts b/tests/boundary/filesystem/javascriptExportShapeComparison.test.ts index 03befa093f..76ddddcf99 100644 --- a/tests/boundary/filesystem/javascriptExportShapeComparison.test.ts +++ b/tests/boundary/filesystem/javascriptExportShapeComparison.test.ts @@ -120,6 +120,87 @@ describe("JavaScript export return-shape comparison", () => { }); }); +describe("JavaScript export return-shape container presence", () => { + it.each(["{}", "[]", "{ nested: 1 }"])( + "does not report a retained %s container property as removed", + async (container) => { + const graphs = await analyzeSources({ + left: 'export default () => ({ type: "item", options: false });', + right: `export default () => ({ type: "item", options: ${container} });`, + }); + const result = compare(...graphs); + const change = result.changes.find(({ path }) => path === "/options"); + expect(change).toMatchObject({ + status: "unknown", + left: { availability: "literal", value: false }, + right: { availability: "unknown" }, + }); + expect(() => + javaScriptExportShapeComparisonResultSchema.parse(result), + ).not.toThrow(); + }, + ); + + it.each(["{}", "[]", "{ nested: 1 }"])( + "does not report a retained %s container becoming a primitive as added", + async (container) => { + const graphs = await analyzeSources({ + left: `export default () => ({ type: "item", options: ${container} });`, + right: 'export default () => ({ type: "item", options: false });', + }); + const result = compare(...graphs); + expect( + result.changes.find(({ path }) => path === "/options"), + ).toMatchObject({ + status: "unknown", + left: { availability: "unknown" }, + right: { availability: "literal", value: false }, + }); + }, + ); + + it.each([ + { left: "", right: ", options: false", status: "added" }, + { left: ", options: false", right: "", status: "removed" }, + ])("preserves genuine primitive $status", async ({ left, right, status }) => { + const graphs = await analyzeSources({ + left: `export default () => ({ type: "item"${left} });`, + right: `export default () => ({ type: "item"${right} });`, + }); + expect(compare(...graphs).changes).toEqual([ + expect.objectContaining({ path: "/options", status }), + ]); + }); + + it("keeps a container transition unknown under incomplete spread coverage", async () => { + const graphs = await analyzeSources({ + left: 'export default () => ({ type: "item", options: false });', + right: + 'export default () => ({ type: "item", options: { ...dynamic } });', + }); + const result = compare(...graphs); + expect( + result.changes.find(({ path }) => path === "/options"), + ).toMatchObject({ + status: "unknown", + right: { availability: "unknown" }, + }); + expect(result.coverage.status).toBe("partial"); + }); + + it("preserves identical container leaves and real primitive changes", async () => { + const graphs = await analyzeSources({ + left: 'export default () => ({ type: "item", options: {}, enabled: false });', + right: + 'export default () => ({ type: "item", options: {}, enabled: true });', + }); + const result = compare(...graphs); + expect(result.changes).toEqual([ + expect.objectContaining({ path: "/enabled", status: "changed" }), + ]); + }); +}); + describe("JavaScript export return-shape selection", () => { it("returns complete candidate, variant, and change inventories", async () => { const candidates = await analyzeSources({