Files
rea/tests/boundary/filesystem/appleDispatchMetadataInputBudget.test.ts
T
Kaoru0822 3637205622 feat(native): let callers raise the Apple dispatch metadata input budget (#6233)
* feat(native): let callers raise the Apple dispatch metadata input budget

inspect_native_dispatch_metadata refused any Mach-O over a fixed 64 MiB
before decoding, and its recovery said there was no override. Most large
macOS binaries exceed it.

max_input_bytes (CLI --max-input-bytes) now selects the budget, default
64 MiB and at most 1 GiB, through the shared input contract, the enhanced
workflow and the native provider. Results record input_admission with the
effective budget, its source and the retained bytes. A refusal names the
observed size, the budget and its source, and the budget to select or, past
1 GiB, a deep analysis provider.

Fixes #3917

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cli): describe --max-input-bytes in inspect-native-dispatch-metadata help

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(native): cover max_input_bytes forwarding through CLI and MCP

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:19:33 -07:00

140 lines
5.2 KiB
TypeScript

import { truncate, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
import { enhancedInputSchemas } from "../../../src/contracts/enhancedInputs.js";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import {
DEFAULT_DISPATCH_METADATA_INPUT_BYTES,
MAXIMUM_DISPATCH_METADATA_INPUT_BYTES,
} from "../../../src/domain/native/objcSwiftMetadata.js";
import {
appleDispatchMetadataFailure,
inspectAppleDispatchMetadata,
} from "../../../src/native/AppleDispatchMetadata.js";
import { withArtifactResourceScope } from "../../fixtures/artifactInventory.js";
import { machoWithPropertyAttributes } from "../../fixtures/objcPropertyMacho.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const fixtureTarget = async (bytes: Buffer, name = "fixture.bin") => {
const directory = await createTestTempDirectory("rea-dispatch-budget-");
const path = join(directory, name);
await writeFile(path, bytes);
const target = await parseBinaryTarget(path);
if (!target.ok) throw target.error;
return { path, target: target.value };
};
const inspect = (
target: Awaited<ReturnType<typeof fixtureTarget>>["target"],
maxInputBytes?: number,
) =>
withArtifactResourceScope((resources) =>
resources.run(() =>
inspectAppleDispatchMetadata(target, 100, resources, { maxInputBytes }),
),
);
const refusal = async (
path: string,
run: () => Promise<unknown>,
): Promise<ReturnType<typeof projectAnalysisError>> => {
const failure = await run().then(
() => undefined,
(cause: unknown) => cause,
);
if (failure === undefined) throw new Error("Expected a budget refusal");
return projectAnalysisError(appleDispatchMetadataFailure(failure, path));
};
it("records the default budget and the caller-selected budget it read under", async () => {
const bytes = machoWithPropertyAttributes("Tq,N");
const { target } = await fixtureTarget(bytes);
await expect(inspect(target)).resolves.toMatchObject({
input_admission: {
maximum_bytes: DEFAULT_DISPATCH_METADATA_INPUT_BYTES,
source: "default",
retained_bytes: bytes.length,
},
});
const exact = await inspect(target, bytes.length);
expect(exact.input_admission).toEqual({
maximum_bytes: bytes.length,
source: "caller",
retained_bytes: bytes.length,
});
// The budget changes admission only, never the decoded metadata.
expect(exact.result).toEqual((await inspect(target)).result);
});
it("names the observed size, effective budget, its source and the budget to select", async () => {
const bytes = machoWithPropertyAttributes("Tq,N");
const { path, target } = await fixtureTarget(bytes);
const projected = await refusal(path, () =>
inspect(target, bytes.length - 1),
);
expect(projected).toMatchObject({
code: "resource_constraint",
retryable: false,
message: `Apple static metadata input (${String(bytes.length)} bytes) exceeds the selected max_input_bytes of ${String(bytes.length - 1)} bytes.`,
remediation: {
action: `Retry with max_input_bytes (CLI --max-input-bytes) of at least ${String(bytes.length)}, at most 1073741824 bytes (1 GiB); REA holds that many bytes in memory for the read.`,
},
details: {
reported_limits: {
input_bytes: bytes.length - 1,
input_bytes_source: "caller",
maximum_selectable_input_bytes: MAXIMUM_DISPATCH_METADATA_INPUT_BYTES,
observed_input_bytes: bytes.length,
},
},
});
expect(JSON.stringify(projected)).not.toContain("no runtime override");
});
it("refuses files above the default without opt-in and points past the maximum to a deep provider", async () => {
// Sparse files: admission is decided from the stat size before any read.
const bytes = machoWithPropertyAttributes("Tq,N");
for (const [size, source, action] of [
[
DEFAULT_DISPATCH_METADATA_INPUT_BYTES + 1,
"default",
`of at least ${String(DEFAULT_DISPATCH_METADATA_INPUT_BYTES + 1)}`,
],
[
MAXIMUM_DISPATCH_METADATA_INPUT_BYTES + 1,
"caller",
"use a deep analysis provider",
],
] as const) {
const { path, target } = await fixtureTarget(bytes, `large-${source}.bin`);
await truncate(path, size);
const projected = await refusal(path, () =>
inspect(
target,
source === "caller" ? MAXIMUM_DISPATCH_METADATA_INPUT_BYTES : undefined,
),
);
expect(projected.details).toMatchObject({
reported_limits: {
input_bytes_source: source,
observed_input_bytes: size,
},
});
expect(projected.remediation.action).toContain(action);
}
});
it("validates max_input_bytes at the shared input contract", () => {
const schema = enhancedInputSchemas.inspect_native_dispatch_metadata;
expect(schema.parse({}).max_input_bytes).toBeUndefined();
expect(
schema.parse({ max_input_bytes: MAXIMUM_DISPATCH_METADATA_INPUT_BYTES })
.max_input_bytes,
).toBe(MAXIMUM_DISPATCH_METADATA_INPUT_BYTES);
for (const value of [0, -1, 1.5, MAXIMUM_DISPATCH_METADATA_INPUT_BYTES + 1])
expect(schema.safeParse({ max_input_bytes: value }).success).toBe(false);
});