From 2012efe5fd95ea3b1c9783b33ce33d6ba00fd412 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:14:59 +0800 Subject: [PATCH] fix(artifacts): retain source identity failures after relocation --- src/artifacts/ArtifactFileSource.ts | 16 ++++- .../pyinstaller/PyInstallerBoundaries.test.ts | 59 ++++++++++++++++++- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/artifacts/ArtifactFileSource.ts b/src/artifacts/ArtifactFileSource.ts index 2711ad465b..887fd13dc8 100644 --- a/src/artifacts/ArtifactFileSource.ts +++ b/src/artifacts/ArtifactFileSource.ts @@ -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) || diff --git a/src/artifacts/pyinstaller/PyInstallerBoundaries.test.ts b/src/artifacts/pyinstaller/PyInstallerBoundaries.test.ts index 93fc2c32f5..2c948fcac4 100644 --- a/src/artifacts/pyinstaller/PyInstallerBoundaries.test.ts +++ b/src/artifacts/pyinstaller/PyInstallerBoundaries.test.ts @@ -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) => {