fix(setup): preserve cleanup ownership and resource reports (#6920)

* 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>
This commit is contained in:
Dery Ferdika
2026-10-24 14:08:10 +07:00
committed by GitHub
parent bdf15b9bd5
commit 98ec00f406
23 changed files with 1543 additions and 188 deletions
@@ -151,6 +151,7 @@ import { RegularFileCleanupFailure } from "../../src/filesystem/OwnedRegularFile
import { OwnedFileHandle } from "../../src/filesystem/OwnedFileHandle.js";
import type { DoctorHost } from "../../src/application/Doctor.js";
import { systemSetupHost } from "../../src/application/SetupHost.js";
import { AnalysisError } from "../../src/domain/analysisErrorBase.js";
import { systemUninstallHost } from "../../src/application/Uninstall.js";
import { ConfigurationError } from "../../src/domain/configurationErrors.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
@@ -1315,7 +1316,16 @@ it.each(["setup", "uninstall"] as const)(
).status,
).toBe("failed");
}
expect(await host.close?.()).toContain("cleanup");
const cleanupFailure = await host.close?.();
if (operation === "setup") {
expect(cleanupFailure).toBeInstanceOf(AnalysisError);
const retainedRoots = (await fs.readdir(root))
.filter((name) => name.startsWith(".rea-config-"))
.map((name) => join(root, name));
expect((cleanupFailure as AnalysisError).cleanupResources).toEqual(
retainedRoots,
);
} else expect(cleanupFailure).toContain("cleanup");
expect(
(await fs.readdir(root)).filter((name) =>
name.startsWith(".rea-config-"),
@@ -1323,6 +1333,8 @@ it.each(["setup", "uninstall"] as const)(
).toHaveLength(1);
state.rejectStageRemoval = false;
expect(await host.close?.()).toBeUndefined();
if (cleanupFailure instanceof AnalysisError)
expect(cleanupFailure.cleanupIncomplete).toBe(false);
expect(
(await fs.readdir(root)).filter((name) =>
name.startsWith(".rea-config-"),