mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 18:05:45 +00:00
Replace synchronous Java execution with bounded owned commands. Keep task-scoped readiness observations fresh across later tasks, defer target client prerequisites behind a closeable owner, and join doctor cancellation and cleanup. Preserve completed physical failures across late cancellation, exact cleanup dependencies and partial observations. Admit target/profile inputs before probing and return typed factory failures instead of unavailable client wrappers. Validation: 405 focused tests across 24 files, 2 explicit skips; 109 provider/doctor controls rerun after final admission refinement; check:fast and docs:check. Controlled macOS Java-shaped commands and public CLI/MCP workflows; no new JVM/Ghidra or Windows/Linux lifecycle verification. Measured warm public doctor baseline/candidate: elapsed 0.447/1.223s, CPU 0.624/1.237s, sampled family RSS 379/529MB, reply tokens 3133/3133. Ownership adds cost; no performance improvement claimed. True cold, whole-agent and MCP costs unmeasured. Refs #4561. Remaining installation file admission tracked in #4672.
960 lines
30 KiB
TypeScript
960 lines
30 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { AnalysisProviderRegistry } from "./AnalysisProviderRegistry.js";
|
|
import {
|
|
createAnalysisExecution,
|
|
type AnalysisClient,
|
|
type AnalysisProviderCandidate,
|
|
type CapabilityDescriptor,
|
|
type ProviderAvailability,
|
|
type ProviderIdentity,
|
|
type ProviderTargetSupport,
|
|
} from "../AnalysisProvider.js";
|
|
import { createAnalysisProfile } from "../../domain/analysisProfile.js";
|
|
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
|
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
|
|
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
|
|
import { ProviderSelectionError } from "../../domain/providerSelectionError.js";
|
|
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
|
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
|
|
import { err, ok } from "../../domain/result.js";
|
|
|
|
const DATABASE_TARGET: BinaryTarget = {
|
|
path: "/tmp/fixture.hop",
|
|
sha256: "a".repeat(64),
|
|
kind: "database",
|
|
format: "analysis-database",
|
|
};
|
|
const ARTIFACT_TARGET: BinaryTarget = {
|
|
path: "/tmp/app.asar",
|
|
sha256: "b".repeat(64),
|
|
kind: "archive",
|
|
format: "asar",
|
|
};
|
|
|
|
it("rejects malformed provider capability declarations", () => {
|
|
const alpha = candidate("alpha");
|
|
expect(
|
|
() => new AnalysisProviderRegistry([alpha.provider, alpha.provider]),
|
|
).toThrow(/Duplicate analysis provider ID: alpha/u);
|
|
expect(
|
|
() =>
|
|
new AnalysisProviderRegistry([
|
|
{
|
|
...alpha.provider,
|
|
capabilities: () => {
|
|
const descriptor = capability(alpha.provider.identity());
|
|
return [descriptor, descriptor];
|
|
},
|
|
},
|
|
]),
|
|
).toThrow(/declares operation address_name more than once/u);
|
|
expect(
|
|
() =>
|
|
new AnalysisProviderRegistry([
|
|
{
|
|
...alpha.provider,
|
|
capabilities: () => [
|
|
capability({ ...alpha.provider.identity(), name: "forged" }),
|
|
],
|
|
},
|
|
]),
|
|
).toThrow(/published mismatched capability provenance/u);
|
|
expect(
|
|
() => new AnalysisProviderRegistry([candidate("auto").provider]),
|
|
).toThrow(/Invalid analysis provider ID: auto/u);
|
|
});
|
|
|
|
it("uses one owner readiness snapshot for candidate availability and capabilities", async () => {
|
|
const base = candidate("alpha").provider;
|
|
let observed = 0;
|
|
let capabilityReads = 0;
|
|
const provider: AnalysisProviderCandidate = {
|
|
...base,
|
|
inspectAvailability: () => {
|
|
throw new Error("availability must come from the owner snapshot");
|
|
},
|
|
capabilities: () => {
|
|
capabilityReads += 1;
|
|
return [capability(base.identity())];
|
|
},
|
|
inspectReadinessSnapshot: () => {
|
|
observed += 1;
|
|
return {
|
|
availability: {
|
|
status: "unavailable",
|
|
code: "runtime_unavailable",
|
|
reason: "native inspection failed",
|
|
diagnostics: { failure_stage: "self-test" },
|
|
},
|
|
capabilities: [capability(base.identity())],
|
|
};
|
|
},
|
|
};
|
|
|
|
const [status] = await new AnalysisProviderRegistry([provider]).candidates();
|
|
expect(observed).toBe(1);
|
|
expect(capabilityReads).toBe(1);
|
|
expect(status).toMatchObject({
|
|
availability: {
|
|
status: "unavailable",
|
|
code: "runtime_unavailable",
|
|
diagnostics: { failure_stage: "self-test" },
|
|
},
|
|
capabilities: [{ operation: "address_name" }],
|
|
});
|
|
});
|
|
|
|
it("selects the sole available provider and honors explicit selection precedence", async () => {
|
|
const alpha = candidate("alpha");
|
|
const beta = candidate("beta", { available: false });
|
|
const sole = await new AnalysisProviderRegistry([
|
|
beta.provider,
|
|
alpha.provider,
|
|
]).select(DATABASE_TARGET);
|
|
expect(sole).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
binding: {
|
|
identity: { id: "alpha" },
|
|
selectionSource: "auto-single-candidate",
|
|
},
|
|
},
|
|
});
|
|
|
|
const selected = new AnalysisProviderRegistry(
|
|
[alpha.provider, candidate("beta").provider],
|
|
"beta",
|
|
);
|
|
expect(await selected.select(DATABASE_TARGET)).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
binding: {
|
|
identity: { id: "beta" },
|
|
selectionSource: "environment",
|
|
},
|
|
},
|
|
});
|
|
expect(await selected.select(DATABASE_TARGET, "alpha")).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
binding: {
|
|
identity: { id: "alpha" },
|
|
selectionSource: "request",
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
describe("inspection without deep binding", () => {
|
|
it("retains discovery facts without resolving a deep profile for covered automatic inspection", async () => {
|
|
const alpha = candidate("alpha", { profile: "error" });
|
|
const beta = candidate("beta", { profile: "error" });
|
|
const registry = new AnalysisProviderRegistry(
|
|
[alpha.provider, beta.provider],
|
|
"auto",
|
|
(target) => target === DATABASE_TARGET,
|
|
);
|
|
expect(await registry.select(DATABASE_TARGET)).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
requestedProviderId: "auto",
|
|
binding: null,
|
|
candidates: [
|
|
{
|
|
provider: { id: "alpha" },
|
|
selected: false,
|
|
availability: { status: "available" },
|
|
},
|
|
{
|
|
provider: { id: "beta" },
|
|
selected: false,
|
|
availability: { status: "available" },
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(alpha.created).toEqual([]);
|
|
expect(beta.created).toEqual([]);
|
|
});
|
|
|
|
it("keeps explicit request and environment choices even when auxiliary inspection covers the target", async () => {
|
|
const alpha = candidate("alpha");
|
|
const beta = candidate("beta");
|
|
const registry = new AnalysisProviderRegistry(
|
|
[alpha.provider, beta.provider],
|
|
"beta",
|
|
() => true,
|
|
);
|
|
expect(await registry.select(DATABASE_TARGET)).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
binding: { identity: { id: "beta" }, selectionSource: "environment" },
|
|
},
|
|
});
|
|
expect(await registry.select(DATABASE_TARGET, "alpha")).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
binding: { identity: { id: "alpha" }, selectionSource: "request" },
|
|
},
|
|
});
|
|
expect(await registry.select(DATABASE_TARGET, "auto")).toMatchObject({
|
|
ok: true,
|
|
value: { binding: null },
|
|
});
|
|
expect(await registry.select(DATABASE_TARGET, "missing")).toMatchObject({
|
|
ok: false,
|
|
error: { reason: "unknown_provider" },
|
|
});
|
|
});
|
|
|
|
it("preserves ordinary ambiguity when auxiliary inspection does not cover the target", async () => {
|
|
const registry = new AnalysisProviderRegistry(
|
|
[candidate("alpha").provider, candidate("beta").provider],
|
|
"auto",
|
|
() => false,
|
|
);
|
|
expect(await registry.select(DATABASE_TARGET)).toMatchObject({
|
|
ok: false,
|
|
error: { reason: "ambiguous" },
|
|
});
|
|
const controller = new AbortController();
|
|
controller.abort();
|
|
expect(
|
|
await registry.select(DATABASE_TARGET, undefined, {
|
|
signal: controller.signal,
|
|
}),
|
|
).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } });
|
|
});
|
|
});
|
|
|
|
describe("analysis provider registry: selection failures", () => {
|
|
it("returns actionable unknown, unavailable, and unsupported failures", async () => {
|
|
const unavailable = candidate("alpha", { available: false });
|
|
const registry = new AnalysisProviderRegistry([unavailable.provider]);
|
|
|
|
const unknown = await registry.select(DATABASE_TARGET, "missing");
|
|
expect(unknown.ok).toBe(false);
|
|
if (!unknown.ok)
|
|
expect(projectAnalysisError(unknown.error)).toMatchObject({
|
|
details: {
|
|
selection_reason: "unknown_provider",
|
|
requested_provider_id: "missing",
|
|
candidate_ids: ["alpha"],
|
|
},
|
|
});
|
|
|
|
const rejected = await registry.select(DATABASE_TARGET, "alpha");
|
|
expect(rejected.ok).toBe(false);
|
|
if (!rejected.ok)
|
|
expect(projectAnalysisError(rejected.error)).toMatchObject({
|
|
code: "provider_unavailable",
|
|
details: {
|
|
rejections: [
|
|
{
|
|
provider_id: "alpha",
|
|
code: "runtime_missing",
|
|
diagnostics: { executable_path: "/opt/alpha/bin/analyze" },
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const unsupported = await new AnalysisProviderRegistry([
|
|
candidate("alpha", { available: false }).provider,
|
|
]).select(ARTIFACT_TARGET, "alpha");
|
|
if (unsupported.ok)
|
|
throw new Error("expected unsupported target rejection");
|
|
expect(unsupported.error).toBeInstanceOf(ProviderSelectionError);
|
|
if (
|
|
!unsupported.ok &&
|
|
unsupported.error instanceof ProviderSelectionError
|
|
) {
|
|
expect(unsupported.error).toMatchObject({
|
|
reason: "target_unsupported",
|
|
});
|
|
expect(unsupported.error.rejections.map(({ code }) => code)).toEqual([
|
|
"runtime_missing",
|
|
"target_kind_unsupported",
|
|
]);
|
|
}
|
|
});
|
|
|
|
it("keeps artifact-only targets unbound under auto or environment preference", async () => {
|
|
const alpha = candidate("alpha");
|
|
const environment = new AnalysisProviderRegistry([alpha.provider], "alpha");
|
|
const automatic = new AnalysisProviderRegistry([alpha.provider]);
|
|
|
|
for (const registry of [environment, automatic]) {
|
|
const selected = await registry.select(ARTIFACT_TARGET);
|
|
expect(selected).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
binding: null,
|
|
candidates: [
|
|
{
|
|
selected: false,
|
|
targetSupport: {
|
|
status: "unsupported",
|
|
code: "target_kind_unsupported",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
}
|
|
const explicit = await automatic.select(ARTIFACT_TARGET, "alpha");
|
|
if (explicit.ok) throw new Error("expected provider selection rejection");
|
|
expect(explicit.error).toBeInstanceOf(ProviderSelectionError);
|
|
if (explicit.error instanceof ProviderSelectionError)
|
|
expect(explicit.error.reason).toBe("target_unsupported");
|
|
});
|
|
|
|
it("rejects an unknown environment preference even when every candidate rejects the target", async () => {
|
|
const alpha = candidate("alpha");
|
|
const misconfigured = new AnalysisProviderRegistry(
|
|
[alpha.provider],
|
|
"missing",
|
|
);
|
|
|
|
for (const selected of [
|
|
await misconfigured.select(ARTIFACT_TARGET),
|
|
await misconfigured.select(ARTIFACT_TARGET, "missing"),
|
|
]) {
|
|
if (selected.ok) throw new Error("expected unknown provider rejection");
|
|
expect(projectAnalysisError(selected.error)).toMatchObject({
|
|
details: {
|
|
selection_reason: "unknown_provider",
|
|
requested_provider_id: "missing",
|
|
candidate_ids: ["alpha"],
|
|
},
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("analysis provider registry: profile and cleanup failures", () => {
|
|
it("does not bind a profile with unresolved version or adapter failure", async () => {
|
|
const unresolved = candidate("unresolved", { profile: "missing" });
|
|
const failed = candidate("failed", { profile: "error" });
|
|
|
|
const automatic = await new AnalysisProviderRegistry([
|
|
unresolved.provider,
|
|
]).select(DATABASE_TARGET);
|
|
expect(automatic).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
binding: null,
|
|
candidates: [
|
|
{
|
|
availability: {
|
|
status: "unavailable",
|
|
code: "version_unresolved",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const explicit = await new AnalysisProviderRegistry([
|
|
failed.provider,
|
|
]).select(DATABASE_TARGET, "failed");
|
|
if (explicit.ok) throw new Error("expected provider selection rejection");
|
|
expect(explicit.error).toMatchObject({
|
|
_tag: "ProviderAdapterError",
|
|
providerId: "failed",
|
|
});
|
|
});
|
|
|
|
it("aborts automatic selection when a provider reports unconfirmed cleanup", async () => {
|
|
const ghidra = candidate("ghidra");
|
|
const alternate = candidate("hopper");
|
|
const cleanupFailure = new ProviderCleanupError(
|
|
"ghidra",
|
|
["/opt/ghidra/data/languages/x86.ldefs"],
|
|
{ reason: "descriptor close failed" },
|
|
{ operation: "resolve_analysis_profile" },
|
|
);
|
|
const provider: AnalysisProviderCandidate = {
|
|
...ghidra.provider,
|
|
resolveAnalysisProfile: () => Promise.resolve(err(cleanupFailure)),
|
|
};
|
|
|
|
const selected = await new AnalysisProviderRegistry([
|
|
alternate.provider,
|
|
provider,
|
|
]).select(DATABASE_TARGET);
|
|
|
|
expect(selected).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
_tag: "ProviderAdapterError",
|
|
providerId: "ghidra",
|
|
cleanupIncomplete: true,
|
|
},
|
|
});
|
|
expect(alternate.created).toEqual([]);
|
|
});
|
|
|
|
it("owns a signal-ignoring profile probe until final completion and joins shutdown", async () => {
|
|
const alpha = candidate("alpha");
|
|
let observedSignal: AbortSignal | undefined;
|
|
let finish: (() => void) | undefined;
|
|
let started: (() => void) | undefined;
|
|
const pending = new Promise<void>((resolve) => {
|
|
started = resolve;
|
|
});
|
|
const gate = new Promise<void>((resolve) => {
|
|
finish = resolve;
|
|
});
|
|
const ignoresCancellation: AnalysisProviderCandidate = {
|
|
...alpha.provider,
|
|
resolveAnalysisProfile: async (target, options) => {
|
|
observedSignal = options?.signal;
|
|
started?.();
|
|
await gate;
|
|
return alpha.provider.resolveAnalysisProfile(target);
|
|
},
|
|
};
|
|
const registry = new AnalysisProviderRegistry([ignoresCancellation]);
|
|
const controller = new AbortController();
|
|
const reason = { caller: "profile finalization" };
|
|
let selected = false;
|
|
const selecting = registry
|
|
.select(DATABASE_TARGET, "alpha", { signal: controller.signal })
|
|
.then((result) => {
|
|
selected = true;
|
|
return result;
|
|
});
|
|
await pending;
|
|
controller.abort(reason);
|
|
let closed = false;
|
|
const closing = registry.closeOwnedResources().then((result) => {
|
|
closed = true;
|
|
return result;
|
|
});
|
|
try {
|
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
expect(selected).toBe(false);
|
|
expect(closed).toBe(false);
|
|
expect(registry.hasPendingOwnedResources()).toBe(true);
|
|
expect(observedSignal?.aborted).toBe(true);
|
|
expect(observedSignal?.reason).toBe(reason);
|
|
finish?.();
|
|
const result = await selecting;
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: { _tag: "AnalysisCancelledError", cause: reason },
|
|
});
|
|
expect(await closing).toEqual(ok(null));
|
|
expect(registry.hasPendingOwnedResources()).toBe(false);
|
|
expect(alpha.created).toEqual([]);
|
|
} finally {
|
|
finish?.();
|
|
await Promise.all([selecting, closing]);
|
|
}
|
|
});
|
|
});
|
|
|
|
interface CandidateFixture {
|
|
readonly provider: AnalysisProviderCandidate;
|
|
readonly created: string[];
|
|
}
|
|
|
|
const candidate = (
|
|
id: string,
|
|
options: {
|
|
readonly available?: boolean;
|
|
readonly profile?: "resolved" | "missing" | "error";
|
|
} = {},
|
|
): CandidateFixture => {
|
|
const identity: ProviderIdentity = {
|
|
id,
|
|
name: `${id} provider`,
|
|
version: null,
|
|
};
|
|
const created: string[] = [];
|
|
const available = options.available ?? true;
|
|
const provider: AnalysisProviderCandidate = {
|
|
identity: () => identity,
|
|
capabilities: () => [capability(identity)],
|
|
inspectAvailability: (): ProviderAvailability =>
|
|
available
|
|
? {
|
|
status: "available",
|
|
code: null,
|
|
reason: null,
|
|
diagnostics: { executable_path: `/opt/${id}/bin/analyze` },
|
|
}
|
|
: {
|
|
status: "unavailable",
|
|
code: "runtime_missing",
|
|
reason: `${id} runtime is unavailable`,
|
|
diagnostics: { executable_path: `/opt/${id}/bin/analyze` },
|
|
},
|
|
inspectTargetSupport: (target): ProviderTargetSupport =>
|
|
target.kind === "database"
|
|
? {
|
|
status: "supported",
|
|
code: null,
|
|
reason: null,
|
|
diagnostics: { target_kind: target.kind },
|
|
}
|
|
: {
|
|
status: "unsupported",
|
|
code: "target_kind_unsupported",
|
|
reason: `${id} only accepts analysis databases in this fixture`,
|
|
diagnostics: { target_kind: target.kind },
|
|
},
|
|
resolveAnalysisProfile: () => {
|
|
if (options.profile === "error")
|
|
return Promise.resolve(
|
|
err(new ProviderAdapterError(id, "resolve_analysis_profile")),
|
|
);
|
|
return Promise.resolve(
|
|
ok({
|
|
profile:
|
|
options.profile === "missing"
|
|
? null
|
|
: createAnalysisProfile(
|
|
{ id, name: identity.name, version: "1" },
|
|
{ fixture: id },
|
|
),
|
|
}),
|
|
);
|
|
},
|
|
createClient: (_target, profile): AnalysisClient => {
|
|
created.push(id);
|
|
const executionIdentity = profile?.provider ?? identity;
|
|
return {
|
|
execute: (operation) =>
|
|
Promise.resolve(
|
|
ok(
|
|
createAnalysisExecution(
|
|
operation === "health" ? null : `${id}:${operation}`,
|
|
executionIdentity,
|
|
),
|
|
),
|
|
),
|
|
close: () => Promise.resolve(ok(null)),
|
|
};
|
|
},
|
|
};
|
|
return { provider, created };
|
|
};
|
|
|
|
const capability = (provider: ProviderIdentity): CapabilityDescriptor => ({
|
|
provider,
|
|
operation: "address_name",
|
|
available: true,
|
|
reason: null,
|
|
effects: {
|
|
mutatesArtifact: false,
|
|
launchesProcess: true,
|
|
mayShowUi: false,
|
|
mayAccessNetwork: false,
|
|
mayWriteFilesystem: false,
|
|
changesPermissions: false,
|
|
requiresRoot: false,
|
|
},
|
|
limitations: [],
|
|
});
|
|
|
|
describe("profile resolution terminal facts", () => {
|
|
it.each([false, true])(
|
|
"preserves a physical profile failure across late caller abort (throws: %s)",
|
|
async (throws) => {
|
|
const alpha = candidate("alpha");
|
|
const controller = new AbortController();
|
|
const primary = new ProviderAdapterError(
|
|
"alpha",
|
|
"resolve_analysis_profile",
|
|
{ cause: new Error("physical profile failure") },
|
|
);
|
|
const registry = new AnalysisProviderRegistry([
|
|
{
|
|
...alpha.provider,
|
|
resolveAnalysisProfile: async () => {
|
|
controller.abort({ caller: "after physical failure" });
|
|
if (throws) throw primary;
|
|
return err(primary);
|
|
},
|
|
},
|
|
]);
|
|
const result = await registry.select(DATABASE_TARGET, "alpha", {
|
|
signal: controller.signal,
|
|
});
|
|
expect(result).toEqual(err(primary));
|
|
expect(registry.hasPendingOwnedResources()).toBe(false);
|
|
expect(await registry.closeOwnedResources()).toEqual(ok(null));
|
|
},
|
|
);
|
|
|
|
it("preserves exact cancellation with cleanup and blocks admission until its owner confirms release", async () => {
|
|
const alpha = candidate("alpha");
|
|
const controller = new AbortController();
|
|
const reason = { caller: "final cleanup" };
|
|
const primary = new AnalysisCancelledError("resolve_analysis_profile", {
|
|
cause: reason,
|
|
cleanup: { reason: "file close rejected", resources: ["profile-file"] },
|
|
});
|
|
let attempts = 0;
|
|
let allowClose = false;
|
|
const registry = new AnalysisProviderRegistry([
|
|
{
|
|
...alpha.provider,
|
|
resolveAnalysisProfile: async () => {
|
|
attempts++;
|
|
controller.abort(reason);
|
|
return err(primary);
|
|
},
|
|
closeOwnedResources: async () => {
|
|
if (!allowClose) return err(primary);
|
|
primary.resolveCleanup();
|
|
return ok(null);
|
|
},
|
|
},
|
|
]);
|
|
const result = await registry.select(DATABASE_TARGET, "alpha", {
|
|
signal: controller.signal,
|
|
});
|
|
expect(result).toEqual(err(primary));
|
|
expect(primary.cause).toBe(reason);
|
|
expect(registry.hasPendingOwnedResources()).toBe(true);
|
|
expect(await registry.select(DATABASE_TARGET, "alpha")).toEqual(
|
|
err(primary),
|
|
);
|
|
expect(attempts).toBe(1);
|
|
allowClose = true;
|
|
expect(await registry.closeOwnedResources()).toEqual(ok(null));
|
|
expect(primary.cleanupIncomplete).toBe(false);
|
|
expect(registry.hasPendingOwnedResources()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("automatic profile cleanup ownership", () => {
|
|
it.each(["alpha", "beta"])(
|
|
"keeps automatic mixed-child cleanup conjunctive when %s releases first",
|
|
async (first) => {
|
|
const alpha = candidate("alpha");
|
|
const beta = candidate("beta");
|
|
const controller = new AbortController();
|
|
const reason = { caller: "mixed profile finalization" };
|
|
const failures = {
|
|
alpha: new AnalysisCancelledError("resolve_analysis_profile", {
|
|
cause: reason,
|
|
cleanup: { reason: "alpha descriptor", resources: ["alpha-file"] },
|
|
}),
|
|
beta: new ProviderCleanupError("beta", ["beta-file"], {
|
|
reason: "beta descriptor",
|
|
}),
|
|
};
|
|
let entered = 0;
|
|
let admitChildren: (() => void) | undefined;
|
|
const admitted = new Promise<void>((resolve) => {
|
|
admitChildren = resolve;
|
|
});
|
|
const released = new Set<string>();
|
|
const registry = new AnalysisProviderRegistry(
|
|
[alpha, beta].map((fixture) => {
|
|
const id = fixture.provider.identity().id as "alpha" | "beta";
|
|
return {
|
|
...fixture.provider,
|
|
resolveAnalysisProfile: async () => {
|
|
entered++;
|
|
if (entered === 2) {
|
|
controller.abort(reason);
|
|
admitChildren?.();
|
|
}
|
|
await admitted;
|
|
return err(failures[id]);
|
|
},
|
|
closeOwnedResources: async () => {
|
|
if (!released.has(id)) return err(failures[id]);
|
|
failures[id].resolveCleanup();
|
|
return ok(null);
|
|
},
|
|
};
|
|
}),
|
|
);
|
|
const result = await registry.select(DATABASE_TARGET, "auto", {
|
|
signal: controller.signal,
|
|
});
|
|
if (result.ok) throw new Error("Mixed failed profiles admitted");
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
expect(result.error.cleanupResources).toEqual([
|
|
"alpha-file",
|
|
"beta-file",
|
|
]);
|
|
expect(result.error.diagnostics?.profile_errors).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ provider_id: "alpha" }),
|
|
expect.objectContaining({ provider_id: "beta" }),
|
|
]),
|
|
);
|
|
const close1 = await registry.closeOwnedResources();
|
|
expect(close1.ok).toBe(false);
|
|
released.add(first);
|
|
const close2 = await registry.closeOwnedResources();
|
|
expect(close2.ok).toBe(false);
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
expect(result.error.cleanupResources).toEqual([
|
|
first === "alpha" ? "beta-file" : "alpha-file",
|
|
]);
|
|
expect(close1.ok ? [] : close1.error.cleanupResources).toEqual([
|
|
first === "alpha" ? "beta-file" : "alpha-file",
|
|
]);
|
|
released.add(first === "alpha" ? "beta" : "alpha");
|
|
expect(await registry.closeOwnedResources()).toEqual(ok(null));
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
if (close1.ok || close2.ok)
|
|
throw new Error("Cleanup unexpectedly succeeded");
|
|
expect(close1.error.cleanupIncomplete).toBe(false);
|
|
expect(close2.error.cleanupIncomplete).toBe(false);
|
|
expect(registry.hasPendingOwnedResources()).toBe(false);
|
|
},
|
|
);
|
|
});
|
|
|
|
it("registers before production and gates fresh selections until shutdown has joined finalization", async () => {
|
|
const alpha = candidate("alpha");
|
|
let calls = 0;
|
|
let enter: (() => void) | undefined;
|
|
let finish: (() => void) | undefined;
|
|
const entered = new Promise<void>((resolve) => {
|
|
enter = resolve;
|
|
});
|
|
const gate = new Promise<void>((resolve) => {
|
|
finish = resolve;
|
|
});
|
|
const registry = new AnalysisProviderRegistry([
|
|
{
|
|
...alpha.provider,
|
|
resolveAnalysisProfile: async (target) => {
|
|
calls++;
|
|
if (calls === 1) {
|
|
enter?.();
|
|
await gate;
|
|
}
|
|
return alpha.provider.resolveAnalysisProfile(target);
|
|
},
|
|
},
|
|
]);
|
|
const selecting = registry.select(DATABASE_TARGET, "alpha");
|
|
await entered;
|
|
const closing = registry.closeOwnedResources();
|
|
const next = registry.select(DATABASE_TARGET, "alpha");
|
|
try {
|
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
expect(calls).toBe(1);
|
|
expect(registry.hasPendingOwnedResources()).toBe(true);
|
|
finish?.();
|
|
expect(await selecting).toMatchObject({
|
|
ok: false,
|
|
error: { _tag: "AnalysisCancelledError" },
|
|
});
|
|
expect(await closing).toEqual(ok(null));
|
|
expect((await next).ok).toBe(true);
|
|
expect(calls).toBe(2);
|
|
expect(registry.hasPendingOwnedResources()).toBe(false);
|
|
} finally {
|
|
finish?.();
|
|
await Promise.all([selecting, closing, next]);
|
|
await registry.closeOwnedResources();
|
|
}
|
|
});
|
|
|
|
it("joins cancelled readiness finalization and shutdown before admitting another observation", async () => {
|
|
const base = candidate("alpha").provider;
|
|
let enter!: () => void;
|
|
let finish!: () => void;
|
|
const entered = new Promise<void>((resolve) => {
|
|
enter = resolve;
|
|
});
|
|
const gate = new Promise<void>((resolve) => {
|
|
finish = resolve;
|
|
});
|
|
let calls = 0;
|
|
let observed: AbortSignal | undefined;
|
|
const registry = new AnalysisProviderRegistry([
|
|
{
|
|
...base,
|
|
inspectAvailability: async ({ signal } = {}) => {
|
|
calls++;
|
|
if (calls === 1) {
|
|
observed = signal;
|
|
enter();
|
|
await gate;
|
|
}
|
|
return base.inspectAvailability();
|
|
},
|
|
},
|
|
]);
|
|
const controller = new AbortController();
|
|
const reason = { request: "opaque readiness cancellation" };
|
|
let settled = false;
|
|
const observing = registry
|
|
.candidates(undefined, { signal: controller.signal })
|
|
.catch((cause) => {
|
|
settled = true;
|
|
return cause;
|
|
});
|
|
await entered;
|
|
controller.abort(reason);
|
|
let closed = false;
|
|
const closing = registry.closeOwnedResources().then((value) => {
|
|
closed = true;
|
|
return value;
|
|
});
|
|
const next = registry.candidates();
|
|
try {
|
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
expect(settled).toBe(false);
|
|
expect(closed).toBe(false);
|
|
expect(calls).toBe(1);
|
|
expect(observed?.reason).toBe(reason);
|
|
expect(registry.hasPendingOwnedResources()).toBe(true);
|
|
finish();
|
|
const failure = await observing;
|
|
expect(failure).toBeInstanceOf(AnalysisCancelledError);
|
|
expect(failure.cause).toBe(reason);
|
|
expect(await closing).toEqual(ok(null));
|
|
expect(await next).toMatchObject([
|
|
{
|
|
availability: {
|
|
status: "unknown",
|
|
diagnostics: { finalization: "in_progress" },
|
|
},
|
|
},
|
|
]);
|
|
expect(await registry.candidates()).toHaveLength(1);
|
|
expect(calls).toBe(2);
|
|
expect(registry.hasPendingOwnedResources()).toBe(false);
|
|
} finally {
|
|
finish();
|
|
await Promise.allSettled([observing, closing, next]);
|
|
await registry.closeOwnedResources();
|
|
}
|
|
});
|
|
|
|
it("joins every candidate and retains physical failure with exact mixed cleanup owners", async () => {
|
|
const alpha = candidate("alpha").provider;
|
|
const beta = candidate("beta").provider;
|
|
const gamma = candidate("gamma").provider;
|
|
let finish!: () => void;
|
|
let enter!: () => void;
|
|
const entered = new Promise<void>((resolve) => {
|
|
enter = resolve;
|
|
});
|
|
const gate = new Promise<void>((resolve) => {
|
|
finish = resolve;
|
|
});
|
|
const physical = new ProviderAdapterError("alpha", "binary_session", {
|
|
diagnostics: { reason: "physical observation failed" },
|
|
});
|
|
const cleanup = new ProviderCleanupError(
|
|
"beta",
|
|
["beta retained prerequisite"],
|
|
{ reason: "controlled cleanup refusal" },
|
|
);
|
|
let releases = 0;
|
|
const registry = new AnalysisProviderRegistry([
|
|
{
|
|
...alpha,
|
|
inspectAvailability: async () => {
|
|
throw physical;
|
|
},
|
|
},
|
|
{
|
|
...beta,
|
|
inspectAvailability: async () => {
|
|
enter();
|
|
await gate;
|
|
throw cleanup;
|
|
},
|
|
closeOwnedResources: async () => {
|
|
releases++;
|
|
cleanup.resolveCleanup();
|
|
return ok(null);
|
|
},
|
|
},
|
|
gamma,
|
|
]);
|
|
let settled = false;
|
|
const observing = registry.candidates().catch((cause) => {
|
|
settled = true;
|
|
return cause;
|
|
});
|
|
await entered;
|
|
try {
|
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
expect(settled).toBe(false);
|
|
finish();
|
|
const failure = await observing;
|
|
expect(failure.cleanupIncomplete).toBe(true);
|
|
expect(failure.cleanupResources).toContain("beta retained prerequisite");
|
|
expect(failure.diagnostics).toMatchObject({
|
|
readiness_candidates: [
|
|
{ provider: { id: "gamma" }, availability: { status: "available" } },
|
|
],
|
|
});
|
|
expect(failure.cleanupPrimary).toBe(physical);
|
|
expect(projectAnalysisError(failure).details).toMatchObject({
|
|
diagnostics: {
|
|
readiness_errors: expect.arrayContaining([
|
|
{
|
|
provider_id: "alpha",
|
|
error: expect.objectContaining({ code: "execution_failure" }),
|
|
},
|
|
{
|
|
provider_id: "beta",
|
|
error: expect.objectContaining({ code: "cleanup_incomplete" }),
|
|
},
|
|
]),
|
|
},
|
|
});
|
|
expect(registry.hasPendingOwnedResources()).toBe(true);
|
|
expect(await registry.closeOwnedResources()).toEqual(ok(null));
|
|
expect(releases).toBe(1);
|
|
expect(failure.cleanupIncomplete).toBe(false);
|
|
expect(projectAnalysisError(failure)).toMatchObject({
|
|
code: "execution_failure",
|
|
details: { diagnostics: { reason: "physical observation failed" } },
|
|
});
|
|
expect(registry.hasPendingOwnedResources()).toBe(false);
|
|
} finally {
|
|
finish();
|
|
await observing;
|
|
await registry.closeOwnedResources();
|
|
}
|
|
});
|
|
|
|
it("registers selection before async readiness can race shutdown", async () => {
|
|
let calls = 0;
|
|
let observed: AbortSignal | undefined;
|
|
const base = candidate("alpha").provider;
|
|
const registry = new AnalysisProviderRegistry([
|
|
{
|
|
...base,
|
|
inspectAvailability: async ({ signal } = {}) => {
|
|
calls++;
|
|
observed = signal;
|
|
return base.inspectAvailability();
|
|
},
|
|
},
|
|
]);
|
|
const selecting = registry.select(DATABASE_TARGET, "alpha");
|
|
const closing = registry.closeOwnedResources();
|
|
const [selected, closed] = await Promise.all([selecting, closing]);
|
|
expect(selected).toMatchObject({
|
|
ok: false,
|
|
error: { _tag: "AnalysisCancelledError" },
|
|
});
|
|
expect(closed).toEqual(ok(null));
|
|
expect(calls).toBe(1);
|
|
expect(observed?.aborted).toBe(true);
|
|
expect((await registry.select(DATABASE_TARGET, "alpha")).ok).toBe(true);
|
|
expect(calls).toBe(2);
|
|
expect(registry.hasPendingOwnedResources()).toBe(false);
|
|
});
|