From 11feab01da77339943d88a2f12e09f8f7d76aa37 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 07:04:01 +0800 Subject: [PATCH] fix(environment): return input permission refusals through Result --- .../environmentBundleWrite.test.ts | 25 +++++++++++-------- .../environment/environmentExport.test.ts | 5 ++++ .../environment/environmentExport.ts | 9 +++---- .../environmentStateAnswer.test.ts | 2 +- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/application/environment/environmentBundleWrite.test.ts b/src/application/environment/environmentBundleWrite.test.ts index 5a10e337c9..8422eb3de8 100644 --- a/src/application/environment/environmentBundleWrite.test.ts +++ b/src/application/environment/environmentBundleWrite.test.ts @@ -20,10 +20,7 @@ import { } from "../../../tests/fixtures/environmentServe.js"; import { SafeOutputTree } from "../../artifacts/SafeOutputTree.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; -import { - AnalysisAccessDeniedError, - AnalysisOutputError, -} from "../../domain/analysisErrorCore.js"; +import { AnalysisOutputError } from "../../domain/analysisErrorCore.js"; import { exportEnvironmentBundle, readEnvironmentExportInputs, @@ -190,9 +187,14 @@ it.skipIf(process.platform === "win32")( expect(result).toMatchObject({ ok: false, error: { - _tag: "AnalysisAccessDeniedError", + _tag: "AnalysisOutputError", operation: "environment-export", - diagnostics: { output_directory: directory }, + diagnostics: { + output_directory: directory, + path: join(directory, "README.md"), + system_code: "EACCES", + boundary: "filesystem-write", + }, }, }); expect(await readdir(directory)).toEqual([]); @@ -232,12 +234,15 @@ it.skipIf(process.platform === "win32")( }); if (result.ok) throw new Error("expected a permission refusal"); expect(result.error).toMatchObject({ - _tag: "AnalysisAccessDeniedError", + _tag: "AnalysisOutputError", diagnostics: { output_directory: directory }, }); - if (!(result.error instanceof AnalysisAccessDeniedError)) - throw result.error; - expect(result.error.path.startsWith(`${directory}/`)).toBe(true); + if (!(result.error instanceof AnalysisOutputError)) throw result.error; + expect(result.error.diagnostics).toMatchObject({ + path: expect.stringContaining(`${directory}/`), + system_code: "EACCES", + boundary: "filesystem-write", + }); expect(await readdir(directory)).not.toEqual([]); } finally { writer.mockRestore(); diff --git a/src/application/environment/environmentExport.test.ts b/src/application/environment/environmentExport.test.ts index 2519c2c602..d14c72b3d2 100644 --- a/src/application/environment/environmentExport.test.ts +++ b/src/application/environment/environmentExport.test.ts @@ -268,6 +268,11 @@ const registerBundleDestinationAdmissionCases = () => { files.capturePath, ); if (readDenied.ok) throw new Error("Expected read denial"); + expect(readDenied.error).toMatchObject({ + _tag: "AnalysisAccessDeniedError", + path: files.manifestPath, + cause: { code: "EACCES", path: files.manifestPath }, + }); expect(projectAnalysisError(readDenied.error)).toMatchObject({ code: "access_denied", details: { boundary: "filesystem-read" }, diff --git a/src/application/environment/environmentExport.ts b/src/application/environment/environmentExport.ts index 859188afc9..d559d80a51 100644 --- a/src/application/environment/environmentExport.ts +++ b/src/application/environment/environmentExport.ts @@ -193,11 +193,6 @@ const hostFailurePath = (cause: unknown): string | undefined => { return undefined; }; -/** Host permission failures retain their typed cause; unexpected failures propagate. */ -const rethrowUnlessDenied = (cause: unknown, path: string): never => { - throw accessDeniedError(cause, path) ?? cause; -}; - /** A path that is not a readable regular file, including inline JSON text offered as one. */ const NOT_A_FILE_CODES = new Set([ "ENOENT", @@ -300,7 +295,9 @@ const readInput = async ( `${path} could not be read as the ${role} file.`, ), ); - return rethrowUnlessDenied(cause, path); + const denied = accessDeniedError(cause, path); + if (denied !== undefined) return err(denied); + throw cause; } let parsed: ReturnType; try { diff --git a/src/application/environment/environmentStateAnswer.test.ts b/src/application/environment/environmentStateAnswer.test.ts index 1642bd0154..6bad107da5 100644 --- a/src/application/environment/environmentStateAnswer.test.ts +++ b/src/application/environment/environmentStateAnswer.test.ts @@ -64,7 +64,7 @@ it("finds state entities using the recorded path's repeated separators", () => { ["GET", "/api//books/1", { id: 1, title: "Book" }], ]); const tool = serving.source.manifest.tools.find( - (candidate) => candidate.path_template === "/api//books/{id}", + (candidate) => candidate.path_template === "/api//books/{numeric}", ); if (tool === undefined) throw new Error("Missing double-separator item tool"); const result = answerFromState(serving, tool, { id: "1" });