test(artifacts): respect destination filesystem case sensitivity

This commit is contained in:
morluto
2026-10-10 15:29:42 +08:00
parent cbcba2902d
commit b12d0c6361
2 changed files with 64 additions and 32 deletions
@@ -100,7 +100,6 @@ describe("entry-order validation and reader controls", () => {
it.each([
["pkg/data.txt", "pkg/data.txt/"],
["pkg/data.txt/", "pkg/data.txt"],
["pkg/data.txt", "PKG/other.txt"],
["pkg/data.txt", "pkg"],
["../escape.txt"],
])("keeps invalid/colliding paths rejected for %j", async (...entries) => {
@@ -112,6 +111,25 @@ describe("entry-order validation and reader controls", () => {
});
});
it("keeps case-distinct directory names separate in either entry order", async () => {
const root = await createTestTempDirectory("rea-entry-order-case-");
const archive = join(root, "case-distinct.zip");
const entries = ["pkg/", "pkg/data.txt", "PKG/", "PKG/other.txt"];
const parents = {
".": null,
pkg: ".",
"pkg/data.txt": "pkg",
PKG: ".",
"PKG/other.txt": "PKG",
};
for (const ordered of [entries, entries.toReversed()]) {
await writeOrderedZip(archive, ordered);
expect(artifactParentPaths(await inventoryArtifact(archive))).toEqual(
parents,
);
}
});
it("preserves ordinary directory and ASAR structure", async () => {
const root = await createTestTempDirectory("rea-entry-order-readers-");
const directory = join(root, "source");
@@ -1,4 +1,4 @@
import { readFile, writeFile } from "node:fs/promises";
import { lstat, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
@@ -20,6 +20,7 @@ import { artifactInventoryResultSchema } from "../../../../src/domain/artifactGr
import type { BinaryTarget } from "../../../../src/domain/binaryTargetTypes.js";
import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js";
import { parseEvidence } from "../../../../src/domain/evidence.js";
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
const { runProviderAnalysis } = createDirectAnalysis({});
@@ -136,38 +137,51 @@ describe("artifact archive safety", () => {
destinationCaseCollisionMessage("res/2f.xml", ["2f.xml", "2F.xml"]),
).toBeUndefined();
});
});
it("extracts logical names that differ only in case", async () => {
const root = await createTestTempDirectory("rea-case-distinct-zip-");
const path = join(root, "fixture.zip");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("Main.js", new TextReader("upper"));
await writer.add("main.js", new TextReader("lower"));
await writeFile(path, await writer.close());
const parsed = await parseBinaryTarget(path);
expect(parsed.ok).toBe(true);
if (!parsed.ok) return;
const result = await inventory(parsed.value);
expect(result.limitations).toContain(
ENGLISH_UNICODE_CASE_INVENTORY_LIMITATION,
it("extracts case-distinct names only when the destination can preserve them", async () => {
const root = await createTestTempDirectory("rea-case-distinct-zip-");
await writeFile(join(root, "case-probe"), "lower");
await writeFile(join(root, "CASE-PROBE"), "upper");
const caseSensitive =
(await readFile(join(root, "case-probe"), "utf8")) === "lower";
const path = join(root, "fixture.zip");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("Main.js", new TextReader("upper"));
await writer.add("main.js", new TextReader("lower"));
await writeFile(path, await writer.close());
const parsed = await parseBinaryTarget(path);
if (!parsed.ok) throw parsed.error;
const result = await inventory(parsed.value);
expect(result.limitations).toContain(
ENGLISH_UNICODE_CASE_INVENTORY_LIMITATION,
);
expect(
result.occurrences
.filter(({ logical_path }) => logical_path !== ".")
.map(({ logical_path }) => logical_path)
.sort(),
).toEqual(["Main.js", "main.js"]);
const output = join(root, "output");
const extracted = await new ArtifactProvider(process.env)
.createClient(parsed.value)
.execute(
"extract_artifact",
artifactExtractionExecutionSchema.parse({ output_root: output }),
);
expect(
result.occurrences
.filter(({ logical_path }) => logical_path !== ".")
.map(({ logical_path }) => logical_path)
.sort(),
).toEqual(["Main.js", "main.js"]);
const output = join(root, "output");
const extracted = await new ArtifactProvider(process.env)
.createClient(parsed.value)
.execute(
"extract_artifact",
artifactExtractionExecutionSchema.parse({ output_root: output }),
);
expect(extracted.ok).toBe(true);
expect(await readFile(join(output, "Main.js"), "utf8")).toBe("upper");
expect(await readFile(join(output, "main.js"), "utf8")).toBe("lower");
});
if (!caseSensitive) {
if (extracted.ok) throw new Error("expected a destination case collision");
expect(projectAnalysisError(extracted.error)).toMatchObject({
message:
"Destination filesystem cannot store both main.js and Main.js; inventory retains both logical names.",
details: { operation: "extract_artifact", reason: "path" },
});
await expect(lstat(output)).rejects.toMatchObject({ code: "ENOENT" });
return;
}
if (!extracted.ok) throw extracted.error;
expect(await readFile(join(output, "Main.js"), "utf8")).toBe("upper");
expect(await readFile(join(output, "main.js"), "utf8")).toBe("lower");
});
const inventory = async (targetValue: BinaryTarget) => {
const result = await new ArtifactProvider(process.env)