mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(session): track retained bundle cleanup dependencies
This commit is contained in:
1 parent
e80fb638be
commit
6a9911ea77
4 files changed
+145
-12
No files matched your search
@@ -939,14 +939,20 @@ export class BinarySession
|
||||
);
|
||||
if (!closed.ok) {
|
||||
retained.push(session);
|
||||
const previous = failure;
|
||||
failure =
|
||||
failure === undefined
|
||||
previous === undefined
|
||||
? closed.error
|
||||
: analysisErrorWithCleanupFailure(
|
||||
failure,
|
||||
previous,
|
||||
closed.error,
|
||||
"close_binary",
|
||||
);
|
||||
if (
|
||||
previous?.cleanupIncomplete === true &&
|
||||
closed.error.cleanupIncomplete
|
||||
)
|
||||
failure.bindCleanupDependencies([previous, closed.error]);
|
||||
}
|
||||
}
|
||||
this.#pendingBundleResourceCleanup = retained;
|
||||
|
||||
@@ -16,12 +16,12 @@ import { SessionProviderRouter } from "./SessionProviderRouter.js";
|
||||
import { bundleResourceRootSchema } from "../../domain/apple/bundleResourceRoot.js";
|
||||
import { AnalysisSnapshotMismatchError } from "../../domain/evidenceErrors.js";
|
||||
import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js";
|
||||
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
|
||||
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
|
||||
import { err } from "../../domain/result.js";
|
||||
import { err, ok } from "../../domain/result.js";
|
||||
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
|
||||
import { createEvidence } from "../../domain/evidence.js";
|
||||
import { createAnalysisProfile } from "../../domain/analysisProfile.js";
|
||||
import { ok } from "../../domain/result.js";
|
||||
import { createTestProviderRouter } from "../../../tests/fixtures/binarySession.js";
|
||||
|
||||
const identity: ProviderIdentity = {
|
||||
@@ -797,3 +797,63 @@ it("cancels a binary switch during resource retirement and retains null-subject
|
||||
expect(await session.close()).toMatchObject({ ok: true });
|
||||
expect(resourceCloseAttempts).toBe(2);
|
||||
});
|
||||
|
||||
it("tracks independent pending resource owners after a failed session replacement", async () => {
|
||||
const rootPath = await mkdtemp(
|
||||
join(tmpdir(), "rea-resource-pending-owners-"),
|
||||
);
|
||||
temporaryDirectories.push(rootPath);
|
||||
await writeFile(join(rootPath, "Info.plist"), "<plist><dict/></plist>");
|
||||
const failures = [
|
||||
new ProviderCleanupError(identity.id, ["previous-reader"], {}),
|
||||
new ProviderCleanupError(identity.id, ["candidate-reader"], {}),
|
||||
];
|
||||
const released = [false, false];
|
||||
let created = 0;
|
||||
const session = new BinarySession(
|
||||
SessionProviderRouter.selectable(new AnalysisProviderRegistry([]), []),
|
||||
new InvestigationRecords(),
|
||||
{
|
||||
createClient: () => {
|
||||
const index = created++;
|
||||
const failure = failures[index];
|
||||
if (failure === undefined)
|
||||
throw new Error("Unexpected resource client");
|
||||
return {
|
||||
capabilities: [],
|
||||
client: {
|
||||
execute: async () => ok(createAnalysisExecution(null, identity)),
|
||||
close: async () => {
|
||||
if (!released[index]) return err(failure);
|
||||
failure.resolveCleanup();
|
||||
return ok(null);
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(await session.openBundleResources(rootPath)).toMatchObject({
|
||||
ok: true,
|
||||
});
|
||||
expect(await session.openBundleResources(rootPath)).toMatchObject({
|
||||
ok: false,
|
||||
});
|
||||
const first = await session.close();
|
||||
if (first.ok)
|
||||
throw new Error("Expected both resource owners to remain pending");
|
||||
expect(new Set(first.error.cleanupResources)).toEqual(
|
||||
new Set(["previous-reader", "candidate-reader"]),
|
||||
);
|
||||
|
||||
released[0] = true;
|
||||
expect(await session.close()).toMatchObject({ ok: false });
|
||||
expect(first.error.cleanupIncomplete).toBe(true);
|
||||
expect(first.error.cleanupResources).toEqual(["candidate-reader"]);
|
||||
|
||||
released[1] = true;
|
||||
expect(await session.close()).toMatchObject({ ok: true });
|
||||
expect(first.error.cleanupIncomplete).toBe(false);
|
||||
expect(first.error.cleanupResources).toEqual([]);
|
||||
expect(await session.status()).toMatchObject({ cleanup_pending: false });
|
||||
});
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
type AnalysisClient,
|
||||
type ProviderIdentity,
|
||||
} from "../AnalysisProvider.js";
|
||||
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
|
||||
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
|
||||
import { err } from "../../domain/result.js";
|
||||
import { err, ok } from "../../domain/result.js";
|
||||
|
||||
const identity: ProviderIdentity = {
|
||||
id: "bundle-resource-close-fixture",
|
||||
@@ -104,3 +105,67 @@ it("reports the first mixed child failure before awaiting the other owner and re
|
||||
expect(artifactCloseAttempts).toBe(2);
|
||||
expect(nativeCloseAttempts).toBe(2);
|
||||
});
|
||||
|
||||
it("updates retained cleanup facts as the two child owners release independently", async () => {
|
||||
const artifactFailure = new ProviderCleanupError(
|
||||
identity.id,
|
||||
["artifact-reader"],
|
||||
{},
|
||||
);
|
||||
const nativeFailure = new ProviderCleanupError(
|
||||
identity.id,
|
||||
["native-reader"],
|
||||
{},
|
||||
);
|
||||
let releaseArtifact = false;
|
||||
let releaseNative = false;
|
||||
const client = (
|
||||
failure: ProviderCleanupError,
|
||||
released: () => boolean,
|
||||
): AnalysisClient => ({
|
||||
execute: async () => ok(createAnalysisExecution(null, identity)),
|
||||
close: async () => {
|
||||
if (!released()) return err(failure);
|
||||
failure.resolveCleanup();
|
||||
return ok(null);
|
||||
},
|
||||
});
|
||||
const artifact = client(artifactFailure, () => releaseArtifact);
|
||||
const native = client(nativeFailure, () => releaseNative);
|
||||
const combined = combineBundleResourceClients(artifact, native);
|
||||
const first = await combined.close();
|
||||
if (first.ok) throw new Error("Expected both child owners to remain open");
|
||||
expect(new Set(first.error.cleanupResources)).toEqual(
|
||||
new Set(["artifact-reader", "native-reader"]),
|
||||
);
|
||||
|
||||
releaseArtifact = true;
|
||||
expect(await artifact.close()).toMatchObject({ ok: true });
|
||||
expect(first.error.cleanupIncomplete).toBe(true);
|
||||
expect(first.error.cleanupResources).toEqual(["native-reader"]);
|
||||
|
||||
releaseNative = true;
|
||||
expect(await combined.close()).toMatchObject({ ok: true });
|
||||
expect(first.error.cleanupIncomplete).toBe(false);
|
||||
expect(first.error.cleanupResources).toEqual([]);
|
||||
expect(new Set(first.error.cleanup?.resources)).toEqual(
|
||||
new Set(["artifact-reader", "native-reader"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps cleanup unknown when a child close failure has no cleanup lifecycle", async () => {
|
||||
const owned = new ProviderCleanupError(identity.id, ["artifact-reader"], {});
|
||||
const client = (
|
||||
failure: AnalysisInputError | ProviderCleanupError,
|
||||
): AnalysisClient => ({
|
||||
execute: async () => ok(createAnalysisExecution(null, identity)),
|
||||
close: async () => err(failure),
|
||||
});
|
||||
const closed = await combineBundleResourceClients(
|
||||
client(owned),
|
||||
client(new AnalysisInputError("close_binary")),
|
||||
).close();
|
||||
if (closed.ok) throw new Error("Expected child close failures");
|
||||
owned.resolveCleanup();
|
||||
expect(closed.error.cleanupIncomplete).toBe(true);
|
||||
});
|
||||
@@ -70,14 +70,16 @@ export const combineBundleResourceClients = (
|
||||
closeAnalysisClient(artifactClient, "bundle-resources", options),
|
||||
closeAnalysisClient(nativeClient, "bundle-resources", options),
|
||||
]);
|
||||
if (!artifact.ok && !native.ok)
|
||||
return err(
|
||||
analysisErrorWithCleanupFailure(
|
||||
artifact.error,
|
||||
native.error,
|
||||
"close_binary",
|
||||
),
|
||||
if (!artifact.ok && !native.ok) {
|
||||
const failure = analysisErrorWithCleanupFailure(
|
||||
artifact.error,
|
||||
native.error,
|
||||
"close_binary",
|
||||
);
|
||||
if (artifact.error.cleanupIncomplete && native.error.cleanupIncomplete)
|
||||
failure.bindCleanupDependencies([artifact.error, native.error]);
|
||||
return err(failure);
|
||||
}
|
||||
if (!artifact.ok) return artifact;
|
||||
if (!native.ok) return native;
|
||||
return ok(null);
|
||||
|
||||
Reference in new issue
Block a user