mirror of
https://github.com/morluto/rea.git
synced 2026-11-06 14:56:32 +00:00
fix(session): preserve failed target restoration
This commit is contained in:
1 parent
3431541073
commit
0ddde829b2
2 files changed
+154
-5
No files matched your search
@@ -10,7 +10,11 @@ import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisError,
|
||||
primaryAnalysisError,
|
||||
} from "../../domain/analysisErrorBase.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import { createAnalysisExecutionEvidence } from "../../domain/analysisObservation.js";
|
||||
@@ -1031,9 +1035,15 @@ export class BinarySession
|
||||
primary: AnalysisError,
|
||||
): Promise<AnalysisError> {
|
||||
const restored = await this.#restore(previous);
|
||||
return restored.ok
|
||||
? primary
|
||||
: sessionCleanupFailure(primary, restored.error, "open_binary");
|
||||
if (restored.ok) return primary;
|
||||
primary.retainSecondaryFailure(restored.error, {
|
||||
restoration_error: projectAnalysisError(
|
||||
primaryAnalysisError(restored.error),
|
||||
),
|
||||
});
|
||||
return restored.error.cleanupIncomplete
|
||||
? sessionCleanupFailure(primary, restored.error, "open_binary")
|
||||
: primary;
|
||||
}
|
||||
|
||||
/** Describe the active binary session. */
|
||||
@@ -1460,7 +1470,7 @@ export class BinarySession
|
||||
}
|
||||
const closed = await this.#retireClient(run);
|
||||
return closed.ok
|
||||
? ok(null)
|
||||
? err(started.error)
|
||||
: err(sessionCleanupFailure(started.error, closed.error, "open_binary"));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { expect, it } from "vitest";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
import { createAnalysisProfile } from "../../domain/analysisProfile.js";
|
||||
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
|
||||
import { err, ok } from "../../domain/result.js";
|
||||
import { createAnalysisExecution } from "../AnalysisProvider.js";
|
||||
import { AnalysisProviderRegistry } from "./AnalysisProviderRegistry.js";
|
||||
import { BinarySession } from "./BinarySession.js";
|
||||
import {
|
||||
SessionProviderRouter,
|
||||
type SessionProviderRoute,
|
||||
} from "./SessionProviderRouter.js";
|
||||
|
||||
it.each([
|
||||
{ cancelled: false, cleanupFails: false },
|
||||
{ cancelled: true, cleanupFails: false },
|
||||
{ cancelled: false, cleanupFails: true },
|
||||
{ cancelled: true, cleanupFails: true },
|
||||
])(
|
||||
"retains failed restoration after switch cancellation=$cancelled, cleanup failure=$cleanupFails",
|
||||
async ({ cancelled, cleanupFails }) => {
|
||||
const session = new BinarySession(
|
||||
SessionProviderRouter.selectable(new AnalysisProviderRegistry([]), []),
|
||||
);
|
||||
const controller = new AbortController();
|
||||
const identity = {
|
||||
id: "restoration-test",
|
||||
name: "Restoration test",
|
||||
version: "1",
|
||||
};
|
||||
const restorationFailure = new AnalysisOutputError(
|
||||
"health",
|
||||
"Previous target could not restart",
|
||||
);
|
||||
const switchFailure = new AnalysisOutputError(
|
||||
"health",
|
||||
"Selected target could not start",
|
||||
);
|
||||
const cleanupFailure = new ProviderCleanupError(
|
||||
identity.id,
|
||||
["restored-client"],
|
||||
{ reason: "close failed" },
|
||||
);
|
||||
let created = 0;
|
||||
let restorationCloseCalls = 0;
|
||||
const route: SessionProviderRoute = {
|
||||
identity,
|
||||
capabilities: new Map(),
|
||||
profile: createAnalysisProfile(identity, { fixture: "original" }),
|
||||
binding: null,
|
||||
selection: undefined,
|
||||
createClient: () => {
|
||||
const restoring = created++ > 0;
|
||||
return {
|
||||
execute: async () =>
|
||||
restoring
|
||||
? err(restorationFailure)
|
||||
: ok(createAnalysisExecution(null, identity)),
|
||||
close: async () => {
|
||||
if (!restoring) {
|
||||
if (cancelled) controller.abort("cancel switch");
|
||||
return ok(null);
|
||||
}
|
||||
restorationCloseCalls += 1;
|
||||
if (cleanupFails && restorationCloseCalls === 1)
|
||||
return err(cleanupFailure);
|
||||
cleanupFailure.resolveCleanup();
|
||||
return ok(null);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
try {
|
||||
const target = await session.resolveTarget(process.execPath);
|
||||
expect(target.ok).toBe(true);
|
||||
if (!target.ok) return;
|
||||
expect(
|
||||
(await session.openResolvedTarget({ target: target.value, route })).ok,
|
||||
).toBe(true);
|
||||
expect(session.activeTarget()).toEqual(target.value);
|
||||
const result = await session.openResolvedTarget(
|
||||
{
|
||||
target: target.value,
|
||||
route: {
|
||||
...route,
|
||||
profile: createAnalysisProfile(identity, { fixture: "selected" }),
|
||||
createClient: () => ({
|
||||
execute: async () => err(switchFailure),
|
||||
close: async () => ok(null),
|
||||
}),
|
||||
},
|
||||
},
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(session.activeTarget()).toBeUndefined();
|
||||
expect(await session.status()).toMatchObject({
|
||||
open: false,
|
||||
cleanup_pending: cleanupFails,
|
||||
});
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: cleanupFails
|
||||
? "cleanup_incomplete"
|
||||
: cancelled
|
||||
? "cancelled"
|
||||
: "unreadable_output",
|
||||
});
|
||||
expect((await session.close()).ok).toBe(true);
|
||||
expect(restorationCloseCalls).toBe(cleanupFails ? 2 : 1);
|
||||
expect(await session.status()).toMatchObject({
|
||||
open: false,
|
||||
cleanup_pending: false,
|
||||
});
|
||||
expect(projectAnalysisError(result.error).code).toBe(
|
||||
cancelled ? "cancelled" : "unreadable_output",
|
||||
);
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
details: {
|
||||
diagnostics: {
|
||||
restoration_error: projectAnalysisError(restorationFailure),
|
||||
},
|
||||
},
|
||||
});
|
||||
if (cancelled) {
|
||||
const primary = result.error.cleanupPrimary ?? result.error;
|
||||
expect(primary).toBeInstanceOf(AnalysisCancelledError);
|
||||
} else {
|
||||
expect(result.error.cleanupPrimary ?? result.error).toBe(switchFailure);
|
||||
}
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user