From 35eec3f20c7f94afbf30a716f62f85f19e86252d Mon Sep 17 00:00:00 2001 From: gensokyo-dev Date: Mon, 19 Oct 2026 19:55:48 +0000 Subject: [PATCH] fix(environment): report changed export inputs as tagged failures --- .../environment/environmentExport.test.ts | 38 +++++++++++++++++++ .../environment/environmentExport.ts | 13 +++++++ 2 files changed, 51 insertions(+) diff --git a/src/application/environment/environmentExport.test.ts b/src/application/environment/environmentExport.test.ts index 889bea962d..c7d15a5076 100644 --- a/src/application/environment/environmentExport.test.ts +++ b/src/application/environment/environmentExport.test.ts @@ -12,13 +12,16 @@ import { import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js"; import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; import { PACKAGE_METADATA } from "../../generatedPackageMetadata.js"; +import { RegularFileChangedError } from "../../filesystem/RegularFile.js"; import { nativeJsonParseRequiredBytes } from "../JsonInputFile.js"; +import * as regularFileRead from "../RegularFileRead.js"; import { exportEnvironmentBundle, readEnvironmentExportInputs, } from "./environmentExport.js"; vi.mock("node:v8", { spy: true }); +vi.mock("../RegularFileRead.js", { spy: true }); const listFiles = async (root: string): Promise => (await readdir(root, { recursive: true, withFileTypes: true })) @@ -44,6 +47,41 @@ const prepare = async (prefix: string) => { }; describe("readEnvironmentExportInputs", () => { + it.each(["manifest", "evidence"] as const)( + "returns a tagged input error when the %s snapshot changes", + async (role) => { + const root = await createTestTempDirectory( + "rea-environment-input-changed-", + ); + const files = await writeEnvironmentServeFiles(root); + const changedPath = + role === "manifest" ? files.manifestPath : files.capturePath; + const actual = await vi.importActual( + "../RegularFileRead.js", + ); + const reader = vi.mocked(regularFileRead.readRegularFile); + reader.mockImplementation(async (path, options) => { + if (path === changedPath) throw new RegularFileChangedError(path); + return actual.readRegularFile(path, options); + }); + try { + const result = await readEnvironmentExportInputs( + files.manifestPath, + files.capturePath, + ); + if (result.ok) throw new Error("Expected changed input refusal"); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "invalid_request", + details: { + operation: "environment-export", + issues: [{ path: [role], reason: "invalid_value" }], + }, + }); + } finally { + reader.mockImplementation(actual.readRegularFile); + } + }, + ); it("reads each file once as the bytes on disk and the value they parse to", async () => { const root = await createTestTempDirectory("rea-environment-inputs-"); const fixture = environmentServeFixture(); diff --git a/src/application/environment/environmentExport.ts b/src/application/environment/environmentExport.ts index 7847f63f9b..2953f8effb 100644 --- a/src/application/environment/environmentExport.ts +++ b/src/application/environment/environmentExport.ts @@ -12,6 +12,7 @@ import { } from "../../domain/analysisErrorCore.js"; import type { JsonValue } from "../../domain/jsonValue.js"; import { err, ok, type Result } from "../../domain/result.js"; +import { RegularFileChangedError } from "../../filesystem/RegularFile.js"; import { sealOwnedJsonInput } from "../OwnedJsonInput.js"; import { nativeJsonParseRequiredBytes } from "../JsonInputFile.js"; import { readRegularFile } from "../RegularFileRead.js"; @@ -219,6 +220,18 @@ const readInput = async ( }); } catch (cause: unknown) { if (cause instanceof AnalysisResourceConstraintError) return err(cause); + if (cause instanceof RegularFileChangedError) + return err( + inputError( + { + path: [role], + reason: "invalid_value", + expected: "one stable regular file snapshot", + message: `The ${role} file changed while it was read.`, + }, + `${path} changed while being read; retry with stable input files.`, + ), + ); if (NOT_A_FILE_CODES.has(systemCode(cause) ?? "")) return err( inputError(