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.
This commit is contained in:
morluto 2026-10-12 15:02:43 +08:00
parent d4e8ef7499
commit b3dec005bc
3 changed files with 26 additions and 9 deletions

View File

@ -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",

View File

@ -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

View File

@ -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);