fix(session): retain unknown cleanup across mixed failures

This commit is contained in:
morluto committed 2026-10-27 07:53:54 +08:00
1 parent 25a08ed639
commit eb24da4481
4 files changed
+98 -11

No files matched your search

@@ -1132,6 +1132,34 @@ it("joins cancelled readiness finalization and shutdown before admitting another
}
});
it.each(["alpha", "beta"])(
"retains unknown owned-resource cleanup when %s has no completion signal",
async (unknownOwner) => {
const unknown = new ProviderAdapterError(unknownOwner, "close_binary", {
userMessage: "cleanup completion unknown",
});
const tracked = new ProviderCleanupError("rea", ["owned-reader"], {});
let released = false;
const registry = new AnalysisProviderRegistry(
["alpha", "beta"].map((id) => ({
...candidate(id).provider,
closeOwnedResources: async () => {
if (!released) return err(id === unknownOwner ? unknown : tracked);
if (id !== unknownOwner) tracked.resolveCleanup();
return ok(null);
},
})),
);
const closed = await registry.closeOwnedResources();
if (closed.ok) throw new Error("Expected incomplete registry close");
expect(closed.error.cleanupIncomplete).toBe(true);
released = true;
expect(await registry.closeOwnedResources()).toEqual(ok(null));
expect(tracked.cleanupIncomplete).toBe(false);
expect(closed.error.cleanupIncomplete).toBe(true);
},
);
it("joins every candidate and retains physical failure with exact mixed cleanup owners", async () => {
const alpha = candidate("alpha").provider;
const beta = candidate("beta").provider;
@@ -252,16 +252,15 @@ export class AnalysisProviderRegistry {
const distinct = [...new Set(failures)];
if (distinct.length === 0) return ok(null);
if (distinct.length === 1) return err(distinct[0]!);
return err(
new ProviderCleanupError(
"rea",
[...new Set(distinct.flatMap((failure) => failure.cleanupResources))],
{ reason: distinct.map((failure) => failure.message).join("; ") },
{ cause: new AggregateError(distinct) },
).bindCleanupDependencies(
distinct.filter((failure) => failure.cleanupIncomplete),
),
const failure = new ProviderCleanupError(
"rea",
[...new Set(distinct.flatMap((failure) => failure.cleanupResources))],
{ reason: distinct.map((failure) => failure.message).join("; ") },
{ cause: new AggregateError(distinct) },
);
if (distinct.every((failure) => failure.cleanupIncomplete))
failure.bindCleanupDependencies(distinct);
return err(failure);
}
/** List the unique deep-operation declarations without probing candidates. */
+4 -2
View File
@@ -913,7 +913,8 @@ export class BinarySession
next,
"close_binary",
);
resourceFailure.bindCleanupDependencies([previous, next]);
if (previous.cleanupIncomplete && next.cleanupIncomplete)
resourceFailure.bindCleanupDependencies([previous, next]);
}
if (!closed.ok && resourceFailure !== undefined) {
const combined = analysisErrorWithCleanupFailure(
@@ -921,7 +922,8 @@ export class BinarySession
resourceFailure,
"close_binary",
);
combined.bindCleanupDependencies([closed.error, resourceFailure]);
if (closed.error.cleanupIncomplete && resourceFailure.cleanupIncomplete)
combined.bindCleanupDependencies([closed.error, resourceFailure]);
return err(combined);
}
if (!closed.ok) return closed;
@@ -982,6 +982,64 @@ it.each([0, 1])(
},
);
it.each([
["binary", "client"],
["binary", "files"],
["bundle", "client"],
["bundle", "files"],
] as const)(
"keeps unknown %s close completion when the %s owner has no cleanup signal",
async (targetKind, unknownOwner) => {
const tracked = new ProviderCleanupError(identity.id, ["owned-reader"], {});
const unknown = new ProviderAdapterError(identity.id, "close_binary", {
userMessage: "cleanup completion unknown",
});
let released = false;
let failCleanup = false;
const closeOwner = async (owner: "client" | "files") => {
if (!failCleanup) return ok(null);
if (!released) return err(owner === unknownOwner ? unknown : tracked);
if (owner !== unknownOwner) tracked.resolveCleanup();
return ok(null);
};
const client: AnalysisClient = {
execute: async () => ok(createAnalysisExecution(null, binaryIdentity)),
close: () => closeOwner("client"),
};
// Exercise the public session close composition with independently owned
// file cleanup; the file owner's failure shape is the regression boundary.
class SessionWithFileCleanup extends BinarySession {
override closeFileResources() {
return closeOwner("files");
}
}
const session = new SessionWithFileCleanup(
createTestProviderRouter(() => client),
new InvestigationRecords(),
{ createClient: () => ({ client, capabilities: [] }) },
);
if (targetKind === "binary") {
expect(await session.open(process.execPath)).toMatchObject({ ok: true });
} else {
const rootPath = await mkdtemp(join(tmpdir(), "rea-resource-close-"));
temporaryDirectories.push(rootPath);
await writeFile(join(rootPath, "Info.plist"), "<plist><dict/></plist>");
expect(await session.openBundleResources(rootPath)).toMatchObject({
ok: true,
});
}
failCleanup = true;
const closed = await session.close();
if (closed.ok) throw new Error("Expected incomplete close");
expect(closed.error.cleanupIncomplete).toBe(true);
released = true;
expect(await session.close()).toMatchObject({ ok: true });
expect(tracked.cleanupIncomplete).toBe(false);
expect(closed.error.cleanupIncomplete).toBe(true);
expect(await session.status()).toMatchObject({ cleanup_pending: false });
},
);
it.each(["health", "cancellation"] as const)(
"updates early resource-open %s cleanup after its candidate retries",
async (failureStage) => {