fix(io): consolidate managed snapshots and retain cleanup ownership

Reuse managed snapshot admission and hashing in sequential path comparisons, retaining caller paths and completed inspection Evidence on later failure. Preserve exact failed-close owners alongside typed read and cancellation failures, and retry them through caller-owned resource scopes. Require canonical Hopper lease identities, remove signed offset coercion, and repair stale setup cleanup expectations that failed CI.
This commit is contained in:
morluto
2026-10-12 14:46:37 +08:00
parent e9970855ba
commit 98d8470d36
16 changed files with 899 additions and 225 deletions
+6
View File
@@ -53,6 +53,12 @@ CIL decoding, and projected inspection records have a separate conservative
output occurrences still consume that budget. These are inspection resource
policies, not PE format limits or a hard bound on total V8 heap use.
Path-based member comparison uses the same per-input snapshot policy and
inspects its two selected paths sequentially, retaining each inspection rather
than both full byte snapshots. The limit therefore remains a per-file resource
admission rule, not an aggregate comparison limit. Comparison Evidence keeps
the caller-selected paths as its parameters and artifact locations.
Input overflow returns a resource constraint. Decode or projection overflow
reports `resource-limit` with unavailable or partial coverage; omitted facts do
not become empty observations. Ambiguous PE section mappings are rejected
+17 -9
View File
@@ -207,18 +207,26 @@ export const parseBinaryTarget = async (
if (cause instanceof AnalysisCancelledError) return err(cause);
if (cause instanceof BinaryTargetError) return err(cause);
if (!(cause instanceof Error)) throw cause;
const code = filesystemErrorCode(cause);
if (code === undefined) throw cause;
return err(
new BinaryTargetError(
candidate,
targetFailureReason(code, cause.message),
{ cause },
),
);
const failure = classifyBinaryTargetFilesystemFailure(candidate, cause);
if (failure === undefined) throw cause;
return err(failure);
}
};
/** Classify a filesystem failure while reading a previously selected target. */
export const classifyBinaryTargetFilesystemFailure = (
path: string,
cause: unknown,
): BinaryTargetError | undefined => {
if (!(cause instanceof Error)) return undefined;
const code = filesystemErrorCode(cause);
return code === undefined
? undefined
: new BinaryTargetError(path, targetFailureReason(code, cause.message), {
cause,
});
};
const filesystemErrorCode = (cause: Error): string | undefined => {
const code: unknown = Reflect.get(cause, "code");
return typeof code === "string" ? code : undefined;
@@ -71,5 +71,9 @@ it("preserves the original explanation and collected output alongside cleanup un
},
},
});
expect(combined.cause).toBe(primary);
expect(combined.cause).toBeInstanceOf(AggregateError);
expect(combined.cause).toMatchObject({
cause: primary,
errors: [primary, cleanup],
});
});
@@ -1,6 +1,3 @@
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { z } from "zod";
import {
@@ -9,14 +6,20 @@ import {
type CompareManagedMembersInput,
} from "../../domain/managed/managedMemberComparison.js";
import { parseManagedMemberEvidence } from "../../domain/managed/managedMemberComparisonMatch.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import { AnalysisProtocolError } from "../../domain/analysisErrorCore.js";
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import { parseBinaryTarget } from "../BinaryTargetResolver.js";
import {
classifyBinaryTargetFilesystemFailure,
parseBinaryTarget,
} from "../BinaryTargetResolver.js";
import { err, ok, type Result } from "../../domain/result.js";
import { inspectManagedMembersBytes } from "../../dotnet/ManagedMemberInspector.js";
import { readManagedSnapshot } from "../../dotnet/ManagedSnapshot.js";
import {
MANAGED_STATIC_PROVIDER,
MANAGED_WORKFLOW_PROVIDER,
@@ -43,109 +46,95 @@ export const compareManagedMembersEvidenceValidated = (
interface ManagedMemberComparisonPathDependencies {
readonly resolveTarget: typeof parseBinaryTarget;
readonly readBytes: (path: string) => Promise<Buffer>;
readonly readSnapshot: typeof readManagedSnapshot;
}
const DEFAULT_PATH_DEPENDENCIES: ManagedMemberComparisonPathDependencies = {
resolveTarget: parseBinaryTarget,
readBytes: readFile,
};
/** Inspect two local PE artifacts and return one derived comparison Evidence. */
export const compareManagedMemberPaths = async (
input: {
readonly leftPath: string;
readonly rightPath: string;
},
dependencies: ManagedMemberComparisonPathDependencies = DEFAULT_PATH_DEPENDENCIES,
dependencies: Partial<ManagedMemberComparisonPathDependencies> = {},
resources?: ArtifactResourceScope,
): Promise<Result<Evidence, AnalysisError>> => {
const operation = "compare_managed_members";
const resolveTarget = dependencies.resolveTarget ?? parseBinaryTarget;
const readSnapshot = dependencies.readSnapshot ?? readManagedSnapshot;
let partialEvidence: Evidence | undefined;
try {
const [leftTarget, rightTarget] = await Promise.all([
dependencies.resolveTarget(input.leftPath),
dependencies.resolveTarget(input.rightPath),
resolveTarget(input.leftPath),
resolveTarget(input.rightPath),
]);
// Report an unopenable path like every other path-based command does,
// with the failed path and constraint, rather than a bare input error.
if (!leftTarget.ok) return err(leftTarget.error);
if (!rightTarget.ok) return err(rightTarget.error);
const [leftBytes, rightBytes] = await Promise.all([
dependencies.readBytes(leftTarget.value.path),
dependencies.readBytes(rightTarget.value.path),
]);
for (const [target, bytes] of [
[leftTarget.value, leftBytes],
[rightTarget.value, rightBytes],
] as const) {
const observedSha256 = createHash("sha256").update(bytes).digest("hex");
if (observedSha256 !== target.sha256)
return err(
new EvidenceIntegrityError(
`Managed artifact digest changed after open: expected ${target.sha256}, observed ${observedSha256} at ${target.path}`,
),
const inspectTarget = async (
target: BinaryTarget,
selectedPath: string,
) => {
const { path } = target;
let snapshot: Awaited<ReturnType<typeof readManagedSnapshot>>;
try {
snapshot = await readSnapshot(
path,
operation,
resources === undefined ? {} : { resources },
);
}
const leftInspection = inspectManagedMembersBytes(
leftBytes,
leftTarget.value,
);
const rightInspection = inspectManagedMembersBytes(
rightBytes,
rightTarget.value,
);
const leftEvidence = createEvidence(
leftTarget.value,
MANAGED_STATIC_PROVIDER,
{
} catch (cause: unknown) {
const failure = classifyBinaryTargetFilesystemFailure(path, cause);
if (failure !== undefined) throw failure;
throw cause;
}
if (snapshot.sha256 !== target.sha256)
throw new EvidenceIntegrityError(
`Managed artifact digest changed after open: expected ${target.sha256}, observed ${snapshot.sha256} at ${target.path}`,
);
const inspection = inspectManagedMembersBytes(snapshot.bytes, target);
const evidence = createEvidence(target, MANAGED_STATIC_PROVIDER, {
operation: "inspect_managed_members",
parameters: {},
result: jsonValueSchema.parse(leftInspection),
result: jsonValueSchema.parse(inspection),
rawResult: null,
limitations: leftInspection.limitations,
locations: [{ kind: "artifact-path", path: leftTarget.value.path }],
},
);
const rightEvidence = createEvidence(
rightTarget.value,
MANAGED_STATIC_PROVIDER,
{
operation: "inspect_managed_members",
parameters: {},
result: jsonValueSchema.parse(rightInspection),
rawResult: null,
limitations: rightInspection.limitations,
locations: [{ kind: "artifact-path", path: rightTarget.value.path }],
},
);
limitations: inspection.limitations,
locations: [{ kind: "artifact-path", path: selectedPath }],
});
return { inspection, evidence };
};
const left = await inspectTarget(leftTarget.value, input.leftPath);
partialEvidence = left.evidence;
const right = await inspectTarget(rightTarget.value, input.rightPath);
const result = compareManagedMembers(
{
evidenceId: leftEvidence.evidence_id,
result: leftInspection,
evidenceId: left.evidence.evidence_id,
result: left.inspection,
},
{
evidenceId: rightEvidence.evidence_id,
result: rightInspection,
evidenceId: right.evidence.evidence_id,
result: right.inspection,
},
);
return ok(
createManagedMemberComparisonEvidence(
{
left: leftEvidence,
right: rightEvidence,
left: left.evidence,
right: right.evidence,
},
result,
{ leftPath: input.leftPath, rightPath: input.rightPath },
),
);
} catch (cause: unknown) {
return workflowFailure(operation, cause);
return workflowFailure(operation, cause, partialEvidence);
}
};
/** ECMA-335 representable limits for path-based member comparison. */
const createManagedMemberComparisonEvidence = (
parameters: Pick<CompareManagedMembersInput, "left" | "right">,
result: z.infer<typeof managedMemberComparisonResultSchema>,
sourcePaths?: { readonly leftPath: string; readonly rightPath: string },
): Evidence =>
createEvidence(undefined, MANAGED_WORKFLOW_PROVIDER, {
predicateType: "rea.managed-member-comparison",
@@ -153,6 +142,12 @@ const createManagedMemberComparisonEvidence = (
parameters: {
left_evidence_id: parameters.left.evidence_id,
right_evidence_id: parameters.right.evidence_id,
...(sourcePaths === undefined
? {}
: {
left_path: sourcePaths.leftPath,
right_path: sourcePaths.rightPath,
}),
},
result: jsonValueSchema.parse(result),
rawResult: null,
@@ -160,18 +155,33 @@ const createManagedMemberComparisonEvidence = (
authority: "analyst-inference",
environment: null,
limitations: result.limitations,
locations:
sourcePaths === undefined
? []
: [
{ kind: "artifact-path", path: sourcePaths.leftPath },
{ kind: "artifact-path", path: sourcePaths.rightPath },
],
evidenceLinks: result.evidence_links,
});
const workflowFailure = (
operation: string,
cause: unknown,
): Result<never, AnalysisError> =>
err(
cause instanceof z.ZodError || cause instanceof TypeError
? workflowInputError(operation, cause)
: new AnalysisProtocolError(
"Managed member comparison produced an invalid result",
{ cause },
),
partialEvidence?: Evidence,
): Result<never, AnalysisError> => {
const failure =
cause instanceof AnalysisError
? cause
: cause instanceof z.ZodError || cause instanceof TypeError
? workflowInputError(operation, cause)
: new AnalysisProtocolError(
"Managed member comparison produced an invalid result",
{ cause },
);
return err(
partialEvidence === undefined
? failure
: failure.retainPartialObservation(partialEvidence),
);
};
+43 -4
View File
@@ -4,8 +4,13 @@ import { compareManagedMemberPaths } from "../application/managed/ManagedMemberC
import { verifyManagedNativeBoundariesEvidence } from "../application/managed/ManagedNativeVerificationService.js";
import { importManagedReconstructionEvidence } from "../application/managed/ManagedReconstructionService.js";
import type { DirectAnalysis } from "../composition/directAnalysis.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { parseCliJsonInput } from "../cliJsonInput.js";
import { MANAGED_STATIC_PROVIDER } from "../application/InvestigationProviders.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import type { Logger } from "pino";
@@ -112,10 +117,44 @@ const registerCompareManagedMembers = (
args: compareManagedMemberArgs,
run: ({ args }) =>
logCliCommand(logger, "compare-managed-members", async () => {
const result = await compareManagedMemberPaths({
leftPath: args.leftPath,
rightPath: args.rightPath,
});
const resources = new ArtifactResourceScope();
const result = await compareManagedMemberPaths(
{
leftPath: args.leftPath,
rightPath: args.rightPath,
},
{},
resources,
);
try {
await resources.close();
} catch (cause: unknown) {
const cleanup = ArtifactReaderFailure.cleanupObservation(
cause,
"Managed member comparison resources",
);
const failure = new ProviderCleanupError(
MANAGED_STATIC_PROVIDER.id,
cleanup.resources,
{ reason: cleanup.reason },
{
cause,
operation: "compare_managed_members",
...(result.ok ? { partialObservation: result.value } : {}),
},
);
const error = result.ok
? failure
: analysisErrorWithCleanupFailure(
result.error,
failure,
"compare_managed_members",
);
return {
error: "Managed member comparison failed",
...projectAnalysisError(error),
};
}
return result.ok
? result.value
: {
+5 -1
View File
@@ -13,7 +13,11 @@ export const analysisErrorWithCleanupFailure = (
cleanup instanceof ProviderAdapterError ? cleanup.providerId : "rea",
operation,
{
cause: primary,
cause: new AggregateError(
[primary, cleanup],
"Analysis failed and cleanup remains incomplete.",
{ cause: primary },
),
userMessage: projectAnalysisError(primary).message,
...(primary.capturedOutput === undefined
? {}
+1 -1
View File
@@ -135,7 +135,7 @@ const requireRange = (
);
};
const align4 = (value: number): number => (value + 3) & ~3;
const align4 = (value: number): number => Math.ceil(value / 4) * 4;
const readStreamName = (
bytes: Buffer,
+1 -1
View File
@@ -132,7 +132,7 @@ export const methodBody = (
"utf8",
);
const methodEnd = ilOffset + header.ilSize;
const sectionOffset = (methodEnd + 3) & ~3;
const sectionOffset = Math.ceil(methodEnd / 4) * 4;
const exceptionRegions =
header.format === "fat" && (header.flags & 8) !== 0
? validateExceptionRegionRanges(
+190
View File
@@ -0,0 +1,190 @@
import { createHash } from "node:crypto";
import type { Stats } from "node:fs";
import type { FileHandle } from "node:fs/promises";
import { MANAGED_STATIC_PROVIDER } from "../application/InvestigationProviders.js";
import { classifyBinaryTargetFilesystemFailure } from "../application/BinaryTargetResolver.js";
import { withRegularFile } from "../application/RegularFileRead.js";
import {
RegularFileCleanupFailure,
type RegularFileReadOptions,
} from "../application/RegularFileRead.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { RegularFileChangedError } from "../filesystem/RegularFile.js";
import { readBoundedFileBytes } from "../process/BoundedFileBytes.js";
// Managed static inspection retains the complete PE snapshot in memory.
export const MAX_MANAGED_SNAPSHOT_BYTES = 128 * 1024 * 1024;
interface ManagedSourceObservation {
readonly byteLength: number;
readonly sha256: string;
readonly bytes?: Buffer;
}
interface ManagedSnapshot extends ManagedSourceObservation {
readonly bytes: Buffer;
}
const managedSnapshotCapacityError = (
operation: string,
): AnalysisResourceConstraintError =>
new AnalysisResourceConstraintError(
operation,
"memory",
"Managed static inspection requires an eagerly retained PE snapshot within its input byte budget.",
{ max_snapshot_bytes: MAX_MANAGED_SNAPSHOT_BYTES },
);
export const readManagedSnapshot = async (
path: string,
operation: string,
options: RegularFileReadOptions = {},
): Promise<ManagedSnapshot> => {
return readManagedFile(
path,
operation,
async (handle, metadata) => {
const signal = options.signal;
if (metadata.size > MAX_MANAGED_SNAPSHOT_BYTES)
throw managedSnapshotCapacityError(operation);
const bytes = await readBoundedFileBytes(
handle,
MAX_MANAGED_SNAPSHOT_BYTES,
signal,
);
if (bytes === undefined) throw managedSnapshotCapacityError(operation);
signal?.throwIfAborted();
return {
byteLength: bytes.length,
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes,
};
},
options,
);
};
export const hashManagedSource = async (
path: string,
operation: string,
options: RegularFileReadOptions = {},
): Promise<ManagedSourceObservation> =>
readManagedFile(
path,
operation,
async (handle, metadata) => {
const signal = options.signal;
if (metadata.size > MAX_MANAGED_SNAPSHOT_BYTES)
throw managedSnapshotCapacityError(operation);
const digest = createHash("sha256");
let byteLength = 0;
const stream = handle.createReadStream({
autoClose: false,
...(signal === undefined ? {} : { signal }),
});
for await (const chunk of stream) {
signal?.throwIfAborted();
if (!Buffer.isBuffer(chunk))
throw new TypeError("Managed source stream returned non-buffer data");
if (chunk.length > MAX_MANAGED_SNAPSHOT_BYTES - byteLength)
throw managedSnapshotCapacityError(operation);
byteLength += chunk.length;
digest.update(chunk);
}
return { byteLength, sha256: digest.digest("hex") };
},
options,
);
const readManagedFile = <Value>(
path: string,
operation: string,
read: (handle: FileHandle, metadata: Stats) => Promise<Value>,
options: RegularFileReadOptions,
): Promise<Value> =>
withRegularFile(path, read, options).catch((cause: unknown) => {
throw normalizeManagedReadFailure(cause, path, operation, options.signal);
});
/** Keep cleanup ownership and the original typed read failure at this boundary. */
const normalizeManagedReadFailure = (
cause: unknown,
fallbackPath: string,
operation: string,
signal?: AbortSignal,
): unknown => {
const cleanupError = managedCleanupError(
cause,
fallbackPath,
operation,
signal,
);
if (cleanupError !== undefined) return cleanupError;
return normalizeReadFailure(cause, fallbackPath, operation, signal);
};
/** Project cleanup failures owned by managed file reads or their shared scope. */
export const managedCleanupError = (
cause: unknown,
fallbackPath: string,
operation: string,
signal?: AbortSignal,
): AnalysisError | undefined => {
if (cause instanceof RegularFileCleanupFailure) {
const cleanup = new ProviderCleanupError(
MANAGED_STATIC_PROVIDER.id,
[cause.path],
{ reason: "A managed input file handle could not be closed" },
{ cause, operation },
);
const failure =
cause.outcome.kind === "failed"
? normalizeReadFailure(
cause.outcome.cause,
cause.path,
operation,
signal,
)
: undefined;
return failure instanceof AnalysisError
? analysisErrorWithCleanupFailure(failure, cleanup, operation)
: cleanup;
}
if (cause instanceof ArtifactReaderFailure && cause.cleanup !== undefined)
return new ProviderCleanupError(
MANAGED_STATIC_PROVIDER.id,
cause.cleanup.resources.length > 0
? cause.cleanup.resources
: [fallbackPath],
{ reason: cause.cleanup.reason },
{ cause, operation },
);
return undefined;
};
const normalizeReadFailure = (
cause: unknown,
path: string,
operation: string,
signal?: AbortSignal,
): unknown =>
cause instanceof RegularFileChangedError
? new EvidenceIntegrityError(
`Managed source changed while ${operation} was reading ${path}.`,
{ cause },
)
: cause instanceof AnalysisError
? cause
: (classifyBinaryTargetFilesystemFailure(path, cause) ??
(signal?.aborted === true
? new AnalysisCancelledError(operation, { cause })
: cause));
+49 -96
View File
@@ -1,5 +1,3 @@
import { createHash } from "node:crypto";
import {
createAnalysisExecution,
type AnalysisClient,
@@ -9,6 +7,8 @@ import {
type ExecutionOptions,
type ProviderIdentity,
} from "../application/AnalysisProvider.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { managedStaticCapabilities } from "./ManagedStaticProviderMetadata.js";
import { MANAGED_STATIC_PROVIDER } from "../application/InvestigationProviders.js";
import {
@@ -22,10 +22,10 @@ import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import type { EvidenceLocation } from "../domain/evidence.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type {
@@ -37,23 +37,12 @@ import { err, ok, type Result } from "../domain/result.js";
import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
import { inspectManagedNativeBoundariesBytes } from "./ManagedNativeBoundaryInspector.js";
import { openRegularFile } from "../filesystem/RegularFile.js";
import { readBoundedFileBytes } from "../process/BoundedFileBytes.js";
// This provider eagerly retains the full PE snapshot. Keep its input resource
// policy separate from decoded metadata/output capacity; it is not a PE format limit.
const MAX_MANAGED_SNAPSHOT_BYTES = 128 * 1024 * 1024;
const snapshotCapacityError = (
operation: string,
): AnalysisResourceConstraintError =>
new AnalysisResourceConstraintError(
operation,
"memory",
"Managed static inspection requires an eagerly retained PE snapshot within its input byte budget.",
{ max_snapshot_bytes: MAX_MANAGED_SNAPSHOT_BYTES },
);
import { RegularFileCleanupFailure } from "../application/RegularFileRead.js";
import {
hashManagedSource,
managedCleanupError,
readManagedSnapshot,
} from "./ManagedSnapshot.js";
/** Execution-free managed PE/CLI auxiliary provider. */
export class ManagedStaticProvider implements AnalysisProvider {
@@ -75,6 +64,7 @@ export class ManagedStaticProvider implements AnalysisProvider {
class ManagedStaticClient implements AnalysisClient {
#snapshotBytes: Buffer | undefined;
readonly #resources = new ArtifactResourceScope();
constructor(private readonly target: BinaryTarget) {}
@@ -107,16 +97,14 @@ class ManagedStaticClient implements AnalysisClient {
const snapshot = this.#snapshotBytes;
const observed =
snapshot === undefined
? await readManagedSnapshot(
this.target.path,
operation,
options?.signal,
)
: await hashManagedSource(
this.target.path,
operation,
options?.signal,
);
? await readManagedSnapshot(this.target.path, operation, {
signal: options?.signal,
resources: this.#resources,
})
: await hashManagedSource(this.target.path, operation, {
signal: options?.signal,
resources: this.#resources,
});
if (observed.sha256 !== this.target.sha256)
return err(
new EvidenceIntegrityError(
@@ -142,9 +130,11 @@ class ManagedStaticClient implements AnalysisClient {
}),
);
} catch (cause: unknown) {
if (isCleanupFailure(cause))
return err(this.#cleanupError(operation, cause));
if (cause instanceof AnalysisError) return err(cause);
if (options?.signal?.aborted === true)
return err(new AnalysisCancelledError(operation));
if (cause instanceof AnalysisError) return err(cause);
return err(
new ProviderAdapterError(MANAGED_STATIC_PROVIDER.id, operation, {
cause,
@@ -153,74 +143,37 @@ class ManagedStaticClient implements AnalysisClient {
}
}
close(): Promise<Result<null, AnalysisError>> {
async close(): Promise<Result<null, AnalysisError>> {
this.#snapshotBytes = undefined;
return Promise.resolve(ok(null));
}
}
interface ManagedSourceObservation {
readonly byteLength: number;
readonly sha256: string;
readonly bytes?: Buffer;
}
const readManagedSnapshot = async (
path: string,
operation: string,
signal?: AbortSignal,
): Promise<ManagedSourceObservation> => {
const handle = await openRegularFile(path, { symlinks: "follow", signal });
try {
const metadata = await handle.stat();
if (metadata.size > MAX_MANAGED_SNAPSHOT_BYTES)
throw snapshotCapacityError(operation);
const bytes = await readBoundedFileBytes(
handle,
MAX_MANAGED_SNAPSHOT_BYTES,
signal,
);
if (bytes === undefined) throw snapshotCapacityError(operation);
signal?.throwIfAborted();
return {
byteLength: bytes.length,
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes,
};
} finally {
await handle.close();
}
};
const hashManagedSource = async (
path: string,
operation: string,
signal?: AbortSignal,
): Promise<ManagedSourceObservation> => {
const handle = await openRegularFile(path, { symlinks: "follow", signal });
try {
if ((await handle.stat()).size > MAX_MANAGED_SNAPSHOT_BYTES)
throw snapshotCapacityError(operation);
const digest = createHash("sha256");
let byteLength = 0;
const stream = handle.createReadStream({
autoClose: false,
...(signal === undefined ? {} : { signal }),
});
for await (const chunk of stream) {
signal?.throwIfAborted();
if (!Buffer.isBuffer(chunk))
throw new TypeError("Managed source stream returned non-buffer data");
if (chunk.length > MAX_MANAGED_SNAPSHOT_BYTES - byteLength)
throw snapshotCapacityError(operation);
byteLength += chunk.length;
digest.update(chunk);
try {
await this.#resources.close();
return ok(null);
} catch (cause: unknown) {
return err(this.#cleanupError("close", cause));
}
return { byteLength, sha256: digest.digest("hex") };
} finally {
await handle.close();
}
};
#cleanupError(operation: string, cause: unknown): ProviderCleanupError {
const failure = managedCleanupError(cause, this.target.path, operation);
if (failure instanceof ProviderCleanupError) return failure;
const cleanup =
cause instanceof ArtifactReaderFailure ? cause.cleanup : undefined;
return new ProviderCleanupError(
MANAGED_STATIC_PROVIDER.id,
cleanup?.resources ?? [this.target.path],
{
reason:
cleanup?.reason ??
(cause instanceof Error ? cause.message : String(cause)),
},
{ cause, operation },
);
}
}
const isCleanupFailure = (cause: unknown): boolean =>
cause instanceof RegularFileCleanupFailure ||
(cause instanceof ArtifactReaderFailure && cause.cleanup !== undefined);
const isManagedOperation = (
operation: AnalysisOperation,
+84 -9
View File
@@ -1,8 +1,17 @@
import { execFile } from "node:child_process";
import { once } from "node:events";
import { lstat, mkdir, mkdtemp, readdir, rm, rmdir } from "node:fs/promises";
import {
lstat,
mkdir,
mkdtemp,
readdir,
rm,
rmdir,
symlink,
writeFile,
} from "node:fs/promises";
import { createServer, type Socket } from "node:net";
import { join } from "node:path";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
import { afterEach, describe, expect, it } from "vitest";
@@ -25,8 +34,9 @@ afterEach(async () => {
describe("Hopper target leases", () => {
it("reports the owning REA session for a duplicate target and profile", async () => {
const directory = await temporaryDirectory();
const targetPath = await temporaryTarget();
const first = await acquireHopperTargetLease({
...leaseInput,
...leaseInput(targetPath),
directory,
runId: "session-first",
});
@@ -34,7 +44,7 @@ describe("Hopper target leases", () => {
if (!first.acquired) return;
const duplicate = await acquireHopperTargetLease({
...leaseInput,
...leaseInput(targetPath),
directory,
runId: "session-second",
});
@@ -45,7 +55,7 @@ describe("Hopper target leases", () => {
await first.lease.release();
const reopened = await acquireHopperTargetLease({
...leaseInput,
...leaseInput(targetPath),
directory,
runId: "session-third",
});
@@ -55,13 +65,14 @@ describe("Hopper target leases", () => {
it("keeps different Hopper profiles independent", async () => {
const directory = await temporaryDirectory();
const targetPath = await temporaryTarget();
const first = await acquireHopperTargetLease({
...leaseInput,
...leaseInput(targetPath),
directory,
runId: "session-one",
});
const second = await acquireHopperTargetLease({
...leaseInput,
...leaseInput(targetPath),
directory,
loaderArgs: ["--aarch64"],
runId: "session-two",
@@ -71,6 +82,63 @@ describe("Hopper target leases", () => {
if (first.acquired) await first.lease.release();
if (second.acquired) await second.lease.release();
});
it.skipIf(process.platform === "win32")(
"shares one lease for symlink aliases of the same target",
async () => {
const directory = await temporaryDirectory();
const targetPath = await temporaryTarget();
const alias = join(dirname(targetPath), "sample-alias");
await symlink(targetPath, alias);
const first = await acquireHopperTargetLease({
...leaseInput(targetPath),
directory,
runId: "canonical-owner",
});
expect(first.acquired).toBe(true);
if (!first.acquired) return;
try {
expect(
await acquireHopperTargetLease({
...leaseInput(alias),
directory,
runId: "alias-request",
}),
).toMatchObject({
acquired: false,
owner: { runId: "canonical-owner" },
});
} finally {
await first.lease.release();
}
},
);
it.each(["missing", "not-directory"] as const)(
"does not create a lease endpoint when target canonicalization fails for %s",
async (failure) => {
const directory = await temporaryDirectory();
const parent = await temporaryDirectory();
const targetPath =
failure === "missing"
? join(parent, "missing-target")
: join(await temporaryTarget(parent), "child");
await expect(
acquireHopperTargetLease({
...leaseInput(targetPath),
directory,
runId: "unresolvable-target",
}),
).rejects.toMatchObject({
_tag: "HopperStartError",
userMessage: expect.stringContaining("canonical path"),
cause: expect.objectContaining({
code: failure === "missing" ? "ENOENT" : "ENOTDIR",
}),
});
expect(await readdir(directory)).toEqual([]);
},
);
});
describe("Linux Hopper application leases", () => {
@@ -212,10 +280,17 @@ const releasedApplicationSocket = async (
return join(directory, name);
};
const leaseInput = {
targetPath: "/tmp/sample-binary",
const leaseInput = (targetPath: string) => ({
targetPath,
targetKind: "executable" as const,
loaderArgs: [],
});
const temporaryTarget = async (parent?: string): Promise<string> => {
const directory = parent ?? (await temporaryDirectory());
const targetPath = join(directory, "sample-binary");
await writeFile(targetPath, "admitted target bytes");
return targetPath;
};
const temporaryDirectory = async (): Promise<string> => {
+12 -11
View File
@@ -6,7 +6,7 @@ import {
type Server,
type Socket,
} from "node:net";
import { join, resolve } from "node:path";
import { join } from "node:path";
import { HopperStartError } from "../domain/hopperErrors.js";
import { safeParseJson } from "../domain/safeJson.js";
@@ -37,16 +37,17 @@ export const acquireHopperTargetLease = async (input: {
readonly runId: string;
readonly directory?: string;
}): Promise<HopperTargetLeaseAcquisition> => {
// The canonical path only derives the lease key; it never authorizes access.
// Common expected `realpath` failures include a missing target or path
// component (ENOENT, ENOTDIR), an unreadable component (EACCES), or a
// symlink loop (ELOOP). This catch also falls back for every other error,
// which may hide a real filesystem problem. The lexical `resolve` fallback
// still yields a deterministic key, but aliases may take separate leases
// whenever `realpath` cannot resolve them.
const targetPath = await realpath(input.targetPath).catch(() =>
resolve(input.targetPath),
);
// Canonicalization is required for lease identity. A lexical fallback could
// let two aliases reserve independent sockets for the same Hopper document.
let targetPath: string;
try {
targetPath = await realpath(input.targetPath);
} catch (cause: unknown) {
throw new HopperStartError({
cause,
userMessage: `REA could not resolve Hopper target ${JSON.stringify(input.targetPath)} to a canonical path before reserving the document. Confirm that the target exists and its path is accessible.`,
});
}
return acquireLease({
...input,
identity: {
@@ -19,6 +19,7 @@ import {
readSkillFile,
skillDestinations,
} from "../../../src/application/SetupSkill.js";
import { RegularFileCleanupFailure } from "../../../src/application/RegularFileRead.js";
import { systemSetupHost } from "../../../src/application/SetupHost.js";
import {
runUninstall,
@@ -67,7 +68,7 @@ const failFirstCloses = (path: string) => {
};
describe("skill regular-file cleanup ownership", () => {
it("keeps setup skill reads owned until setup host disposal", async () => {
it("retains a failed setup skill read until setup host disposal", async () => {
const home = await createTestTempDirectory("rea-setup-skill-close-");
const [destination] = skillDestinations(home, []);
if (destination === undefined) throw new Error("Shared skill root missing");
@@ -81,7 +82,18 @@ describe("skill regular-file cleanup ownership", () => {
);
try {
expect(await host.skillNeedsInstall([])).toBe(true);
const failure = await host.skillNeedsInstall([]).then(
() => undefined,
(cause: unknown) => cause,
);
expect(failure).toBeInstanceOf(RegularFileCleanupFailure);
expect(failure).toMatchObject({
path: manifest,
outcome: { kind: "completed" },
cleanupCause: expect.objectContaining({
message: "test holds the skill handle open",
}),
});
expect(close.attempts.length).toBeGreaterThan(0);
expect(new Set(close.attempts).size).toBe(1);
const attemptsBeforeClose = close.attempts.length;
@@ -147,7 +159,7 @@ describe("skill regular-file cleanup ownership", () => {
});
describe("skill cleanup diagnostics", () => {
it("preserves a doctor report and cleanup diagnostic when host close still fails", async () => {
it("preserves read and host cleanup failures when doctor identity cannot complete", async () => {
const home = await createTestTempDirectory("rea-doctor-skill-close-");
const [destination] = skillDestinations(home, []);
if (destination === undefined) throw new Error("Shared skill root missing");
@@ -172,15 +184,23 @@ describe("skill cleanup diagnostics", () => {
});
try {
const report = await runDoctorOwned(undefined, host, { skill: true });
expect(report.healthy).toBe(false);
expect(report.identity?.skill.installed_version).toBeNull();
expect(report.checks).toContainEqual(
const failure = await runDoctorOwned(undefined, host, {
skill: true,
}).then(
() => undefined,
(cause: unknown) => cause,
);
expect(failure).toBeInstanceOf(AggregateError);
if (!(failure instanceof AggregateError))
throw new Error("Doctor cleanup should preserve both failures");
expect(failure.errors[0]).toBeInstanceOf(RegularFileCleanupFailure);
expect(failure.errors[0]).toMatchObject({
path: manifest,
outcome: { kind: "completed" },
});
expect(failure.errors[1]).toEqual(
expect.objectContaining({
name: "doctor-resource-cleanup",
ok: false,
detail: expect.stringContaining(manifest),
message: expect.stringContaining(manifest),
}),
);
expect(close.attempts.length).toBeGreaterThan(0);
@@ -1,9 +1,11 @@
import { readFile, writeFile } from "node:fs/promises";
import { truncate, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { ArtifactResourceScope } from "../../../../src/artifacts/ArtifactResourceScope.js";
import { OwnedFileHandle } from "../../../../src/filesystem/OwnedFileHandle.js";
import {
compareManagedMembersEvidenceValidated,
@@ -19,6 +21,11 @@ import { compareManagedMembersInputSchema } from "../../../../src/domain/managed
import { inspectManagedMembersBytes } from "../../../../src/dotnet/ManagedMemberInspector.js";
import { managedPeFixtureTarget } from "../../../../src/dotnet/ManagedPe.fixture.js";
import { buildManagedPeFixture } from "../../../../src/dotnet/ManagedPe.fixture.js";
import {
MAX_MANAGED_SNAPSHOT_BYTES,
readManagedSnapshot,
} from "../../../../src/dotnet/ManagedSnapshot.js";
import * as boundedFileBytes from "../../../../src/process/BoundedFileBytes.js";
it.each([
["accessibility", 6, 0x0011],
@@ -184,6 +191,14 @@ describe("managed member comparison path workflow", () => {
confidence: "inferred",
authority: "analyst-inference",
});
expect(result.value.parameters).toMatchObject({
left_path: leftPath,
right_path: rightPath,
});
expect(result.value.locations).toEqual([
{ kind: "artifact-path", path: leftPath },
{ kind: "artifact-path", path: rightPath },
]);
expect(
managedMemberComparisonResultSchema.parse(result.value.normalized_result)
.matching.exact_il_signature,
@@ -205,7 +220,179 @@ describe("managed member comparison path workflow", () => {
details: { path: expect.stringContaining("missing.dll") },
});
});
});
describe("managed comparison post-admission read failures", () => {
it.each([
["left", "disappearance", "ENOENT"],
["left", "permission denial", "EACCES"],
["right", "disappearance", "ENOENT"],
["right", "permission denial", "EACCES"],
])(
"classifies a %s-side %s during the post-resolution read",
async (side, _name, code) => {
const directory = await createTestTempDirectory(
"rea-managed-read-failure-",
);
const leftPath = join(directory, "left.dll");
const rightPath = join(directory, "right.dll");
await writeFile(leftPath, buildManagedPeFixture());
await writeFile(rightPath, buildManagedPeFixture());
const result = await compareManagedMemberPaths(
{ leftPath, rightPath },
{
resolveTarget: parseBinaryTarget,
readSnapshot: async (path, operation, options) => {
if (path === (side === "left" ? leftPath : rightPath)) {
const failure = Object.assign(new Error(`read failed: ${code}`), {
code,
});
throw failure;
}
return readManagedSnapshot(path, operation, options);
},
},
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error._tag).toBe("BinaryTargetError");
expect(projectAnalysisError(result.error)).toMatchObject({
details: { path: side === "left" ? leftPath : rightPath },
});
if (side === "right")
expect(result.error.partialObservation).toMatchObject({
operation: "inspect_managed_members",
subject: {
local_path: leftPath,
digest: { sha256: expect.stringMatching(/^[0-9a-f]{64}$/u) },
},
});
else expect(result.error.partialObservation).toBeUndefined();
if (code === "EACCES")
expect(projectAnalysisError(result.error)).toMatchObject({
code: "access_denied",
});
},
);
});
describe("managed comparison snapshot admission and cleanup", () => {
it("rejects a sparse oversized snapshot through the default reader before reading it", async () => {
const directory = await createTestTempDirectory(
"rea-managed-oversized-snapshot-",
);
const leftPath = join(directory, "oversized.dll");
const rightPath = join(directory, "right.dll");
const fixture = buildManagedPeFixture();
await writeFile(leftPath, Buffer.alloc(0));
await truncate(leftPath, MAX_MANAGED_SNAPSHOT_BYTES + 1);
await writeFile(rightPath, fixture);
const template = managedPeFixtureTarget(fixture);
const read = vi.spyOn(boundedFileBytes, "readBoundedFileBytes");
try {
const result = await compareManagedMemberPaths(
{ leftPath, rightPath },
{
resolveTarget: async (path) => ({
ok: true,
value: { ...template, path },
}),
},
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error).toMatchObject({
_tag: "AnalysisResourceConstraintError",
operation: "compare_managed_members",
reportedLimits: {
max_snapshot_bytes: MAX_MANAGED_SNAPSHOT_BYTES,
},
});
expect(read).not.toHaveBeenCalled();
} finally {
read.mockRestore();
}
});
it("retries a transient snapshot close failure through the same resource owner", async () => {
const directory = await createTestTempDirectory(
"rea-managed-compare-close-",
);
const leftPath = join(directory, "left.dll");
const rightPath = join(directory, "right.dll");
await writeFile(leftPath, buildManagedPeFixture());
await writeFile(rightPath, buildManagedPeFixture());
const resources = new ArtifactResourceScope();
const originalClose = OwnedFileHandle.prototype.close;
const owners: OwnedFileHandle[] = [];
let attempts = 0;
const close = vi
.spyOn(OwnedFileHandle.prototype, "close")
.mockImplementation(async function (this: OwnedFileHandle) {
owners.push(this);
attempts += 1;
if (attempts === 1) throw new Error("injected transient close failure");
await originalClose.call(this);
});
try {
const result = await compareManagedMemberPaths(
{ leftPath, rightPath },
{},
resources,
);
expect(result.ok).toBe(true);
expect(owners[0]).toBe(owners[1]);
expect(attempts).toBe(3);
} finally {
close.mockRestore();
await resources.close();
}
});
it("projects persistent snapshot cleanup failure and retains its exact owner", async () => {
const directory = await createTestTempDirectory(
"rea-managed-compare-cleanup-",
);
const leftPath = join(directory, "left.dll");
const rightPath = join(directory, "right.dll");
await writeFile(leftPath, buildManagedPeFixture());
await writeFile(rightPath, buildManagedPeFixture());
const resources = new ArtifactResourceScope();
const cleanupCause = new Error("injected persistent close failure");
const owners: OwnedFileHandle[] = [];
const close = vi
.spyOn(OwnedFileHandle.prototype, "close")
.mockImplementation(async function (this: OwnedFileHandle) {
owners.push(this);
throw cleanupCause;
});
let result: Awaited<ReturnType<typeof compareManagedMemberPaths>>;
try {
result = await compareManagedMemberPaths(
{ leftPath, rightPath },
{},
resources,
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(projectAnalysisError(result.error)).toMatchObject({
code: "cleanup_incomplete",
details: { cleanup: "incomplete", resources: [leftPath] },
});
expect(owners[0]).toBe(owners[1]);
expect(owners[0]?.handle.fd).toBeGreaterThanOrEqual(0);
} finally {
close.mockRestore();
await resources.close();
}
});
});
describe("managed comparison target identity", () => {
it("rejects a file that changes after target identity is resolved", async () => {
const directory = await createTestTempDirectory(
"rea-managed-compare-race-",
@@ -227,7 +414,6 @@ describe("managed member comparison path workflow", () => {
await writeFile(path, replacement);
return target;
},
readBytes: (path) => readFile(path),
},
);
@@ -1,8 +1,8 @@
import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { truncate, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
@@ -15,6 +15,180 @@ import {
} from "../../../../src/domain/managed/managedArtifact.js";
import { ManagedStaticProvider } from "../../../../src/dotnet/ManagedStaticProvider.js";
import { buildManagedPeFixture } from "../../../../src/dotnet/ManagedPe.fixture.js";
import {
MAX_MANAGED_SNAPSHOT_BYTES,
readManagedSnapshot,
} from "../../../../src/dotnet/ManagedSnapshot.js";
import { OwnedFileHandle } from "../../../../src/filesystem/OwnedFileHandle.js";
import { ArtifactResourceScope } from "../../../../src/artifacts/ArtifactResourceScope.js";
import { RegularFileCleanupFailure } from "../../../../src/application/RegularFileRead.js";
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
import * as boundedFileBytes from "../../../../src/process/BoundedFileBytes.js";
describe("managed static snapshot capacity failure", () => {
it("preserves a typed capacity failure when closing the admitted file also fails", async () => {
const directory = await createTestTempDirectory(
"rea-managed-capacity-cleanup-",
);
const path = join(directory, "sparse.exe");
await writeFile(path, Buffer.alloc(0));
await truncate(path, MAX_MANAGED_SNAPSHOT_BYTES + 1);
const resources = new ArtifactResourceScope();
const originalClose = OwnedFileHandle.prototype.close;
let permitClose = false;
const owners: OwnedFileHandle[] = [];
const close = vi
.spyOn(OwnedFileHandle.prototype, "close")
.mockImplementation(async function (this: OwnedFileHandle) {
owners.push(this);
if (!permitClose) throw new Error("fixture handle close failure");
await originalClose.call(this);
});
try {
const failure = await readManagedSnapshot(
path,
"inspect_managed_artifact",
{
resources,
},
).catch((cause: unknown) => cause);
expect(failure).toMatchObject({
cleanupIncomplete: true,
cleanupResources: [path],
diagnostics: { primary_error: { code: "resource_constraint" } },
});
expect(owners.length).toBeGreaterThan(0);
expect(owners.every((owner) => owner === owners[0])).toBe(true);
permitClose = true;
await resources.close();
} finally {
permitClose = true;
await resources.close();
close.mockRestore();
}
});
});
describe("managed static snapshot cancellation", () => {
it("preserves cancellation when snapshot cleanup also fails", async () => {
const directory = await createTestTempDirectory(
"rea-managed-cancel-cleanup-",
);
const path = join(directory, "fixture.exe");
await writeFile(path, buildManagedPeFixture());
const parsed = await parseBinaryTarget(path);
if (!parsed.ok) throw parsed.error;
const client = new ManagedStaticProvider().createClient(parsed.value);
const controller = new AbortController();
const originalRead = boundedFileBytes.readBoundedFileBytes;
const read = vi
.spyOn(boundedFileBytes, "readBoundedFileBytes")
.mockImplementation(async (...arguments_) => {
controller.abort(new Error("fixture cancellation"));
return originalRead(...arguments_);
});
const originalClose = OwnedFileHandle.prototype.close;
let permitClose = false;
const owners: OwnedFileHandle[] = [];
const close = vi
.spyOn(OwnedFileHandle.prototype, "close")
.mockImplementation(async function (this: OwnedFileHandle) {
owners.push(this);
if (!permitClose) throw new Error("fixture handle close failure");
await originalClose.call(this);
});
try {
const result = await client.execute(
"inspect_managed_artifact",
{},
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(projectAnalysisError(result.error)).toMatchObject({
code: "cleanup_incomplete",
details: {
diagnostics: {
primary_error: { code: "cancelled" },
},
},
});
expect(result.error.cause).toBeInstanceOf(AggregateError);
if (!(result.error.cause instanceof AggregateError)) return;
const cleanupError = result.error.cause.errors[1];
expect(cleanupError).toBeInstanceOf(Error);
if (!(cleanupError instanceof Error)) return;
expect(cleanupError.cause).toBeInstanceOf(RegularFileCleanupFailure);
if (!(cleanupError.cause instanceof RegularFileCleanupFailure)) return;
expect(cleanupError.cause.owner).toBe(owners[0]);
expect(owners[0]?.handle.fd).toBeGreaterThanOrEqual(0);
permitClose = true;
expect(await client.close()).toMatchObject({ ok: true });
expect(owners.every((owner) => owner === owners[0])).toBe(true);
} finally {
permitClose = true;
await client.close();
close.mockRestore();
read.mockRestore();
}
});
});
describe("managed static file ownership", () => {
it("retains the same failed file owner, blocks reads, and retries it on client close", async () => {
const directory = await createTestTempDirectory("rea-managed-close-");
const bytes = buildManagedPeFixture();
const path = join(directory, "fixture.exe");
await writeFile(path, bytes);
const parsed = await parseBinaryTarget(path);
if (!parsed.ok) throw parsed.error;
const client = new ManagedStaticProvider().createClient(parsed.value);
const originalClose = OwnedFileHandle.prototype.close;
const owners: OwnedFileHandle[] = [];
let permitClose = false;
const close = vi
.spyOn(OwnedFileHandle.prototype, "close")
.mockImplementation(async function (this: OwnedFileHandle) {
owners.push(this);
if (!permitClose) throw new Error("fixture handle close failure");
await originalClose.call(this);
});
try {
const failed = await client.execute("inspect_managed_artifact", {});
expect(failed).toMatchObject({
ok: false,
error: {
cleanupIncomplete: true,
cleanupResources: [path],
},
});
const blocked = await client.execute("inspect_managed_artifact", {});
expect(blocked).toMatchObject({
ok: false,
error: {
cleanupIncomplete: true,
cleanupResources: [path],
},
});
expect(owners).toHaveLength(3);
expect(owners.every((owner) => owner === owners[0])).toBe(true);
permitClose = true;
expect(await client.close()).toMatchObject({ ok: true });
expect(owners).toHaveLength(4);
expect(owners.every((owner) => owner === owners[0])).toBe(true);
} finally {
permitClose = true;
await client.close();
close.mockRestore();
}
});
});
describe("managed static provider path boundary", () => {
it("executes as a read-only auxiliary provider with digest, cancellation, and format boundaries", async () => {
@@ -219,7 +219,11 @@ describe("managed one-shot cleanup outcomes", () => {
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.cause).toBe(primaryError);
expect(result.error.cause).toBeInstanceOf(AggregateError);
expect(result.error.cause).toMatchObject({
cause: primaryError,
errors: [primaryError, cleanupError],
});
expect(projectAnalysisError(result.error)).toMatchObject({
code: "cleanup_incomplete",
details: {