fix(artifacts): retain source identity failures after relocation

This commit is contained in:
morluto committed 2026-10-26 15:15:00 +08:00
1 parent 0c147a4a70
commit 2012efe5fd
2 files changed
+72 -3

No files matched your search

+15 -1
View File
@@ -135,7 +135,21 @@ export class ArtifactFileSource {
const source = await this.#opening;
const [observed, selected] = await Promise.all([
source.handle.stat(),
lstat(this.path),
lstat(this.path).catch((cause: unknown) => {
// Admission already established this selected name. Its disappearance
// or a replaced ancestor is a source change, not an unrelated I/O fault.
if (
cause instanceof Error &&
"code" in cause &&
(cause.code === "ENOENT" || cause.code === "ENOTDIR")
)
throw new ArtifactReaderFailure(
"integrity",
`${this.diagnosticLabel} source identity changed: ${this.path}`,
{ cause },
);
throw cause;
}),
]);
if (
!sameRegularFileState(source.initial, observed) ||
@@ -1,8 +1,8 @@
import { withPyInstallerReader } from "../../../tests/fixtures/pyinstallerReader.js";
import { join } from "node:path";
import { dirname, join } from "node:path";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { createHash, randomBytes } from "node:crypto";
import { writeFile, type FileHandle } from "node:fs/promises";
import { rename, unlink, writeFile, type FileHandle } from "node:fs/promises";
import { buffer } from "node:stream/consumers";
import { deflateSync } from "node:zlib";
import { setImmediate } from "node:timers/promises";
@@ -10,6 +10,8 @@ import { expect, it, vi } from "vitest";
import { PyInstallerArtifactReader } from "../PyInstallerArtifactReader.js";
import { OwnedFileHandle } from "../../filesystem/OwnedFileHandle.js";
import { hashReadable } from "../ArtifactHash.js";
import { translateArtifactFailure } from "../ArtifactOperationFailure.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import { marshalCode } from "../../python/PythonPyc.fixture.js";
import { PyInstallerPyzStore } from "./PyInstallerPyzStore.js";
import {
@@ -279,6 +281,59 @@ it("binds materialization to the inventoried digest and checks cached carrier fr
});
});
it.each(["ENOENT", "ENOTDIR"] as const)(
"retains changed-source evidence when an admitted carrier becomes unaddressable (%s)",
async (code) => {
const body = Buffer.from("observed before the source moved");
const bytes = archive(body);
await withPyInstallerReader(bytes, async (reader, handle, path) => {
await reader.prepareContainer(hash(bytes));
const [entry] = await members(reader);
if (entry === undefined) throw new Error("Missing original member");
const observed = await hashReadable(await reader.open(entry));
expect(observed).toMatchObject({
sha256: hash(body),
bytes: body.length,
});
const moved = code === "ENOENT" ? path : dirname(path);
await rename(moved, `${moved}.moved`);
if (code === "ENOTDIR") await writeFile(moved, "replaced ancestor");
try {
const attempts = [
() => reader.prepareContainer(hash(bytes)),
() => members(reader),
() => reader.open(entry),
];
for (const attempt of attempts) {
const cause = await attempt().then(
() => undefined,
(failure: unknown) => failure,
);
expect(cause).toMatchObject({
reason: "integrity",
message: `PyInstaller source identity changed: ${path}`,
});
expect(
projectAnalysisError(
translateArtifactFailure("extract_artifact", cause),
),
).toMatchObject({
category: "integrity_mismatch",
details: {
reason: "integrity",
detail: `PyInstaller source identity changed: ${path}`,
},
});
}
expect(handle.fd).toBeGreaterThanOrEqual(0);
} finally {
if (code === "ENOTDIR") await unlink(moved);
await rename(`${moved}.moved`, moved);
}
});
},
);
it("bounds descriptor reads while hashing a selected stored member", async () => {
const payload = Buffer.alloc(2 * 1024 * 1024, 0x61);
await withPyInstallerReader(archive(payload), async (reader, handle) => {