From 3896f725524d7ee360a4532399bdc785363caaef Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 11 Nov 2026 17:19:37 +0800 Subject: [PATCH] fix(environment): preserve null bodies and unique member matches (#10014) --- .../environmentCompareBody.test.ts | 22 +++++++++++++++++++ .../environment/environmentCompareDiff.ts | 4 +++- .../environmentCompareIdsIntegration.test.ts | 19 ++++++++++++++++ .../environment/environmentCompareProbe.ts | 7 +++--- 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/application/environment/environmentCompareBody.test.ts b/src/application/environment/environmentCompareBody.test.ts index fc3ef07848..70232dc778 100644 --- a/src/application/environment/environmentCompareBody.test.ts +++ b/src/application/environment/environmentCompareBody.test.ts @@ -166,3 +166,25 @@ it.each([ expect(requestBody(capture.records[0]!)).toEqual(expected); }, ); + +it.each([ + ["null", null], + ["false", false], + ["0", 0], + ['""', ""], + ["not-json", "not-json"], +] as const)( + "preserves the recorded request JSON value %s", + (text, expected) => { + const capture = environmentManifestCaptureFixture([ + { + request: { + method: "POST", + url: "http://app.example/api/value", + postData: { mimeType: "application/json", text }, + }, + }, + ]); + expect(requestBody(capture.records[0]!)).toEqual(expected); + }, +); diff --git a/src/application/environment/environmentCompareDiff.ts b/src/application/environment/environmentCompareDiff.ts index 25d2db8c13..020db4ced3 100644 --- a/src/application/environment/environmentCompareDiff.ts +++ b/src/application/environment/environmentCompareDiff.ts @@ -429,7 +429,9 @@ class Differ { for (const [key, value] of Object.entries(live)) { const child = `${pointer}/${escapePointer(key)}`; const other = envKey(key); - if (!Object.hasOwn(env, other)) + // A translated key and a verbatim key may name the same entry; an + // environment member can account for only one live member. + if (!Object.hasOwn(env, other) || matched.has(other)) this.add({ category: "missing-field", pointer: child, diff --git a/src/application/environment/environmentCompareIdsIntegration.test.ts b/src/application/environment/environmentCompareIdsIntegration.test.ts index b96785734a..1af9baf20c 100644 --- a/src/application/environment/environmentCompareIdsIntegration.test.ts +++ b/src/application/environment/environmentCompareIdsIntegration.test.ts @@ -26,6 +26,25 @@ const compareRows = ( return report.value.probes[0]!; }; +describe("environment comparison collection matching", () => { + it("does not match two live object keys to one environment entry", () => { + const probe = compareRows( + [ + ["GET", "/api/items", [{ id: 1, name: "A" }]], + ["GET", "/api/counts", { counts: { "1": 5 } }], + ], + [ + ["GET", "/api/items", [{ id: 101, name: "A" }]], + ["GET", "/api/counts", { counts: { "1": 5, "101": 5 } }], + ], + ); + expect(probe.results.map((step) => step.equal)).toEqual([true, false]); + expect(probe.results[1]?.differences).toMatchObject([ + { category: "missing-field", pointer: "/counts/101" }, + ]); + }); +}); + describe("environment comparison identifier semantics", () => { it("reports changed business tokens and numeric arrays through the application comparison boundary", () => { const probe = compareRows( diff --git a/src/application/environment/environmentCompareProbe.ts b/src/application/environment/environmentCompareProbe.ts index da277d7bce..41f1977041 100644 --- a/src/application/environment/environmentCompareProbe.ts +++ b/src/application/environment/environmentCompareProbe.ts @@ -78,9 +78,10 @@ export const requestBody = ( return undefined; const { mimeType, text } = facts.postData; if (mimeType === undefined || text === undefined) return undefined; - return isJsonLike(mediaTypeEssence(mimeType)) - ? (parseExactJson(text) ?? text) - : text; + const value = isJsonLike(mediaTypeEssence(mimeType)) + ? parseExactJson(text) + : undefined; + return value === undefined ? text : value; }; /** The application's answer as an environment answer carries it: JSON, text, or null when it is empty; a body the probe did not record is marked. */