From b3dec005bcb89b5afe6e0aa41e5251f8a3a31de7 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:02:43 +0800 Subject: [PATCH] test(hopper): align startup fixtures with cleanup and lease contracts Inspect the typed primary startup failure retained by aggregate cleanup errors. Materialize isolated targets before real lease acquisition so launcher scenarios reach the behavior under test. Repairs the Linux shard and Windows curated test failures without weakening cleanup retention coverage. --- .../providers/hopper/bridgeLauncher.test.ts | 4 ++- .../hopper/hopperClient.cleanupRetry.test.ts | 6 ++--- .../hopper/hopperClient.helperFailure.test.ts | 25 +++++++++++++++---- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/tests/boundary/providers/hopper/bridgeLauncher.test.ts b/tests/boundary/providers/hopper/bridgeLauncher.test.ts index c0da10224..722a25bed 100644 --- a/tests/boundary/providers/hopper/bridgeLauncher.test.ts +++ b/tests/boundary/providers/hopper/bridgeLauncher.test.ts @@ -165,10 +165,12 @@ describe("Hopper bridge launcher selection", () => { it("launches Hopper helpers with the caller-selected environment without restoring ambient values", async () => { const directory = await createTestTempDirectory("rea-hopper-selected-env-"); + const targetPath = join(directory, "target"); + await writeFile(targetPath, "fixture target"); const launcher = new HopperApplicationLauncher({ environment: { REA_SELECTED_VALUE: "caller-value" }, launcherPath: process.execPath, - targetPath: join(directory, "target"), + targetPath, targetKind: "executable", loaderArgs: [ "-e", diff --git a/tests/boundary/providers/hopper/hopperClient.cleanupRetry.test.ts b/tests/boundary/providers/hopper/hopperClient.cleanupRetry.test.ts index 030a192ac..62310facf 100644 --- a/tests/boundary/providers/hopper/hopperClient.cleanupRetry.test.ts +++ b/tests/boundary/providers/hopper/hopperClient.cleanupRetry.test.ts @@ -107,7 +107,7 @@ const failedStartup = async (launcher: RetryCleanupLauncher) => { error: { _tag: "ProviderAdapterError", cleanupIncomplete: true, - cause: { _tag: "HopperTimeoutError" }, + cause: { cause: { _tag: "HopperTimeoutError" } }, }, }); const session = launcher.sessions[0]; @@ -233,7 +233,7 @@ it("blocks a fresh launch while cleanup remains unverified", async () => { ok: false, error: { cleanupIncomplete: true, - cause: { _tag: "HopperTimeoutError" }, + cause: { cause: { _tag: "HopperTimeoutError" } }, diagnostics: { primary_error: { code: "provider_timeout" } }, }, }); @@ -263,7 +263,7 @@ it("preserves failed startup state through a blocked retry until cleanup succeed error: { _tag: "ProviderAdapterError", cleanupIncomplete: true, - cause: { _tag: "HopperTimeoutError" }, + cause: { cause: { _tag: "HopperTimeoutError" } }, }, }); // ...but must not wipe the failed-startup state: the client still diff --git a/tests/boundary/providers/hopper/hopperClient.helperFailure.test.ts b/tests/boundary/providers/hopper/hopperClient.helperFailure.test.ts index 13601e172..06e725dc1 100644 --- a/tests/boundary/providers/hopper/hopperClient.helperFailure.test.ts +++ b/tests/boundary/providers/hopper/hopperClient.helperFailure.test.ts @@ -1,8 +1,11 @@ import { spawn } from "node:child_process"; -import { access } from "node:fs/promises"; +import { access, writeFile } from "node:fs/promises"; +import { join } from "node:path"; import { expect, it, onTestFinished } from "vitest"; +import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; + import { AnalysisError } from "../../../../src/domain/analysisErrorBase.js"; import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js"; import { ok } from "../../../../src/domain/result.js"; @@ -67,17 +70,23 @@ const trackFailedHelperClient = ( const projectPrimaryStartupFailure = (error: AnalysisError) => { if (!error.cleanupIncomplete) return projectAnalysisError(error); expect(projectAnalysisError(error).code).toBe("cleanup_incomplete"); - const primary = error.cause; + const cause = error.cause; + if (!(cause instanceof AggregateError)) + throw new Error("Cleanup failure did not retain its aggregate causes"); + const primary = cause.cause; if (!(primary instanceof AnalysisError)) throw new Error("Cleanup failure lost its typed startup cause"); return projectAnalysisError(primary); }; it("does not equate an owned native helper exit with an external GUI exit", async () => { + const directory = await createTestTempDirectory("rea-hopper-helper-failure-"); + const targetPath = join(directory, "target"); + await writeFile(targetPath, "fixture target"); const launcher = new HopperApplicationLauncher({ environment: {}, launcherPath: process.execPath, - targetPath: "/target", + targetPath, targetKind: "executable", loaderArgs: [ "-e", @@ -91,7 +100,10 @@ it("does not equate an owned native helper exit with an external GUI exit", asyn const result = await client.start(); expect(result).toMatchObject({ ok: false, - error: { cleanupIncomplete: true, cause: { _tag: "HopperStartError" } }, + error: { + cleanupIncomplete: true, + cause: { cause: { _tag: "HopperStartError" } }, + }, }); if (result.ok) throw new Error("Expected the native helper to fail"); expect(projectPrimaryStartupFailure(result.error)).toMatchObject({ @@ -140,7 +152,10 @@ it.each(["exit", "signal", "exit75"] as const)( const result = await client.start(); expect(result).toMatchObject({ ok: false, - error: { cleanupIncomplete: true, cause: { _tag: "HopperStartError" } }, + error: { + cleanupIncomplete: true, + cause: { cause: { _tag: "HopperStartError" } }, + }, }); if (result.ok) throw new Error("Expected the launcher to fail"); const projected = projectPrimaryStartupFailure(result.error);