mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
fb146bf40b
Return a closeable owner before prerequisite work begins. Share creation across independent callers, join the final departure and late handoffs, retain exact failed resource owners, and gate operation dispatch again after acquisition. Keep reuse admission separate from native health. Move shared provider ownership below application workflows and consolidate failure attribution; remove the superseded lazy client. Validation: 34 focused owner, lazy, composite and session controls; check:fast. Compiled baseline/candidate recording-provider workflow reproduces then fences dispatch after close. No native provider claim. Refs #4561, #4667
257 lines
8.5 KiB
TypeScript
257 lines
8.5 KiB
TypeScript
import { expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
createAnalysisExecution,
|
|
type AnalysisClient,
|
|
type AnalysisOperation,
|
|
type AnalysisProvider,
|
|
type CapabilityDescriptor,
|
|
type ProviderIdentity,
|
|
} from "../AnalysisProvider.js";
|
|
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
|
|
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
|
import { err, ok } from "../../domain/result.js";
|
|
import { CompositeProvider } from "./CompositeProvider.js";
|
|
|
|
const identity = (id: string): ProviderIdentity => ({
|
|
id,
|
|
name: id,
|
|
version: "1",
|
|
});
|
|
|
|
const operationCapability = (
|
|
provider: ProviderIdentity,
|
|
operation: Exclude<AnalysisOperation, "health">,
|
|
): CapabilityDescriptor => ({
|
|
provider,
|
|
operation,
|
|
available: true,
|
|
reason: null,
|
|
effects: {
|
|
mutatesArtifact: false,
|
|
launchesProcess: false,
|
|
mayShowUi: false,
|
|
mayAccessNetwork: false,
|
|
mayWriteFilesystem: false,
|
|
changesPermissions: false,
|
|
requiresRoot: false,
|
|
},
|
|
limitations: [],
|
|
});
|
|
|
|
const failingProvider = (
|
|
id: string,
|
|
operation: Exclude<AnalysisOperation, "health">,
|
|
resource: string,
|
|
) => {
|
|
const providerIdentity = identity(id);
|
|
const cleanupError = new ProviderCleanupError(
|
|
id,
|
|
[resource],
|
|
{
|
|
reason: `${id} cleanup is unresolved`,
|
|
child_marker: `${id}-detail`,
|
|
},
|
|
{ remediationAction: `Resolve ${resource} before retrying ${id}.` },
|
|
);
|
|
const close = vi.fn(async () => err(cleanupError));
|
|
const provider: AnalysisProvider = {
|
|
identity: () => providerIdentity,
|
|
capabilities: () => [operationCapability(providerIdentity, operation)],
|
|
createClient: (): AnalysisClient => ({
|
|
execute: (name) =>
|
|
Promise.resolve(
|
|
ok(createAnalysisExecution(`${id}:${name}`, providerIdentity)),
|
|
),
|
|
close,
|
|
}),
|
|
};
|
|
return { provider, close, cleanupError };
|
|
};
|
|
|
|
const executableTarget = {
|
|
path: "/tmp/fixture",
|
|
sha256: "a".repeat(64),
|
|
kind: "executable",
|
|
format: "elf",
|
|
architecture: "x86_64",
|
|
availableArchitectures: ["x86_64"],
|
|
} as const;
|
|
|
|
it("does not create a previously unused child after the composite closes", async () => {
|
|
const child = failingProvider("provider-only", "address_name", "one-db");
|
|
const create = vi.spyOn(child.provider, "createClient");
|
|
const client = new CompositeProvider([child.provider]).createClient(
|
|
executableTarget,
|
|
);
|
|
expect(await client.close()).toEqual(ok(null));
|
|
expect(client.reuseReadiness?.()).toBe("blocked");
|
|
expect(await client.executeIfReady?.("address_name", {})).toBeUndefined();
|
|
expect(await client.execute("address_name", {})).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
_tag: "ProviderAdapterError",
|
|
diagnostics: { reason: "client_closed" },
|
|
},
|
|
});
|
|
expect(create).not.toHaveBeenCalled();
|
|
expect(child.close).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("retains every failed child resource across composite cleanup retries", async () => {
|
|
const left = failingProvider("provider-left", "address_name", "left-db");
|
|
const right = failingProvider("provider-right", "procedure_info", "right-db");
|
|
const client = new CompositeProvider([
|
|
left.provider,
|
|
right.provider,
|
|
]).createClient(executableTarget);
|
|
|
|
expect((await client.execute("address_name", {})).ok).toBe(true);
|
|
expect((await client.execute("procedure_info", {})).ok).toBe(true);
|
|
|
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
const closed = await client.close();
|
|
expect(closed.ok).toBe(false);
|
|
if (closed.ok) throw new Error("Expected composite cleanup failure");
|
|
expect(closed.error).toMatchObject({
|
|
cleanupIncomplete: true,
|
|
cleanupResources: ["left-db", "right-db"],
|
|
diagnostics: {
|
|
failures: [
|
|
{ provider_id: "provider-left", resources: ["left-db"] },
|
|
{ provider_id: "provider-right", resources: ["right-db"] },
|
|
],
|
|
},
|
|
});
|
|
expect(closed.error).toBeInstanceOf(ProviderCleanupError);
|
|
const projected = projectAnalysisError(closed.error);
|
|
expect(projected.remediation.action).toContain(
|
|
"Resolve left-db before retrying provider-left.",
|
|
);
|
|
expect(projected.remediation.action).toContain(
|
|
"Resolve right-db before retrying provider-right.",
|
|
);
|
|
expect(closed.error.diagnostics).toMatchObject({
|
|
failures: [
|
|
{
|
|
diagnostics: { reason: "provider-left cleanup is unresolved" },
|
|
},
|
|
{
|
|
diagnostics: { reason: "provider-right cleanup is unresolved" },
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
expect(left.close).toHaveBeenCalledTimes(2);
|
|
expect(right.close).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("preserves one child's typed cleanup error unchanged", async () => {
|
|
const child = failingProvider("provider-only", "address_name", "one-db");
|
|
const client = new CompositeProvider([child.provider]).createClient(
|
|
executableTarget,
|
|
);
|
|
expect((await client.execute("address_name", {})).ok).toBe(true);
|
|
|
|
const closed = await client.close();
|
|
|
|
expect(closed.ok).toBe(false);
|
|
if (closed.ok) throw new Error("Expected child cleanup failure");
|
|
expect(closed.error).toBe(child.cleanupError);
|
|
});
|
|
|
|
it("does not close a child again after its cleanup succeeded", async () => {
|
|
const firstIdentity = identity("provider-first");
|
|
const secondIdentity = identity("provider-second");
|
|
let secondCloseAttempts = 0;
|
|
const firstClose = vi.fn(async () => ok(null));
|
|
const secondClose = vi.fn(async () => {
|
|
secondCloseAttempts += 1;
|
|
return secondCloseAttempts === 1
|
|
? err(new ProviderCleanupError("provider-second", ["second-db"], {}))
|
|
: ok(null);
|
|
});
|
|
const makeProvider = (
|
|
providerIdentity: ProviderIdentity,
|
|
operation: Exclude<AnalysisOperation, "health">,
|
|
close: AnalysisClient["close"],
|
|
): AnalysisProvider => ({
|
|
identity: () => providerIdentity,
|
|
capabilities: () => [operationCapability(providerIdentity, operation)],
|
|
createClient: () => ({
|
|
execute: async () => ok(createAnalysisExecution(null, providerIdentity)),
|
|
close,
|
|
}),
|
|
});
|
|
const client = new CompositeProvider([
|
|
makeProvider(firstIdentity, "address_name", firstClose),
|
|
makeProvider(secondIdentity, "procedure_info", secondClose),
|
|
]).createClient(executableTarget);
|
|
await client.execute("address_name", {});
|
|
await client.execute("procedure_info", {});
|
|
|
|
expect((await client.close({ retainDocument: true })).ok).toBe(false);
|
|
expect((await client.close()).ok).toBe(true);
|
|
expect(firstClose).toHaveBeenCalledTimes(1);
|
|
expect(secondClose).toHaveBeenCalledTimes(2);
|
|
expect((await client.close({ retainDocument: true })).ok).toBe(true);
|
|
expect(firstClose).toHaveBeenCalledTimes(1);
|
|
expect(secondClose).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it.each(["busy", "unknown", "idle", undefined] as const)(
|
|
"keeps partial health truthful with child state %s and an unobserved peer",
|
|
async (state) => {
|
|
const observedIdentity = identity("provider-observed");
|
|
const unobservedIdentity = identity("provider-unobserved");
|
|
const observed: AnalysisProvider = {
|
|
identity: () => observedIdentity,
|
|
capabilities: () => [
|
|
operationCapability(observedIdentity, "address_name"),
|
|
],
|
|
createClient: () => ({
|
|
execute: async () =>
|
|
ok(createAnalysisExecution(null, observedIdentity)),
|
|
operationHealthSnapshot: () =>
|
|
state === undefined
|
|
? undefined
|
|
: {
|
|
state,
|
|
stage: state === "idle" ? null : "analysis",
|
|
retryAction:
|
|
state === "busy"
|
|
? "wait"
|
|
: state === "idle"
|
|
? null
|
|
: "unknown",
|
|
exitCode: null,
|
|
requests: [],
|
|
},
|
|
close: async () => ok(null),
|
|
}),
|
|
};
|
|
const unobserved: AnalysisProvider = {
|
|
identity: () => unobservedIdentity,
|
|
capabilities: () => [
|
|
operationCapability(unobservedIdentity, "procedure_info"),
|
|
],
|
|
createClient: () => ({
|
|
execute: async () =>
|
|
ok(createAnalysisExecution(null, unobservedIdentity)),
|
|
close: async () => ok(null),
|
|
}),
|
|
};
|
|
const client = new CompositeProvider([observed, unobserved]).createClient(
|
|
executableTarget,
|
|
);
|
|
await client.execute("address_name", {});
|
|
await client.execute("procedure_info", {});
|
|
|
|
if (state === "idle" || state === undefined)
|
|
expect(client.operationHealthSnapshot?.()).toBeUndefined();
|
|
else expect(client.operationHealthSnapshot?.()).toMatchObject({ state });
|
|
expect((await client.close()).ok).toBe(true);
|
|
},
|
|
);
|