mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
98ec00f406
* fix(setup): report host-owned cleanup resources alongside the primary's
SetupHost.close documents that it reports the resources a setup host still
owns, but its contract returned only a string. runSetup therefore built the
ProviderCleanupError from the primary failure's cleanupResources alone, so a
resource the host failed to release reached the projection only inside a
free-text reason while the error asks the caller to review the reported local
resources.
Return a SetupCloseReport instead. closeSetupResources absorbs the structured
resources of any cause that reports them, through
ArtifactReaderFailure.cleanupObservation, and leaves opaque failures
message-only so no resource name is invented. runSetup then unions the host's
resources with the primary's before building the typed cleanup error.
Two boundary tests read the close result directly: the skill-cleanup case now
asserts the retained backup path in the report reason rather than in a bare
string, and the shared ownership helper takes an opaque close result because it
only ever awaits undefined from either host it is given.
Validation: RED then green in src/application/Setup.recovery.test.ts, where
the new case previously received only the primary resource; 116 tests across
the seven Setup and cleanup-ownership suites pass; tsc --noEmit over the
changed set, its setup callers, and both boundary tests exits 0; oxlint
reports 0 errors; oxfmt --check is clean; verify:module-boundaries passes.
The two failures in src/setupEnvironment.test.ts reproduce identically on
pristine bded9c628 with this change stashed.
* fix(setup): preserve cleanup ownership through host reports
* fix(setup): bind finalization reports to cleanup owners
* fix(setup): retain live resources in returned cleanup views
* fix(setup): preserve completed cleanup and exact installer resources
* fix(setup): retain delegated cleanup across authority changes
* fix(setup): compose callback and source cleanup authorities
* fix(setup): retain producer cleanup failure reasons
* fix(cli): render structured setup cleanup resources
* fix(setup): separate failure evidence from cleanup ownership
* fix(setup): normalize close failures and unify lease binding
* fix(setup): preserve direct lease resource attribution
---------
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
133 lines
4.8 KiB
TypeScript
133 lines
4.8 KiB
TypeScript
import { mkdir, rm, stat } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { afterEach, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
installLinuxHopper,
|
|
systemLinuxHopperInstallHost,
|
|
} from "../../../src/application/LinuxHopper.js";
|
|
import { systemSetupHost } from "../../../src/application/SetupHost.js";
|
|
import { runSetup } from "../../../src/application/Setup.js";
|
|
import { createDoctorHostFixture } from "../../../src/application/Doctor.fixture.js";
|
|
import { AnalysisError } from "../../../src/domain/analysisErrorBase.js";
|
|
import { ConfigurationError } from "../../../src/domain/configurationErrors.js";
|
|
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
vi.mock("../../../src/application/LinuxHopper.js", async (load) => {
|
|
const actual =
|
|
await load<typeof import("../../../src/application/LinuxHopper.js")>();
|
|
return {
|
|
...actual,
|
|
installLinuxHopper: vi.fn(actual.installLinuxHopper),
|
|
systemLinuxHopperInstallHost: vi.fn(actual.systemLinuxHopperInstallHost),
|
|
};
|
|
});
|
|
|
|
afterEach(() => vi.resetAllMocks());
|
|
|
|
it.each(["settled", "late"] as const)(
|
|
"retains and releases the actual temporary directory through setup's Linux cleanup task: %s",
|
|
async (timing) => {
|
|
const home = await createTestTempDirectory("rea-setup-linux-cleanup-");
|
|
const temporary = join(home, "owned-installer-staging");
|
|
await mkdir(temporary);
|
|
let allowCleanup = false;
|
|
const actual = await vi.importActual<
|
|
typeof import("../../../src/application/LinuxHopper.js")
|
|
>("../../../src/application/LinuxHopper.js");
|
|
const child = actual.systemLinuxHopperInstallHost(
|
|
{ HOME: home },
|
|
async () => undefined,
|
|
);
|
|
vi.mocked(systemLinuxHopperInstallHost).mockReturnValue({
|
|
...child,
|
|
cleanup: async (path) => {
|
|
expect(path).toBe(temporary);
|
|
if (!allowCleanup)
|
|
throw new Error(
|
|
"Controlled owned temporary directory cleanup failure",
|
|
);
|
|
await rm(path, { recursive: true });
|
|
},
|
|
});
|
|
// Only the installer outcome is simulated; no package/download action runs.
|
|
let completeInstall: (() => void) | undefined;
|
|
const installGate = new Promise<void>((resolve) => {
|
|
completeInstall = resolve;
|
|
});
|
|
vi.mocked(installLinuxHopper).mockImplementation(async () => {
|
|
if (timing === "late") await installGate;
|
|
return {
|
|
status: "failed",
|
|
reason: "cleanup_failed",
|
|
cleanupFailure: {
|
|
path: temporary,
|
|
reason: "Controlled cleanup failure",
|
|
},
|
|
};
|
|
});
|
|
const closeDoctor = vi.fn(async () => undefined);
|
|
const host = systemSetupHost(
|
|
createDoctorHostFixture({
|
|
platform: "linux",
|
|
homeDirectory: home,
|
|
close: closeDoctor,
|
|
}),
|
|
{ HOME: home },
|
|
);
|
|
try {
|
|
const installing = host.installHopper(false);
|
|
const closing = timing === "late" ? host.close?.() : undefined;
|
|
completeInstall?.();
|
|
expect(await installing).toMatchObject({ status: "failed" });
|
|
if (closing !== undefined)
|
|
expect((await closing)?.cleanupResources).toEqual([temporary]);
|
|
const primary = new ConfigurationError("Controlled state failure", {
|
|
settings: [{ setting: "fixture", constraint: "controlled" }],
|
|
});
|
|
const failure = await runSetup(
|
|
{ approved: true, structured: true, installHopper: false },
|
|
{
|
|
...host,
|
|
initialSetupState: async () => {
|
|
throw primary;
|
|
},
|
|
},
|
|
).then(
|
|
() => undefined,
|
|
(cause: unknown) => cause,
|
|
);
|
|
expect(failure).toBeInstanceOf(AnalysisError);
|
|
if (!(failure instanceof AnalysisError))
|
|
throw new Error("Expected typed cleanup failure");
|
|
expect(projectAnalysisError(failure)).toMatchObject({
|
|
code: "cleanup_incomplete",
|
|
details: { resources: [temporary] },
|
|
});
|
|
expect((await stat(temporary)).isDirectory()).toBe(true);
|
|
allowCleanup = true;
|
|
expect(await host.close?.()).toBeUndefined();
|
|
expect(failure.cleanupIncomplete).toBe(false);
|
|
expect(closeDoctor).toHaveBeenCalledTimes(1);
|
|
expect(projectAnalysisError(failure)).toMatchObject(
|
|
projectAnalysisError(primary),
|
|
);
|
|
expect(projectAnalysisError(failure).details).not.toHaveProperty(
|
|
"cleanup",
|
|
);
|
|
expect(projectAnalysisError(failure).details).not.toHaveProperty(
|
|
"resources",
|
|
);
|
|
expect(projectAnalysisError(failure).details).toHaveProperty(
|
|
"cleanup_history",
|
|
);
|
|
await expect(stat(temporary)).rejects.toMatchObject({ code: "ENOENT" });
|
|
} finally {
|
|
completeInstall?.();
|
|
allowCleanup = true;
|
|
await host.close?.();
|
|
}
|
|
},
|
|
);
|