Files
reverse-engineering-anything/src/application/ClientConfigurationCleanup.ts
T
Dery Ferdikaandgensokyo-dev 98ec00f406 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>
2026-10-24 15:08:10 +08:00

123 lines
4.1 KiB
TypeScript

import {
ArtifactResourceRetryFailure,
type ArtifactResourceOwner,
type ArtifactResourceScope,
} from "../artifacts/ArtifactResourceScope.js";
import type { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import type { AnalysisCleanupObservation } from "../domain/analysisErrorBase.js";
import {
RegularFileCleanupFailure,
type RegularFileOperationOutcome,
} from "../filesystem/OwnedRegularFile.js";
import { retryRegularFileCleanupFailures } from "./RegularFileRead.js";
/** Completion and exact staging owner retained when publication cleanup fails. */
export class ClientConfigurationPublicationFailure<
Value = unknown,
> extends ArtifactResourceRetryFailure {
constructor(
readonly path: string,
readonly outcome: RegularFileOperationOutcome<Value>,
readonly cleanupCause: unknown,
readonly owner: Extract<
ArtifactResourceOwner,
{ readonly kind: "staged-output" }
>,
readonly cleanupLease: AnalysisCleanupLease,
) {
super(
[{ owner, lease: cleanupLease }],
"Configuration publication cleanup remains incomplete",
{
cause: new AggregateError(
[...(outcome.kind === "failed" ? [outcome.cause] : []), cleanupCause],
"Configuration operation and staging cleanup failures",
),
cleanup: ArtifactReaderFailure.cleanupObservation(
cleanupCause,
owner.resource,
),
},
);
}
}
type ClientConfigurationCleanupFailure =
| RegularFileCleanupFailure
| ClientConfigurationPublicationFailure;
export const isClientConfigurationCleanupFailure = (
cause: unknown,
): cause is ClientConfigurationCleanupFailure =>
cause instanceof RegularFileCleanupFailure ||
cause instanceof ClientConfigurationPublicationFailure;
/** Adopt only capabilities carried by explicit operation outcomes, never opaque causes. */
export const retryClientConfigurationCleanup = async (
failure: ClientConfigurationCleanupFailure,
resources: ArtifactResourceScope,
): Promise<{
readonly primary: unknown;
readonly failures: readonly RegularFileCleanupFailure[];
readonly cleanupLeases: readonly AnalysisCleanupLease[];
readonly cleanup?: AnalysisCleanupObservation;
}> => {
const failures: RegularFileCleanupFailure[] = [];
const cleanupLeases = new Set<AnalysisCleanupLease>();
const observations: AnalysisCleanupObservation[] = [];
let current = failure;
let primary: unknown;
for (;;) {
let nested: unknown;
if (current instanceof RegularFileCleanupFailure) {
const retried = await retryRegularFileCleanupFailures(current, resources);
failures.push(...retried.failures);
for (const lease of retried.cleanupLeases) cleanupLeases.add(lease);
if (retried.cleanup !== undefined) observations.push(retried.cleanup);
primary = retried.primary;
const last = retried.failures.at(-1)!;
nested =
last.operationFailure === undefined && last.outcome.kind === "failed"
? last.outcome.cause
: undefined;
} else {
const released = await resources.release({
kind: "lease",
resource: current.owner.resource,
lease: current.cleanupLease,
});
if (released.kind === "failed") {
cleanupLeases.add(released.lease);
observations.push(
ArtifactReaderFailure.cleanupObservation(
released.cause,
current.owner.resource,
),
);
}
primary =
current.outcome.kind === "failed"
? current.outcome.cause
: current.cleanupCause;
nested =
current.outcome.kind === "failed" ? current.outcome.cause : undefined;
}
if (!isClientConfigurationCleanupFailure(nested)) break;
current = nested;
}
return {
primary,
failures,
cleanupLeases: [...cleanupLeases],
...(observations.length === 0
? {}
: {
cleanup: {
reason: observations.map(({ reason }) => reason).join("; "),
resources: observations.flatMap(({ resources }) => resources),
},
}),
};
};