mirror of
https://github.com/morluto/rea.git
synced 2026-10-29 02:45:58 +00:00
* 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>
123 lines
4.1 KiB
TypeScript
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),
|
|
},
|
|
}),
|
|
};
|
|
};
|