test: focus diagnostics and evidence checks on behavior (#9303)

* test: focus configuration diagnostics on their owners

Preserve shape errors, selected paths, recovery guidance, and unchanged-file behavior across parser, setup, and uninstall boundaries.

* test: focus artifact and evidence checks on behavior

Retain format, identity, malformed-input, and cleanup coverage while removing fixed prose and closing the admitted test source.

* refactor(debugger): use configured session admission limits

* test: focus evidence and diagnostics on behavior

* fix(test): restore verifier export and formatting

* test(config): verify debugger limit ranges

* test(rizin): keep fixture executable authoritative

* test(rizin): isolate debugger fixture environment
This commit is contained in:
morluto authored and GitHub committed 2026-11-08 16:39:20 +00:00
1 parent 08637b0c24
commit 5044e20171
290 files changed
+2808 -2030

No files matched your search

-2
View File
@@ -98,8 +98,6 @@ const PATH_SCOPES = {
"tests/boundary/providers/GdbCommandQueue.test.ts",
"tests/boundary/providers/gdbTestHelpers.ts",
"tests/boundary/providers/RizinDebugSessionCoverage.part*.test.ts",
"tests/boundary/providers/RizinCommandTimeout.test.ts",
"tests/boundary/providers/RizinTimeoutEvidence.test.ts",
"tests/boundary/providers/RizinDebugSession*.test.ts",
"tests/boundary/providers/RizinInferiorTty.test.ts",
"tests/boundary/providers/RizinDecoderInputs.test.ts",
+2 -2
View File
@@ -32,9 +32,9 @@ const VERSION_TIMEOUT_MS = 10_000;
const ADB_VERSION_REMEDIATION = `Set ${ADB_BINARY_ENV} to an Android SDK Platform-Tools adb binary whose \`adb version\` reports its version, or put one on PATH.`;
/** Repair when the selected adb binary does not exist. */
export const ADB_MISSING_REMEDIATION = `Set ${ADB_BINARY_ENV} to an absolute path to an existing adb binary.`;
const ADB_MISSING_REMEDIATION = `Set ${ADB_BINARY_ENV} to an absolute path to an existing adb binary.`;
/** Repair when execution of the adb binary selected by the environment is denied. */
export const ADB_NOT_EXECUTABLE_REMEDIATION = `Set ${ADB_BINARY_ENV} to an absolute path to the adb executable file, not a directory, and check execute permission on it and search permission on its parent directories.`;
const ADB_NOT_EXECUTABLE_REMEDIATION = `Set ${ADB_BINARY_ENV} to an absolute path to the adb executable file, not a directory, and check execute permission on it and search permission on its parent directories.`;
const VERSION_MAX_OUTPUT_BYTES = 64 * 1024;
/** How the adb binary was selected for this provider instance. */
+7 -5
View File
@@ -31,9 +31,11 @@ const provider = () =>
environment: { REA_APKTOOL_COMMAND: "/authored/apktool" },
});
const version = {
stdout: { text: "2.9.3\n" },
stderr: { text: "" },
} as Awaited<ReturnType<typeof runOwnedCommand>>;
stdout: { text: "2.9.3\n", bytes: 6, observedBytes: 6 },
stderr: { text: "", bytes: 0, observedBytes: 0 },
exitCode: 0,
signal: null,
};
const inspect = (client: ApktoolProvider, signal?: AbortSignal) =>
client.execute(
{ operation: "inspect_apktool_client", input: {} },
@@ -255,8 +257,8 @@ it("attributes prerequisite cleanup to decode and preserves its live recovery",
try {
const result = await client.execute({
operation: "decode_android_resources",
input: {},
} as never);
input: { path: "/fixture/input.apk", include_strings: false },
});
if (result.ok) throw new Error("Expected prerequisite failure");
expect(result.error).toMatchObject({
operation: "decode_android_resources",
+2 -2
View File
@@ -48,9 +48,9 @@ const apktoolVersionProbeTimeoutGuidance = (): {
const APKTOOL_VERSION_REMEDIATION = `Set ${APKTOOL_COMMAND_ENV} to an Apktool 2.7.0 or newer launcher whose --version prints its version, or put one on PATH.`;
/** Repair when the selected apktool launcher does not exist. */
export const APKTOOL_MISSING_REMEDIATION = `Set ${APKTOOL_COMMAND_ENV} to an absolute apktool launcher (the upstream wrapper or a java -jar script).`;
const APKTOOL_MISSING_REMEDIATION = `Set ${APKTOOL_COMMAND_ENV} to an absolute apktool launcher (the upstream wrapper or a java -jar script).`;
/** Repair when execution of the selected apktool launcher is denied. */
export const APKTOOL_NOT_EXECUTABLE_REMEDIATION = `Check file type and execute permission for ${APKTOOL_COMMAND_ENV} or select an executable apktool launcher.`;
const APKTOOL_NOT_EXECUTABLE_REMEDIATION = `Check file type and execute permission for ${APKTOOL_COMMAND_ENV} or select an executable apktool launcher.`;
/** How the apktool launcher was selected for this provider instance. */
export interface ApktoolCommandSelection {
+9 -5
View File
@@ -49,11 +49,15 @@ afterEach(() => {
vi.mocked(runOwnedCommand).mockReset();
});
const completed = (text: string): ProviderProcessSnapshot =>
({
stdout: { text },
stderr: { text: "" },
}) as ProviderProcessSnapshot;
const completed = (text: string): ProviderProcessSnapshot => {
const bytes = Buffer.byteLength(text);
return {
stdout: { text, bytes, observedBytes: bytes },
stderr: { text: "", bytes: 0, observedBytes: 0 },
exitCode: 0,
signal: null,
};
};
const installLauncher = (): void => {
vi.mocked(runOwnedCommand).mockImplementation(async (spawn, limits) => {
+10 -9
View File
@@ -1555,12 +1555,12 @@ it("discloses qualified and unfamiliar strings tables while preserving exact loc
locale: locale ?? null,
coverage: "complete",
});
expect(result.value.limitations).toContain(
`Other strings tables are not selected by locale; their resource configuration suffixes are preserved without inferring Android runtime matching: ${tables
.map((suffix) => JSON.stringify(`values-${suffix}`))
.sort()
.join(", ")}`,
const otherTables = result.value.limitations.find((line) =>
line.includes("values-unknown_CONFIGURATION"),
);
expect(otherTables).toMatch(/without inferring.*runtime matching/iu);
for (const suffix of tables)
expect(otherTables).toContain(JSON.stringify(`values-${suffix}`));
if (locale === undefined)
expect(result.value.result).toMatchObject({
strings: [
@@ -1570,8 +1570,8 @@ it("discloses qualified and unfamiliar strings tables while preserving exact loc
});
else if (locale === "en-rUS") {
expect(result.value.result).toMatchObject({ strings: [] });
expect(result.value.limitations).toContain(
"No strings.xml was found for locale en-rUS",
expect(result.value.limitations).toContainEqual(
expect.stringMatching(/strings\.xml.*locale en-rUS/iu),
);
} else
expect(result.value.result).toMatchObject({
@@ -1691,8 +1691,9 @@ it.each([
});
const limitations = result.value.limitations.join("\n");
if (kind === "unsupported") {
expect(limitations).toContain(
"String entries omitted due to unsupported markup or missing names: 2",
expect(limitations).toMatch(/omitted[^\n]*2/iu);
expect(limitations).toMatch(
/omitted.*unsupported markup.*missing names/iu,
);
expect(limitations).not.toContain("entry counts are unknown");
} else {
+12 -8
View File
@@ -36,9 +36,11 @@ const owner = {
it("runs the version probe as an owned interpreter-script process group", async () => {
vi.mocked(runOwnedCommand).mockImplementationOnce(async (_spawn, limits) => {
const output = {
stdout: { text: "2.9.3\n" },
stderr: { text: "" },
} as Awaited<ReturnType<typeof runOwnedCommand>>;
stdout: { text: "2.9.3\n", bytes: 6, observedBytes: 6 },
stderr: { text: "", bytes: 0, observedBytes: 0 },
exitCode: 0,
signal: null,
};
limits.observeCompleted?.(output);
return output;
});
@@ -152,8 +154,8 @@ it("keeps unresolved probe cleanup typed when the launcher overflows its output"
vi.mocked(runOwnedCommand).mockRejectedValueOnce(overflow());
const result = await provider.execute({
operation: "decode_android_resources",
input: {},
} as never);
input: { path: "/fixture/input.apk", include_strings: false },
});
expect(result.ok ? null : result.error.cleanupIncomplete).toBe(true);
expect(result.ok ? null : projectAnalysisError(result.error)).toMatchObject({
code: "cleanup_incomplete",
@@ -169,9 +171,11 @@ it.each(["linux", "win32"] as const)(
vi.mocked(runOwnedCommand).mockImplementationOnce(
async (_spawn, limits) => {
const output = {
stdout: { text: "2.9.2\n" },
stderr: { text: "" },
} as Awaited<ReturnType<typeof runOwnedCommand>>;
stdout: { text: "2.9.2\n", bytes: 6, observedBytes: 6 },
stderr: { text: "", bytes: 0, observedBytes: 0 },
exitCode: 0,
signal: null,
};
limits.observeCompleted?.(output);
return output;
},
@@ -3,7 +3,6 @@ import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { plistNestingLimitMessage } from "../domain/apple/plistValue.js";
import { resolveAppBundleExecutable } from "./AppBundleExecutable.js";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
@@ -34,7 +33,7 @@ describe("OpenStep Info.plist nesting during app target resolution", () => {
error: {
path: join(app, "Contents", "Info.plist"),
cause: expect.objectContaining({
message: plistNestingLimitMessage(),
message: expect.stringMatching(/128 nesting levels/iu),
}),
},
});
@@ -18,7 +18,7 @@ import {
expectKeptPartial,
expectOutputError,
expectOwnerError,
} from "./ownerObservationExpectation.fixture.js";
} from "../../tests/fixtures/ownerObservationExpectation.js";
import { BrowserObservationError } from "../domain/browserObservationError.js";
import { err, ok } from "../domain/result.js";
import { passiveNetworkCapture } from "../../tests/fixtures/passiveNetworkCapture.js";
@@ -20,7 +20,7 @@ import {
expectDroppedPartial,
expectKeptPartial,
expectOwnerError,
} from "./ownerObservationExpectation.fixture.js";
} from "../../tests/fixtures/ownerObservationExpectation.js";
const scenario = browserScenarioInputSchema.parse({
browser: { mode: "launch", executable_path: "/fixture/chrome" },
+11 -4
View File
@@ -698,8 +698,7 @@ describe("partial navigation availability", () => {
expect(availability).toMatchObject({
reason: "provider_missing",
remediation:
"Configure a provider for one of: current_document, current_address, resolve_containing_procedure.",
remediation: expect.stringMatching(/configure.*provider/i),
modes: [
{
name: "current_selection",
@@ -718,6 +717,13 @@ describe("partial navigation availability", () => {
},
],
});
for (const operation of [
"current_document",
"current_address",
"resolve_containing_procedure",
]) {
expect(availability.remediation?.split(operation)).toHaveLength(2);
}
});
});
@@ -820,8 +826,9 @@ describe("binary_overview capability requirements", () => {
).toMatchObject({
available: false,
reason: "provider_missing",
remediation:
"Configure a provider for one of: current_document, list_documents.",
remediation: expect.stringMatching(
/configure.*provider.*current_document.*list_documents/i,
),
});
});
@@ -1,34 +1,32 @@
import { describe, expect, it } from "vitest";
import {
ClientConfigurationShapeError,
parseClientConfiguration,
shadowedClientServers,
} from "./ClientConfigurationDocument.js";
describe("parseClientConfiguration shape diagnostics", () => {
it.each([
["null", "null"],
["array", "[]"],
["string", '"x"'],
] as const)("names a non-object JSON root (%s)", (_label, text) => {
expect(() => parseClientConfiguration(text, "json")).toThrow(
new TypeError("The configuration root must be an object"),
);
["null root", "null", "root"],
["array root", "[]", "root"],
["string root", '"x"', "root"],
["null server table", '{"mcpServers":null,"keep":1}', "mcpServers"],
["array server table", '{"mcpServers":[],"keep":1}', "mcpServers"],
["string server table", '{"mcpServers":"x","keep":1}', "mcpServers"],
])("reports a non-object %s", (_label, text, location) => {
let error: unknown;
try {
parseClientConfiguration(text, "json");
} catch (cause: unknown) {
error = cause;
}
expect(error).toBeInstanceOf(ClientConfigurationShapeError);
if (!(error instanceof Error)) throw new Error("Expected shape error");
expect(error.message).toContain(location);
expect(error.message).toContain("object");
});
it.each([
["null", "null"],
["array", "[]"],
["string", '"x"'],
] as const)(
'names a non-object "mcpServers" value (%s)',
(_label, servers) => {
expect(() =>
parseClientConfiguration(`{"mcpServers":${servers},"keep":1}`, "json"),
).toThrow(new TypeError('"mcpServers" must be an object'));
},
);
it("still rejects unparsable JSON with SyntaxError", () => {
expect(() => parseClientConfiguration("not-json", "json")).toThrow(
SyntaxError,
+13 -10
View File
@@ -255,8 +255,9 @@ describe("doctor native analysis configuration", () => {
expect.objectContaining({
name: "hopper-startup-timeout",
ok: false,
detail:
"REA_HOPPER_STARTUP_TIMEOUT_MS: must be a positive decimal integer",
detail: expect.stringMatching(
/REA_HOPPER_STARTUP_TIMEOUT_MS.*positive decimal integer/i,
),
}),
);
expect(result.scope_checks).toContainEqual(
@@ -650,8 +651,9 @@ describe("doctor", () => {
ok: false,
classification: "config_drift",
detail: "/missing/ilspycmd",
remediation:
"Unset REA_ILSPY_CMD_PATH or point it at a runnable ilspycmd executable.",
remediation: expect.stringMatching(
/REA_ILSPY_CMD_PATH.*runnable ilspycmd/i,
),
});
});
@@ -669,8 +671,9 @@ describe("doctor", () => {
ok: false,
classification: "config_drift",
detail: configuredPath,
remediation:
"Unset or update HOPPER_LAUNCHER_PATH to an executable Hopper launcher.",
remediation: expect.stringMatching(
/HOPPER_LAUNCHER_PATH.*executable Hopper launcher/i,
),
});
});
});
@@ -738,7 +741,7 @@ describe("doctor installation identity", () => {
skill: {
installed_version: "10",
state: "stale",
remediation: "Run rea setup to update the installed REA skill.",
remediation: expect.stringMatching(/rea setup.*update.*skill/i),
},
});
});
@@ -751,14 +754,14 @@ describe("doctor installation identity", () => {
expect(result.healthy).toBe(false);
expect(result.identity?.skill).toMatchObject({
state: "missing",
remediation: "Run rea setup to update the installed REA skill.",
remediation: expect.stringMatching(/rea setup.*update.*skill/i),
});
expect(result.checks).toContainEqual({
name: "skill:identity",
ok: false,
classification: "config_drift",
detail: "Installed REA skill identity is missing.",
remediation: "Run rea setup to update the installed REA skill.",
detail: expect.stringMatching(/skill identity.*missing/i),
remediation: result.identity?.skill.remediation,
});
});
it("classifies an unreadable installed skill as unknown, not drift", async () => {
+21 -22
View File
@@ -10,8 +10,6 @@ import {
} from "./HopperDownload.js";
import {
installMacHopper,
macHopperHostVersionDetail,
macHopperIncomparableMinimumDetail,
type MacHopperBundleVerification,
type MacHopperBundleInstallResult,
type MacHopperInstallHost,
@@ -309,17 +307,19 @@ describe("macOS Hopper release host requirements", () => {
host.feedBytes = macFeed(host, { deployment_target: "12.4" });
const result = await installMacHopper({}, host);
expect(result).toEqual({
expect(result).toMatchObject({
status: "failed",
reason: "incompatible_host_version",
diagnostic: macHopperHostVersionDetail({
source: "release_feed",
packageUrl: PACKAGE_URL,
minimumVersion: "12.4",
hostVersion,
}),
});
expectNamed(result, [PACKAGE_URL, "12.4", hostVersion]);
expectNamed(result, [
PACKAGE_URL,
"12.4",
hostVersion,
"deployment_target",
]);
if (result.status !== "failed")
throw new Error("Expected version refusal");
expect(result.diagnostic).toMatch(/not downloaded/i);
expect(host.packageDownloads).toBe(0);
expect(host.mounts).toBe(0);
expect(host.installs).toBe(0);
@@ -353,16 +353,14 @@ describe("macOS Hopper release host requirements", () => {
host.feedBytes = macFeed(host, fields);
const result = await installMacHopper({}, host);
expect(result).toEqual({
expect(result).toMatchObject({
status: "failed",
reason: "incompatible_host_version",
diagnostic: macHopperHostVersionDetail({
source: "signed_app",
minimumVersion: "12.4",
hostVersion: "12.0",
}),
});
expectNamed(result, ["LSMinimumSystemVersion", "12.4", "12.0"]);
if (result.status !== "failed")
throw new Error("Expected version refusal");
expect(result.diagnostic).toMatch(/nothing was copied/i);
expect(host.packageDownloads).toBe(1);
expect(host.installs).toBe(0);
expect(host.opens).toBe(0);
@@ -400,12 +398,13 @@ describe("macOS Hopper release host requirements", () => {
host.appMinimum = "12.x";
const result = await installMacHopper({}, host);
expect(result).toEqual({
status: "failed",
reason: "bundle",
diagnostic: macHopperIncomparableMinimumDetail("12.x", "14.5"),
});
expectNamed(result, ['"12.x"', "14.5"]);
expect(result).toMatchObject({ status: "failed", reason: "bundle" });
if (result.status !== "failed") throw new Error("Expected bundle failure");
expect(result.diagnostic).toContain("LSMinimumSystemVersion");
expect(result.diagnostic).toContain("12.x");
expect(result.diagnostic).toContain("14.5");
expect(result.diagnostic).toMatch(/cannot compare/i);
expect(result.diagnostic).toMatch(/nothing was copied/i);
expect(host.installs).toBe(0);
expect(host.unmounts).toBe(1);
expect(host.cleanups).toBe(1);
+2 -2
View File
@@ -576,7 +576,7 @@ export const systemMacHopperInstallHost = (
};
/** Installer detail naming the declared minimum this Mac's version does not meet. */
export const macHopperHostVersionDetail = (
const macHopperHostVersionDetail = (
refusal: (
| { readonly source: "release_feed"; readonly packageUrl: string }
| { readonly source: "signed_app" }
@@ -587,7 +587,7 @@ export const macHopperHostVersionDetail = (
: `The signed Hopper application declares LSMinimumSystemVersion ${refusal.minimumVersion}; this Mac runs macOS ${refusal.hostVersion}. Nothing was copied.`;
/** Installer detail for a signed-app minimum REA cannot compare with this Mac's version. */
export const macHopperIncomparableMinimumDetail = (
const macHopperIncomparableMinimumDetail = (
declared: string,
hostVersion: string,
): string =>
@@ -185,10 +185,20 @@ describe("semantic uncertainty at the obligation candidate boundary", () => {
expect(ledger.obligations).toHaveLength(1);
expect(ledger.reports.residual_unknown_ids).toEqual([]);
expect(ledger.coverage.status).toBe("partial");
expect(ledger.limitations).toEqual([
`Semantic graph ${source.graph.graph_id} coverage is unknown; omitted candidates remain unknown.`,
`Semantic graph ${source.graph.graph_id}: Event source was not fully examined.`,
]);
expect(ledger.limitations).toHaveLength(2);
expect(ledger.limitations).toContainEqual(
expect.stringMatching(
/coverage is unknown.*omitted candidates remain unknown/iu,
),
);
expect(
ledger.limitations.every((limitation) =>
limitation.includes(source.graph.graph_id),
),
).toBe(true);
expect(ledger.limitations).toContainEqual(
expect.stringContaining("Event source was not fully examined."),
);
});
it("keeps a complete semantic source complete without fabricating uncertainty", () => {
@@ -198,8 +198,11 @@ describe("reconstruction obligation candidates", () => {
const generated = deriveCandidates(createEvidenceBundle([evidence]), []);
expect(generated.candidates).toEqual([]);
expect(generated.limitations).toContain(
`Process capture ${evidence.evidence_id} has no validated executable artifact identity; packaged-process lifecycle obligation was not bound to an artifact.`,
const warning = generated.limitations.find((value) =>
value.includes(evidence.evidence_id),
);
expect(warning).toMatch(
/process capture.*no validated executable artifact identity.*lifecycle obligation.*not bound to an artifact/iu,
);
});
@@ -229,9 +232,11 @@ describe("reconstruction obligation candidates", () => {
});
const generated = deriveCandidates(createEvidenceBundle([evidence]), []);
expect(generated.limitations).toEqual([
`Process Evidence ${evidence.evidence_id} could not generate candidates: at manifest.completed_at: Invalid input: expected string, received undefined (1 more issues)`,
]);
expect(generated.limitations).toHaveLength(1);
expect(generated.limitations[0]).toContain(evidence.evidence_id);
expect(generated.limitations[0]).toMatch(
/Process Evidence.*could not generate candidates.*manifest\.completed_at/iu,
);
});
});
@@ -319,10 +324,22 @@ it("keeps each semantic candidate bound to its own file and authority context",
createEvidenceBundle([...sourceRecords, application]),
[],
);
expect(generated.limitations).toEqual([
`Semantic graph ${semanticGraph.graph_id} coverage is unknown; omitted candidates remain unknown.`,
`Semantic graph ${semanticGraph.graph_id}: Semantic relations are unavailable in this minimal example.`,
]);
expect(generated.limitations).toHaveLength(2);
expect(generated.limitations).toContainEqual(
expect.stringMatching(
/coverage is unknown.*omitted candidates remain unknown/iu,
),
);
expect(
generated.limitations.every((limitation) =>
limitation.includes(semanticGraph.graph_id),
),
).toBe(true);
expect(generated.limitations).toContainEqual(
expect.stringContaining(
"Semantic relations are unavailable in this minimal example.",
),
);
expect(generated.candidates).toHaveLength(requests.length);
for (const [index, node] of requests.entries()) {
const source = sourceRecords[Math.floor(index / 2)];
@@ -304,8 +304,10 @@ describe("reconstruction obligation manifest references", () => {
expect(result.status).toBe("unknown");
expect(result.coverage.status).toBe("partial");
expect(result.evidence_links).toContain(absent);
expect(result.limitations).toContain(
"The ledger retains 1 Evidence reference(s) absent from the input bundle; closure cannot be claimed.",
expect(result.limitations).toContainEqual(
expect.stringMatching(
/1 Evidence reference.*absent from the input bundle.*closure cannot be claimed/iu,
),
);
}
const contradiction = build({
@@ -92,9 +92,10 @@ it("keeps source entries when a module has an empty specifier", () => {
"other.txt",
]);
expect(parsed.relationships).toEqual([]);
expect(parsed.limitations).toEqual([
"Empty module specifier in main.mjs was not recorded as a relationship.",
]);
expect(parsed.limitations).toHaveLength(1);
expect(parsed.limitations[0]).toContain("main.mjs");
expect(parsed.limitations[0]).toMatch(/empty.*specifier/iu);
expect(parsed.limitations[0]).toMatch(/not recorded.*relationship/iu);
});
it("retains Kotlin source identity and language across the inventory projection", () => {
+20 -11
View File
@@ -5,20 +5,27 @@ import { setupInstallFailure } from "./SetupInstallFailure.js";
describe("macOS Hopper setup failure projection", () => {
it("projects a publisher mismatch onto its own code and recovery", () => {
expect(setupInstallFailure("publisher_verification_failed")).toEqual({
const result = setupInstallFailure("publisher_verification_failed");
expect(result).toMatchObject({
status: "failed",
code: "publisher_verification_failed",
remediation: `The mounted Hopper application is not signed by the publisher REA pins (${HOPPER_PUBLISHER.bundleIdentifier}, Apple team ${HOPPER_PUBLISHER.teamIdentifier}); nothing was copied or launched. Install Hopper yourself and set HOPPER_LAUNCHER_PATH to its launcher, then rerun rea setup.`,
});
for (const fact of [
HOPPER_PUBLISHER.bundleIdentifier,
HOPPER_PUBLISHER.teamIdentifier,
"HOPPER_LAUNCHER_PATH",
"rea setup",
])
expect(result.remediation).toContain(fact);
expect(result.remediation).toMatch(/nothing was copied or launched/i);
});
it("keeps a broken bundle distinct from a publisher mismatch", () => {
expect(setupInstallFailure("bundle")).toEqual({
status: "failed",
code: "bundle_invalid",
remediation:
"The mounted package did not contain the expected Hopper application bundle. Retry setup; if it repeats, update REA before installing.",
});
const result = setupInstallFailure("bundle");
expect(result).toMatchObject({ status: "failed", code: "bundle_invalid" });
expect(result.remediation).toMatch(/bundle/i);
expect(result.remediation).toMatch(/retry setup/i);
expect(result.remediation).toMatch(/update REA/i);
});
it("keeps a host below the release minimum distinct from an unsupported host", () => {
@@ -59,12 +66,14 @@ describe("Linux package-manager setup failure projection", () => {
});
it("preserves the established remediation if no package output exists", () => {
expect(setupInstallFailure("authorization_or_package_manager")).toEqual({
const result = setupInstallFailure("authorization_or_package_manager");
expect(result).toMatchObject({
status: "failed",
code: "authorization_or_package_manager_failed",
remediation:
"Allow the system package-manager operation or install the planned Hopper package and Linux runtime dependencies, then rerun rea setup.",
});
expect(result.remediation).toMatch(/package.manager/i);
expect(result.remediation).toMatch(/Linux runtime dependencies/i);
expect(result.remediation).toContain("rea setup");
});
it("keeps truncated package output within its excerpt budget", () => {
+63 -33
View File
@@ -1,13 +1,13 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { err, ok } from "../domain/result.js";
import { err, ok, type Result } from "../domain/result.js";
import { safeParseJson } from "../domain/safeJson.js";
import { PRODUCT_IDENTITY } from "../identity.js";
import {
npmLatestVersionCommand,
npmReleaseLookup,
parseNpmLatestVersion,
} from "./UpdateRuntime.js";
UpdateCommandRunner,
type UpdateCommandFailure,
} from "./UpdateCommandRunner.js";
import { systemUpdateHost } from "./UpdateRuntime.js";
const installation = {
manager: "npm",
@@ -15,14 +15,30 @@ const installation = {
packageRoot: "/opt/rea/lib/node_modules/rea-agents",
} as const;
describe("npm latest release metadata", () => {
it("accepts npm 11's scalar JSON response", () => {
expect(parseNpmLatestVersion('"6.0.0"')).toEqual(ok("6.0.0"));
});
const lookup = async (
response: Result<string, UpdateCommandFailure>,
selected: typeof installation | null = installation,
) => {
const probe = vi
.spyOn(UpdateCommandRunner.prototype, "runProbe")
.mockResolvedValue(response);
const host = systemUpdateHost();
try {
const result = await host.latestVersion(selected ?? undefined);
return { result, command: probe.mock.calls[0]?.[0] };
} finally {
await host.close?.();
probe.mockRestore();
}
};
it("accepts npm 12's singleton-array JSON response", () => {
expect(parseNpmLatestVersion('[\n "6.0.0"\n]\n')).toEqual(ok("6.0.0"));
});
describe("npm latest release metadata", () => {
it.each(['"6.0.0"', '[\n "6.0.0"\n]\n'])(
"accepts a scalar or singleton-array response: %j",
async (metadata) => {
expect((await lookup(ok(metadata))).result).toEqual(ok("6.0.0"));
},
);
it.each([
["empty array", "[]"],
@@ -31,39 +47,52 @@ describe("npm latest release metadata", () => {
["number", "6"],
["object", '{"version":"6.0.0"}'],
["null", "null"],
])("rejects %s response data", (_description, metadata) => {
expect(parseNpmLatestVersion(metadata)).toEqual(
err(
"Invalid npm release metadata: expected a version string or a single-element version array.",
),
])("rejects %s response data", async (_description, metadata) => {
const { result } = await lookup(ok(metadata));
expect(result).toMatchObject({
ok: false,
error: { kind: "invalid-metadata" },
});
if (result.ok || result.error.kind !== "invalid-metadata")
throw new Error("Expected invalid metadata");
expect(result.error.detail).toMatch(
/version string.*single-element version array/i,
);
});
it("preserves malformed JSON separately from a wrong metadata shape", () => {
it("preserves malformed JSON separately from a wrong metadata shape", async () => {
const metadata = '"6.0.0';
const decoded = safeParseJson(metadata);
expect(decoded.ok).toBe(false);
if (decoded.ok) return;
expect(parseNpmLatestVersion(metadata)).toEqual(err(decoded.error));
expect(decoded.error).not.toContain("single-element version array");
if (decoded.ok) throw new Error("Expected malformed JSON");
expect((await lookup(ok(metadata))).result).toEqual(
err({ kind: "invalid-metadata", detail: decoded.error }),
);
});
it("keeps command failures distinct from rejected metadata", () => {
expect(npmReleaseLookup(err("npm registry returned HTTP 503"))).toEqual(
it("keeps command failures distinct from rejected metadata", async () => {
expect(
(
await lookup(
err({ kind: "execution", detail: "npm registry returned HTTP 503" }),
)
).result,
).toEqual(
err({ kind: "unavailable", detail: "npm registry returned HTTP 503" }),
);
expect(npmReleaseLookup(ok("[]"))).toEqual(
expect(
(await lookup(err({ kind: "launch", detail: "npm missing" }))).result,
).toEqual(
err({
kind: "invalid-metadata",
detail:
"Invalid npm release metadata: expected a version string or a single-element version array.",
kind: "package-manager-unavailable",
manager: "npm",
detail: "npm missing",
}),
);
expect(npmReleaseLookup(ok('["6.0.0"]'))).toEqual(ok("6.0.0"));
});
it("keeps the global registry query and adds the owning prefix", () => {
expect(npmLatestVersionCommand(installation)).toEqual([
it("keeps the global registry query and adds the owning prefix", async () => {
const selected = await lookup(ok('"6.0.0"'));
expect(selected.command).toEqual([
"npm",
"view",
"--global",
@@ -75,7 +104,8 @@ describe("npm latest release metadata", () => {
"--fetch-retries=0",
"--fetch-timeout=10000",
]);
expect(npmLatestVersionCommand(undefined)).toEqual([
const unspecified = await lookup(ok('"6.0.0"'), null);
expect(unspecified.command).toEqual([
"npm",
"view",
"--global",
+3 -5
View File
@@ -153,7 +153,7 @@ export const BUN_LATEST_VERSION_COMMAND: readonly string[] = [
];
/** npm view arguments for dist-tags.latest, scoped to the owning prefix when known. */
export const npmLatestVersionCommand = (
const npmLatestVersionCommand = (
installation: NpmInstallation | undefined,
): readonly string[] => [
"npm",
@@ -171,9 +171,7 @@ export const npmLatestVersionCommand = (
* Read one latest version from `npm view --json`.
* npm 11 emits a JSON string. npm 12 emits that string as a one-element array.
*/
export const parseNpmLatestVersion = (
metadata: string,
): Result<string, string> => {
const parseNpmLatestVersion = (metadata: string): Result<string, string> => {
const decoded = safeParseJson(metadata);
if (!decoded.ok) return err(decoded.error);
const version = readNpmLatestVersion(decoded.value);
@@ -193,7 +191,7 @@ const nonemptyVersion = (value: string): string | undefined =>
value.length === 0 ? undefined : value;
/** Classify `npm view` or `bun info` output as a version or a lookup failure. */
export const npmReleaseLookup = (
const npmReleaseLookup = (
response: Result<string, string>,
): Result<string, ReleaseLookupFailure> => {
if (!response.ok) return err({ kind: "unavailable", detail: response.error });
@@ -142,6 +142,9 @@ it("projects the unknown-subject explanation before generating its Evidence ID",
});
if (!result.ok) throw result.error;
expect(result.value.subject).toBeNull();
expect(result.value.limitations).toContainEqual(
expect.stringMatching(/digest.*unknown.*Evidence/u),
);
expect(result.value.limitations.every((text) => !text.includes("a"))).toBe(
true,
);
+1 -1
View File
@@ -17,7 +17,7 @@ import {
expectDroppedPartial,
expectKeptPartial,
expectOwnerError,
} from "./ownerObservationExpectation.fixture.js";
} from "../../tests/fixtures/ownerObservationExpectation.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { parseEvidence } from "../domain/evidence.js";
@@ -106,8 +106,7 @@ it.each([
path: ["generated_position", "line"],
minimum: 1,
maximum: 2,
message:
"No line 3; the retained script ends at line 2 (one-based lines).",
message: expect.stringMatching(/line 3.*line 2.*one-based/i),
},
},
{
@@ -116,8 +115,9 @@ it.each([
path: ["generated_position", "column"],
minimum: 0,
maximum: 3,
message:
"No column 4 on line 1; it ends at column 3 (zero-based UTF-16 columns).",
message: expect.stringMatching(
/column 4.*line 1.*column 3.*zero-based UTF-16/i,
),
},
},
{
@@ -262,8 +262,10 @@ describe("browser scenario analysis view pages", () => {
},
};
const summary = await view(dropped, { kind: "summary" });
expect(summary.unknowns).toContain(
"3 events were dropped by capture budgets; counts and pages cover retained events only.",
expect(summary.unknowns).toContainEqual(
expect.stringMatching(
/3 events.*dropped.*counts and pages.*retained events only/iu,
),
);
expect(summary).toMatchObject({
summary: {
@@ -32,7 +32,6 @@ import { NATIVE_MACOS_PROVIDER_IDENTITY } from "../../native/NativeMacosProvider
import {
buildCapabilityInventory,
compactToolAvailability,
MULTI_MODE_REMEDIATION,
} from "../CapabilityInventory.js";
const projectedStatusSchema = z.object({
@@ -583,7 +582,9 @@ it.each([
: ["inspect_native_dispatch_metadata", "list_names"],
});
if (!route) {
expect(trace?.remediation).toBe(MULTI_MODE_REMEDIATION);
expect(trace?.remediation).toMatch(
/no mode.*inspect each mode.*missing operations and remediation/iu,
);
const authored = trace?.modes?.find(
(mode) => mode.name === "authored_ui_dispatch",
);
@@ -638,7 +639,9 @@ it("keeps distinct recovery for an unbound non-bundle session", async () => {
expect(trace).toMatchObject({
available: false,
reason: "provider_unavailable",
remediation: MULTI_MODE_REMEDIATION,
remediation: expect.stringMatching(
/no mode.*inspect each mode.*missing operations and remediation/iu,
),
});
const authored = trace?.modes?.find(
(mode) => mode.name === "authored_ui_dispatch",
@@ -296,9 +296,12 @@ describe("exportDesktopApplication and the desktop Trash", () => {
"Documents/today.txt",
],
});
expect(result.limitations).toContain(
"The desktop Trash held 2 file(s) at the seed checkpoint, which are not seeded: ~/.local/share/Trash/files/today.txt, ~/.local/share/Trash/info/today.txt.trashinfo. Restore one the task needs and capture again.",
const warning = result.limitations.find((line) =>
line.includes("Trash/files/today.txt"),
);
expect(warning).toContain("~/.local/share/Trash/info/today.txt.trashinfo");
expect(warning).toMatch(/Trash.*2 file.*not seeded/iu);
expect(warning).toMatch(/restore.*capture again/iu);
});
});
@@ -44,8 +44,10 @@ describe("environment manifests from capture_browser_scenario parents", () => {
expect(
manifest.tools.some((entry) => entry.name.includes("redacted")),
).toBe(false);
expect(manifest.limitations).toContain(
"Scenario records are the capture's network transactions in request order; each record ordinal is the transaction's zero-based position.",
expect(manifest.limitations).toContainEqual(
expect.stringMatching(
/network transactions in request order.*ordinal.*zero-based position/iu,
),
);
});
@@ -23,7 +23,7 @@ import {
expectDroppedPartial,
expectKeptPartial,
expectOwnerError,
} from "../ownerObservationExpectation.fixture.js";
} from "../../../tests/fixtures/ownerObservationExpectation.js";
const identity = { id: "electron-fixture", name: "Fixture", version: "1" };
const pageProvider = (failure: AnalysisError): ElectronObservationPort => ({
@@ -236,6 +236,7 @@ it.each(["list", "inspect"] as const)(
it("keeps cancellation when the retained Electron inspection is incomplete", async () => {
const reason = { requested: "stop before page inspection completed" };
// A JSON-safe but incomplete inspection cannot be converted into Evidence.
const partial = {
kind: "electron-page-observation",
inspection: { target_id: "electron-page" },
@@ -131,8 +131,10 @@ describe("Electron semantic coverage", () => {
`const { ipcMain } = require("electron");\nipcMain.handle("ping", () => 1);\n`,
);
expect(limitations).not.toContain(EMPTY_COVERAGE);
expect(limitations).toContain(
"Electron relationships are derived from inert syntax; runtime registration, reachability, defaults, and enforcement remain unproven.",
expect(limitations).toContainEqual(
expect.stringMatching(
/inert syntax.*runtime registration.*reachability.*defaults.*enforcement.*unproven/iu,
),
);
expect(electronSemantics).toBe("recovered");
});
@@ -20,7 +20,7 @@ import {
expectOutputError,
expectOwnerError,
expectPreemptedCancellation,
} from "../ownerObservationExpectation.fixture.js";
} from "../../../tests/fixtures/ownerObservationExpectation.js";
const observeCancelled = async (partial: unknown) => {
const reason = { requested: "stop before runtime observation completed" };
@@ -215,8 +215,8 @@ it("projects closure and direct interprocedural flow without execution", () => {
"returns-to-call",
] as const)
expect(relations.has(expected)).toBe(true);
expect(graph.limitations).toContain(
"The semantic graph contains static syntax observations and conservative relationship candidates; it does not claim runtime execution.",
expect(graph.limitations).toContainEqual(
expect.stringMatching(/static.*syntax.*does not claim runtime execution/iu),
);
const parameter = graph.nodes.find(
({ kind, label }) => kind === "parameter" && label === "value",
@@ -114,8 +114,10 @@ it("keeps symbol origin unknown when supplied metadata describes a different sam
const member = report.matches
.flatMap((match) => match.left)
.find((value) => value.address === "0x10");
expect(member?.limitations).toContain(
"Symbol origin unavailable; exact-name matching is unavailable. Independent complete body bytes remain eligible.",
expect(member?.limitations).toContainEqual(
expect.stringMatching(
/symbol origin unavailable.*exact-name matching is unavailable.*complete body bytes remain eligible/iu,
),
);
expect(member?.evidence_links).toContain(alias.evidence_id);
});
@@ -171,8 +173,10 @@ it("preserves contradictory symbol origins from inventory/name facts or multiple
withBytes ? "identical-bytes" : null,
);
const member = report.matches[0]!.left[0]!;
expect(member.limitations).toContain(
"Conflicting generated and named symbol-origin observations; exact-name matching is unavailable. Independent complete body bytes remain eligible.",
expect(member.limitations).toContainEqual(
expect.stringMatching(
/conflicting generated and named symbol-origin.*exact-name matching is unavailable.*complete body bytes remain eligible/iu,
),
);
expect(member.limitations.join(" ")).not.toContain(
"Symbol origin unavailable",
+1 -1
View File
@@ -265,7 +265,7 @@ const cliAnalysisError = (error: AnalysisError): ProcessCliErrorOutput => ({
* This catch includes target execution, and the projection stays non-retryable.
* Tell the user what happened; do not ask them to run the command again.
*/
export const PROCESS_UNEXPECTED_FAILURE_REMEDIATION =
const PROCESS_UNEXPECTED_FAILURE_REMEDIATION =
"Do not repeat this command. Tell the user the process command could not complete and ask them to check the input files, then run `rea doctor`.";
/** Preserve typed failures and local diagnostics at the process CLI boundary. */
@@ -43,5 +43,9 @@ it("keeps producer-owned recovery through cleanup wrapping and public projection
expect(projected.message).toBe(userMessage);
expect(projected.remediation.action).toBe(userMessage);
expect(projected.details?.detail).toBe("Exact command diagnostic");
expect(projected.details?.cleanup).toBeDefined();
expect(projected.details).toMatchObject({
cleanup: "incomplete",
cleanup_reason: "Owned staging cleanup needs retry",
resources: ["owned staging"],
});
});
+28 -21
View File
@@ -9,9 +9,6 @@ import {
normalizeArtifactPath,
} from "./ArtifactPaths.js";
const NOTE =
"a case-insensitive destination cannot store both. This comparison is not a filesystem observation.";
it.each(["drive", "escaped", "normalization", "prefix", "collision"])(
"bounds path failure diagnostics at the owning boundary: %s",
(kind) => {
@@ -66,6 +63,18 @@ const annotate = (paths: readonly string[]) => {
};
};
const expectCollision = (
limitations: string[] | undefined,
...facts: string[]
) => {
expect(limitations).toHaveLength(1);
const warning = limitations?.[0] ?? "";
for (const fact of facts) expect(warning).toContain(fact);
expect(warning).toMatch(/collid.*English.*case folding/iu);
expect(warning).toMatch(/case.insensitive destination.*cannot store both/iu);
expect(warning).toMatch(/not a filesystem observation/iu);
};
describe("English Unicode case collisions", () => {
it("retains repeated same-kind entries while rejecting file-directory aliasing", () => {
const registry = new ArtifactPathRegistry();
@@ -79,12 +88,8 @@ describe("English Unicode case collisions", () => {
it("names the other spelling of a colliding file", () => {
const { limitation, byPath } = annotate([".", "Main.js", "main.js", "x"]);
expect(limitation).toBe(ENGLISH_UNICODE_CASE_INVENTORY_LIMITATION);
expect(byPath.get("Main.js")).toEqual([
`Logical path Main.js collides under English (en-US) Unicode case folding with main.js; ${NOTE}`,
]);
expect(byPath.get("main.js")).toEqual([
`Logical path main.js collides under English (en-US) Unicode case folding with Main.js; ${NOTE}`,
]);
expectCollision(byPath.get("Main.js"), "Main.js", "main.js");
expectCollision(byPath.get("main.js"), "main.js", "Main.js");
expect(byPath.get("x")).toEqual([]);
});
@@ -95,19 +100,18 @@ describe("English Unicode case collisions", () => {
"res/foo",
"res/foo/b.txt",
]);
expect(byPath.get("res/Foo")).toEqual([
`Logical path res/Foo collides under English (en-US) Unicode case folding with res/foo; ${NOTE}`,
]);
expect(byPath.get("res/foo/b.txt")).toEqual([
`Logical path res/foo/b.txt is under res/foo, which collides under English (en-US) Unicode case folding with res/Foo; ${NOTE}`,
]);
expectCollision(byPath.get("res/Foo"), "res/Foo", "res/foo");
expectCollision(
byPath.get("res/foo/b.txt"),
"res/foo/b.txt",
"under res/foo",
"res/Foo",
);
});
it("counts further spellings, each of which is its own occurrence", () => {
const { byPath } = annotate(["A.txt", "a.txt", "a.TXT"]);
expect(byPath.get("a.txt")).toEqual([
`Logical path a.txt collides under English (en-US) Unicode case folding with A.txt and 1 other spelling; ${NOTE}`,
]);
expectCollision(byPath.get("a.txt"), "a.txt", "A.txt", "1 other spelling");
});
it("retains every spelling in a high-cardinality folded sibling group", () => {
@@ -123,9 +127,12 @@ describe("English Unicode case collisions", () => {
}
expect(limitation).toBe(ENGLISH_UNICODE_CASE_INVENTORY_LIMITATION);
expect(byPath.size).toBe(count);
expect(byPath.get(first)).toEqual([
`Logical path ${first} collides under English (en-US) Unicode case folding with ${second} and ${String(count - 2)} other spellings; ${NOTE}`,
]);
expectCollision(
byPath.get(first),
first,
second,
`${String(count - 2)} other spellings`,
);
expect(byPath.get(original)?.[0]).toContain(
`with ${first} and ${String(count - 2)} other spellings;`,
);
+1 -1
View File
@@ -42,7 +42,7 @@ it("waits for admitted operations and shares concurrent close", async () => {
expect(scope.close()).toBe(close);
await expect(scope.run(async () => undefined)).rejects.toMatchObject({
reason: "unavailable",
message: "Artifact resource scope is closed",
message: expect.stringContaining("closed"),
});
let closed = false;
void close.then(() => {
+11 -8
View File
@@ -168,8 +168,11 @@ it.skipIf(process.platform === "win32")(
code: "EACCES",
});
await expect(
readStableArtifact(selected, 1024, undefined, {
const rejected: unknown = await readStableArtifact(
selected,
1024,
undefined,
{
lstat: async (path) => {
if (path === selected) return selectedStats;
throw failure;
@@ -178,11 +181,11 @@ it.skipIf(process.platform === "win32")(
open: async () => {
throw new Error("A rejected symlink must not be opened");
},
}),
).rejects.toMatchObject({
reason: "path",
resolvedPath: undefined,
message: `Expected a regular file without a symlink: ${selected}`,
});
},
).catch((cause: unknown) => cause);
expect(rejected).toMatchObject({ reason: "path", resolvedPath: undefined });
if (!(rejected instanceof Error)) throw new Error("Expected path failure");
expect(rejected.message).toContain(selected);
expect(rejected.message).toContain("symlink");
},
);
+1 -1
View File
@@ -114,6 +114,6 @@ it("decodes admitted JSON across short reads without consuming padding", async (
),
).rejects.toMatchObject({
reason: "format",
message: "Truncated ASAR JSON header",
message: expect.stringMatching(/truncated.*header/iu),
});
});
+15 -11
View File
@@ -2,10 +2,7 @@ import type { Stats } from "node:fs";
import { expect, it } from "vitest";
import {
changeTimeMayStillDrift,
settleChangeTimeBaseline,
} from "../filesystem/ChangeTimeBaseline.js";
import { settleChangeTimeBaseline } from "../filesystem/ChangeTimeBaseline.js";
const DRIFT_MS = 2_000;
@@ -85,13 +82,20 @@ it("exits on the window closing rather than on a repeated value", async () => {
expect(seams.sleeps.length).toBe(2);
});
it("settles an unreadable ChangeTime rather than trusting it", () => {
expect(
changeTimeMayStillDrift(statsAt(Number.NaN), { platform: "win32" }),
).toBe(true);
expect(
changeTimeMayStillDrift(statsAt(Number.NaN), { platform: "linux" }),
).toBe(false);
it("re-observes an unreadable Windows ChangeTime but does not wait on other hosts", async () => {
const windows = scriptedRead([Number.NaN, 1_000]);
const seams = advancingSeams(5_000, 100);
const settled = await settleChangeTimeBaseline(windows.read, seams);
expect(settled.ctimeMs).toBe(1_000);
expect(windows.reads()).toBe(2);
expect(seams.sleeps).toHaveLength(1);
const linux = scriptedRead([Number.NaN, 1_000]);
const unreadable = await settleChangeTimeBaseline(linux.read, {
platform: "linux",
});
expect(unreadable.ctimeMs).toBeNaN();
expect(linux.reads()).toBe(1);
});
it("gives up settling instead of stalling admission forever", async () => {
+4 -4
View File
@@ -152,8 +152,8 @@ it("refuses a suffix CRC mismatch under the default integrity policy", async ()
trust: "observed-untrusted",
}),
]);
expect(recorded.limitations).toContain(
"1 integrity contradiction(s) were recorded; mismatched content is observed-untrusted.",
expect(recorded.limitations).toContainEqual(
expect.stringMatching(/1.*integrity.*observed-untrusted/iu),
);
expect(recorded.occurrences).toHaveLength(4);
});
@@ -184,8 +184,8 @@ it("extracts elements and carries a root contradiction into the extraction", asy
expect(extracted.integrity_contradictions).toEqual([
expect.objectContaining({ logical_path: ".", algorithm: "dfu-crc32" }),
]);
expect(extracted.limitations).toContain(
"The container contradicts its declared integrity metadata; every extracted file is observed-untrusted.",
expect(extracted.limitations).toContainEqual(
expect.stringMatching(/container.*integrity.*observed-untrusted/iu),
);
expect(extracted.limitations.join("\n")).not.toContain(
"extracted file(s) contradict",
@@ -169,8 +169,8 @@ describe("ESP flash layout inventory", () => {
expect(root(snapshot)).toMatchObject({
partition_table: { flash_address: 0xa000, method: "scan" },
});
expect(snapshot.limitations).toContain(
"The partition table at 0xa000 was found by scanning 0x1000-aligned flash sectors for a table whose MD5 verifies; the dump does not record the bootloader's configured table offset.",
expect(snapshot.limitations).toContainEqual(
expect.stringMatching(/0xa000.*0x1000.*MD5.*does not record/iu),
);
});
});
@@ -254,11 +254,18 @@ describe("ESP flash layout coverage and selection", () => {
facts: { status: "erased" },
});
// Only the erased app slot holds bytes the bootloader does not read.
expect(
snapshot.limitations.filter((text) => text.includes("finds no app")),
).toEqual([
`partitions/ota_1.bin begins with erased flash, so ESP-IDF's bootloader finds no app in it, but ${String(indices.length)} of its bytes from offset 0x${start.toString(16)} to 0x${end.toString(16)} are not erased flash; they are not read as an image.`,
]);
const erasedSlotWarnings = snapshot.limitations.filter((text) =>
/bootloader.*no app/iu.test(text),
);
expect(erasedSlotWarnings).toHaveLength(1);
const warning = erasedSlotWarnings[0] ?? "";
expect(warning).toContain("partitions/ota_1.bin");
expect(warning).toContain(String(indices.length));
expect(warning).toContain(`0x${start.toString(16)}`);
expect(warning).toContain(`0x${end.toString(16)}`);
expect(warning).toMatch(
/erased flash.*bootloader.*no app.*not erased.*not read as an image/iu,
);
// A partition erased throughout has no written bytes.
expect(row("partitions/nvs.bin")).toMatchObject({
written_offset: null,
@@ -324,8 +324,9 @@ describe("MachOSliceArtifactReader", () => {
reader.entries()[Symbol.asyncIterator]().next(),
).rejects.toMatchObject({
reason: "format",
message:
"lipo did not produce readable architecture records: lipo output contained no architectures",
message: expect.stringMatching(
/lipo.*readable architecture records.*no architectures/i,
),
cause: new TypeError("lipo output contained no architectures"),
});
});
+1 -1
View File
@@ -32,7 +32,7 @@ import {
} from "./NativeDmgOwnership.js";
const HDIUTIL_COMMAND_TIMEOUT_MS = 120_000;
/** Conditional recovery guidance; a path match never grants attachment ownership. */
export const DMG_EXISTING_ATTACHMENT_RECOVERY =
const DMG_EXISTING_ATTACHMENT_RECOVERY =
"hdiutil refused a private read-only DMG attachment after listing an existing attachment of this image path. If it is still attached, ask its owner to release it when safe, then retry. REA does not borrow or detach existing attachments.";
const DMG_CHECKSUM_ABSENT_LIMITATION =
"hdiutil verify found no stored checksum in this disk image, so its integrity was not verified; the root SHA-256 identifies the inventoried bytes.";
+4 -5
View File
@@ -116,16 +116,15 @@ it("retains malformed successful hdiutil inventory output as a typed producer pa
expect(calls).toEqual(["verify", "info"]);
expect(failure).toMatchObject({
reason: "format",
message: "hdiutil info -plist returned malformed image inventory",
message: expect.stringMatching(/hdiutil.*info.*inventory/iu),
cause: { name: "ZodError" },
});
if (!(failure instanceof Error))
throw new Error("Expected inventory failure");
expect(
projectAnalysisError(translateArtifactFailure("inspect_artifact", failure)),
).toMatchObject({
details: {
reason: "format",
detail: "hdiutil info -plist returned malformed image inventory",
},
details: { reason: "format", detail: failure.message },
});
});
+6 -11
View File
@@ -7,13 +7,10 @@ import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory
import { scanCanonicalArtifactInventory } from "../../tests/fixtures/artifactInventory.js";
import {
NativeDmgArtifactReader,
DMG_EXISTING_ATTACHMENT_RECOVERY,
type NativeDmgHost,
} from "./NativeDmgArtifactReader.js";
import { translateArtifactFailure } from "./ArtifactOperationFailure.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { analysisErrorUserMessage } from "../domain/analysisErrorPresentation.js";
import { ArtifactOperationError } from "../domain/artifactOperationError.js";
it.each(["same", "other", "missing-path", "no-path", "empty-group"] as const)(
"retains DMG root facts and limits existing-attachment guidance to a resolved image path: %s",
@@ -78,7 +75,6 @@ it.each(["same", "other", "missing-path", "no-path", "empty-group"] as const)(
} catch (cause: unknown) {
failure = cause;
}
expect(failure).toBeDefined();
const projected = projectAnalysisError(
translateArtifactFailure("inspect_artifact", failure),
);
@@ -109,16 +105,15 @@ it.each(["same", "other", "missing-path", "no-path", "empty-group"] as const)(
expect.objectContaining({ kind: "artifact-inventory" }),
);
if (listed === "same") {
expect(projected.message).toBe(DMG_EXISTING_ATTACHMENT_RECOVERY);
expect(projected.remediation.action).toBe(
DMG_EXISTING_ATTACHMENT_RECOVERY,
expect(projected.message).toMatch(
/hdiutil.*existing attachment.*owner.*release.*retry.*does not borrow or detach/iu,
);
expect(projected.remediation.action).toBe(projected.message);
} else {
expect(projected.message).toBe(
analysisErrorUserMessage(
new ArtifactOperationError("inspect_artifact", "unavailable"),
),
expect(projected.message).toMatch(
/unavailable.*availability reason.*before retrying/iu,
);
expect(projected.message).not.toMatch(/existing attachment|detach/iu);
}
expect(calls).not.toContain("detach");
},
+45 -33
View File
@@ -12,19 +12,10 @@ import { expect, it, vi } from "vitest";
import * as artifactPaths from "./ArtifactPaths.js";
import { NodeSeaArtifactReader } from "./NodeSeaArtifactReader.js";
import { artifactFormatDescriptor } from "./artifactFormats.js";
import { ArtifactResourceScope } from "./ArtifactResourceScope.js";
import { extractArtifact } from "./extraction/ArtifactExtraction.js";
import {
NODE_SEA_BAD_MAGIC,
NODE_SEA_DUPLICATE_BLOB,
NODE_SEA_ELF_NOTES_REPEATED,
NODE_SEA_FUSE_CONFLICT,
NODE_SEA_FUSE_ABSENT,
NODE_SEA_FUSE_UNSET,
NODE_SEA_MAGIC,
NODE_SEA_TRUNCATED,
NODE_SEA_UNKNOWN_FLAGS,
} from "./nodeSea/NodeSeaBlob.js";
import { NODE_SEA_MAGIC } from "./nodeSea/NodeSeaBlob.js";
import {
NODE_SEA_FUSE_HASH,
buildNodeSeaBlob,
@@ -68,10 +59,14 @@ it.each([
const snapshot = await scanCanonicalArtifactInventory(path);
expect(snapshot.manifest.root_format).toBe("node-sea");
expect(snapshot.coverage.status).toBe("complete");
expect(snapshot.limitations).toContain(
"Node SEA inventory establishes the injected blob, flags, fuse sentinel, main script, optional code cache, snapshot, and assets. V8 snapshots and code cache are not deserialized, and the application is not executed.",
const limitation = artifactFormatDescriptor("node-sea")?.readerLimitation;
if (limitation === undefined) throw new Error("Missing Node SEA limitation");
expect(snapshot.limitations).toContain(limitation);
expect(limitation).toMatch(/snapshots.*code cache.*not deserialized/iu);
expect(limitation).toMatch(/application.*not executed/iu);
expect(snapshot.limitations).not.toContainEqual(
expect.stringMatching(/NODE_SEA_FUSE sentinel.*unset/iu),
);
expect(snapshot.limitations).not.toContain(NODE_SEA_FUSE_UNSET);
const root = snapshot.occurrences.find((row) => row.logical_path === ".");
expect(root).toMatchObject({ artifact_kind: "container" });
expect(root?.format_metadata).toMatchObject({
@@ -221,22 +216,26 @@ it.each([
"reports a %s fuse as a contradiction",
async (_label, fuse, state, sentinel) => {
const reason =
state === "absent" ? NODE_SEA_FUSE_ABSENT : NODE_SEA_FUSE_UNSET;
state === "absent"
? /no NODE_SEA_FUSE sentinel.*observed/iu
: /NODE_SEA_FUSE sentinel.*unset/iu;
const snapshot = await scanCanonicalArtifactInventory(
await writeCarrier(
"fuse",
nodeSeaElf({ ...(fuse === undefined ? {} : { fuse }), main: script }),
),
);
expect(
snapshot.occurrences.find((row) => row.logical_path === ".")
?.format_metadata,
).toMatchObject({
const metadata = snapshot.occurrences.find(
(row) => row.logical_path === ".",
)?.format_metadata;
expect(metadata).toMatchObject({
fuse_state: state,
fuse_sentinel: sentinel,
fuse_contradiction: reason,
fuse_contradiction: expect.stringMatching(reason),
});
expect(snapshot.limitations).toContain(reason);
const contradiction = (metadata as { fuse_contradiction: string })
.fuse_contradiction;
expect(snapshot.limitations).toContain(contradiction);
expect(snapshot.coverage.status).toBe("complete");
},
);
@@ -245,15 +244,17 @@ it("reports disagreeing fuse sentinels", async () => {
const snapshot = await scanCanonicalArtifactInventory(
await writeCarrier("conflict", nodeSeaPe({ fuse: "conflicting" })),
);
expect(
snapshot.occurrences.find((row) => row.logical_path === ".")
?.format_metadata,
).toMatchObject({
const metadata = snapshot.occurrences.find(
(row) => row.logical_path === ".",
)?.format_metadata;
expect(metadata).toMatchObject({
fuse_state: "conflicting",
fuse_sentinel: null,
fuse_contradiction: NODE_SEA_FUSE_CONFLICT,
fuse_contradiction: expect.stringMatching(/fuse sentinels disagree/iu),
});
expect(snapshot.limitations).toContain(NODE_SEA_FUSE_CONFLICT);
const contradiction = (metadata as { fuse_contradiction: string })
.fuse_contradiction;
expect(snapshot.limitations).toContain(contradiction);
});
it("leaves a Mach-O, ELF, or PE without a SEA slot in its native format", async () => {
@@ -354,12 +355,18 @@ it("fails closed when the located blob is truncated, has a bad magic, or unknown
nodeSeaElf({ blob: Buffer.from([0x20, 0xda, 0x43]) }),
),
),
).rejects.toMatchObject({ reason: "format", message: NODE_SEA_TRUNCATED });
).rejects.toMatchObject({
reason: "format",
message: expect.stringMatching(/Node SEA.*truncated/iu),
});
await expect(
scanCanonicalArtifactInventory(
await writeCarrier("bad-magic", nodeSeaMachO({ blob: Buffer.alloc(16) })),
),
).rejects.toMatchObject({ reason: "format", message: NODE_SEA_BAD_MAGIC });
).rejects.toMatchObject({
reason: "format",
message: expect.stringMatching(/magic.*not.*0x143da20/iu),
});
const unknown = Buffer.alloc(9);
unknown.writeUInt32LE(NODE_SEA_MAGIC, 0);
unknown.writeUInt32LE(1 << 5, 4);
@@ -370,7 +377,7 @@ it("fails closed when the located blob is truncated, has a bad magic, or unknown
),
).rejects.toMatchObject({
reason: "format",
message: NODE_SEA_UNKNOWN_FLAGS,
message: expect.stringMatching(/flags.*unknown bits/iu),
});
});
@@ -386,7 +393,7 @@ it("fails closed when two different blobs are declared", async () => {
),
).rejects.toMatchObject({
reason: "format",
message: NODE_SEA_DUPLICATE_BLOB,
message: expect.stringMatching(/Node SEA blob.*declared more than once/iu),
});
await expect(
scanCanonicalArtifactInventory(
@@ -397,7 +404,10 @@ it("fails closed when two different blobs are declared", async () => {
}),
),
),
).rejects.toMatchObject({ message: NODE_SEA_DUPLICATE_BLOB });
).rejects.toMatchObject({
reason: "format",
message: expect.stringMatching(/Node SEA blob.*declared more than once/iu),
});
});
it("keeps one blob when the same bytes are declared twice", async () => {
@@ -473,7 +483,9 @@ it("reads repeated PT_NOTE declarations at most once per note the file holds", a
),
).rejects.toMatchObject({
reason: "format",
message: NODE_SEA_ELF_NOTES_REPEATED,
message: expect.stringMatching(
/ELF note tables.*more notes than the file holds/iu,
),
});
});
+2 -2
View File
@@ -579,7 +579,7 @@ it("refuses to open ZIP symlink entries even when unixMode is absent", async ()
throw new Error(`Missing ZIP symlink ${logicalPath}`);
await expect(reader.open(entry)).rejects.toMatchObject({
reason: "format",
message: "ZIP entry is not a regular file",
message: expect.stringContaining("regular file"),
});
}
} finally {
@@ -839,7 +839,7 @@ it("joins an admitted ZIP enumeration before reader close completes", async () =
release.resolve();
await expect(next).rejects.toMatchObject({
reason: "unavailable",
message: "ZIP reader is closed",
message: expect.stringContaining("closed"),
});
await closing;
const completedReads = reads.mock.calls.length;
@@ -146,8 +146,10 @@ describe("Apple asset catalog application workflow", () => {
next_offset: null,
truncated: false,
});
expect(result.limitations).toContain(
"No compiled Assets.car catalog was found among the app bundle's regular files (symlinks are not followed); any resource keys are reported as unmatched.",
expect(result.limitations).toContainEqual(
expect.stringMatching(
/Assets\.car.*symlinks.*not followed.*unmatched/iu,
),
);
},
);
@@ -224,8 +226,10 @@ it.skipIf(process.platform === "win32")(
throw new Error("No catalog is present");
},
});
expect(result.limitations).toContain(
"Interface Builder resource-key joins may be incomplete because the decoded object inventory is incomplete.",
expect(result.limitations).toContainEqual(
expect.stringMatching(
/resource-key joins.*incomplete.*object inventory.*incomplete/iu,
),
);
},
);
@@ -48,20 +48,24 @@ for (const symlinks of [0, 1, 65])
examined: 2,
omitted: 1 + symlinks,
});
expect(report.limitations).toContain(
"1 Interface Builder archive(s) omitted because unsupported uid exact range.",
expect(report.limitations).toContainEqual(
expect.stringMatching(/1.*omitted.*unsupported uid exact range/iu),
);
if (symlinks > 0)
expect(report.limitations).toContain(
`${symlinks} Interface Builder archive(s) omitted because symlink not followed.`,
expect(report.limitations).toContainEqual(
expect.stringMatching(
new RegExp(`${symlinks}.*omitted.*symlink not followed`, "iu"),
),
);
if (symlinks < 64)
expect(report.limitations).toContain(
"Unsupported.nib: omitted because unsupported uid exact range.",
expect(report.limitations).toContainEqual(
expect.stringMatching(
/Unsupported\.nib.*omitted.*unsupported uid exact range/iu,
),
);
else
expect(report.limitations).toContain(
"2 additional omitted archive path(s) and their reasons were not retained; reporting is bounded to 64 paths.",
expect(report.limitations).toContainEqual(
expect.stringMatching(/2.*omitted.*not retained.*64 paths/iu),
);
const result = await analyzeAppleAssetCatalogs({
bundlePath: bundle,
+22 -16
View File
@@ -154,11 +154,7 @@ describe("keyed archive scalar projection", () => {
offset: 0,
limit: 2,
}),
).toThrow(
expect.objectContaining({
message: "binary plist date object is truncated",
}),
);
).toThrow(/date.*truncated/iu);
});
it("keeps overlapping date precision unknown without changing its sibling object", () => {
@@ -628,6 +624,10 @@ describe("binary keyed archive dates", () => {
Buffer.compare(await readFile(join(root, "archive.plist")), bytes),
).toBe(0);
});
});
describe("binary keyed archive invalid dates", () => {
const selection = { offset: 0, limit: 10 };
it.each(
[0x30, 0x31, 0x32, 0x34].map(
@@ -640,11 +640,17 @@ describe("binary keyed archive dates", () => {
copy[offset] = marker;
});
const original = Buffer.from(bytes);
expect(() => decodeKeyedArchiveBytes(bytes, selection)).toThrow(
new TypeError(
`binary plist date marker 0x${marker.toString(16)} is invalid`,
),
);
let decodeFailure: unknown;
try {
decodeKeyedArchiveBytes(bytes, selection);
} catch (cause: unknown) {
decodeFailure = cause;
}
expect(decodeFailure).toBeInstanceOf(TypeError);
if (!(decodeFailure instanceof Error))
throw new Error("Expected invalid date marker to fail");
expect(decodeFailure.message).toContain(`0x${marker.toString(16)}`);
expect(decodeFailure.message).toMatch(/date marker.*invalid/iu);
expect(Buffer.compare(bytes, original)).toBe(0);
if (marker === 0x30)
expect(createHash("sha256").update(bytes).digest("hex")).toBe(
@@ -662,7 +668,7 @@ describe("binary keyed archive dates", () => {
).rejects.toMatchObject({
name: "ArtifactReaderFailure",
reason: "format",
message: `Cannot decode selected Foundation keyed archive: binary plist date marker 0x${marker.toString(16)} is invalid`,
message: expect.stringContaining(decodeFailure.message),
});
expect(Buffer.compare(await readFile(path), bytes)).toBe(0);
},
@@ -817,16 +823,16 @@ describe("binary plist date object region", () => {
bytes.writeBigUInt64BE(BigInt(offsetTable), trailer + 24);
expect(() =>
estimateBinaryPlistExpansion(bytes, 1024 * 1024, { bounds: "strict" }),
).toThrow(new TypeError("binary plist date object is truncated"));
).toThrow(/date.*truncated/iu);
// Decoder bounds extend other objects to the end of the file. The date
// payload still has to end at the offset table. The keyed-archive reader
// uses those bounds and must refuse the same payload.
expect(() =>
estimateBinaryPlistExpansion(bytes, 1024 * 1024, { bounds: "decoder" }),
).toThrow(new TypeError("binary plist date object is truncated"));
).toThrow(/date.*truncated/iu);
expect(() =>
decodeKeyedArchiveBytes(bytes, { offset: 0, limit: 1 }),
).toThrow(new TypeError("binary plist date object is truncated"));
).toThrow(/date.*truncated/iu);
});
it("refuses a decoder-bounds date whose marker sits past the offset table", () => {
@@ -845,10 +851,10 @@ describe("binary plist date object region", () => {
bytes.writeBigUInt64BE(8n, trailer + 24);
expect(() =>
estimateBinaryPlistExpansion(bytes, 1024 * 1024, { bounds: "decoder" }),
).toThrow(new TypeError("binary plist date object is truncated"));
).toThrow(/date.*truncated/iu);
expect(() =>
decodeKeyedArchiveBytes(bytes, { offset: 0, limit: 1 }),
).toThrow(new TypeError("binary plist date object is truncated"));
).toThrow(/date.*truncated/iu);
});
});
+18 -12
View File
@@ -176,10 +176,13 @@ describe("FIT image facts", () => {
}),
).images;
expect(image?.metadata).toMatchObject({ load: null, arch: null });
expect(image?.limitations).toEqual([
"/images/kernel: arch is not a NUL-terminated UTF-8 string",
"/images/kernel: load is not a 1- or 2-cell address",
]);
expect(image?.limitations).toHaveLength(2);
expect(image?.limitations).toEqual(
expect.arrayContaining([
expect.stringMatching(/\/images\/kernel.*arch.*NUL.*UTF-8/iu),
expect.stringMatching(/\/images\/kernel.*load.*1- or 2-cell/iu),
]),
);
});
});
@@ -275,8 +278,9 @@ describe("FIT image data ranges", () => {
),
).toMatchObject({
reason: "format",
message:
"FIT is malformed: /images/chained: image-data target /images/shared has its own image-data",
message: expect.stringMatching(
/\/images\/chained.*image-data.*\/images\/shared/iu,
),
});
});
@@ -293,8 +297,9 @@ describe("FIT image data ranges", () => {
),
).toMatchObject({
reason: "format",
message:
"FIT is malformed: image data overlaps: /images/shared and the FIT devicetree",
message: expect.stringMatching(
/overlaps.*\/images\/shared.*devicetree/iu,
),
});
});
@@ -325,7 +330,7 @@ describe("FIT image data ranges", () => {
])("fails %s", (_name, file, message) => {
expect(failureOf(file)).toMatchObject({
reason: "format",
message: `FIT is malformed: ${message}`,
message: expect.stringContaining(message),
});
});
});
@@ -395,8 +400,9 @@ describe("FIT hashes", () => {
),
).toMatchObject({
reason: "integrity",
message:
"FIT image kernel hash-1 sha256 value is 31 bytes; sha256 values are 32 bytes",
message: expect.stringMatching(
/kernel.*hash-1.*sha256.*31 bytes.*32 bytes/iu,
),
});
});
});
@@ -567,7 +573,7 @@ describe("FIT fixed limits", () => {
it("fails a structure without /images", () => {
expect(failureOf(devicetreeFixture())).toMatchObject({
reason: "format",
message: "FIT is malformed: /images node is missing",
message: expect.stringMatching(/\/images.*missing/iu),
});
});
});
+120 -38
View File
@@ -4,20 +4,8 @@ import { expect, it } from "vitest";
import { ArtifactReaderFailure } from "../ArtifactReader.js";
import {
NODE_SEA_ASSET_COUNT,
NODE_SEA_ASSET_NAME_BYTES,
NODE_SEA_BAD_MAGIC,
NODE_SEA_EMPTY_CODE_PATH,
NODE_SEA_FUSE_CONFLICT,
NODE_SEA_FUSE_ABSENT,
NODE_SEA_FUSE_UNSET,
NODE_SEA_MAGIC,
NODE_SEA_TRAILING_BYTES,
NODE_SEA_TRUNCATED,
NODE_SEA_UNKNOWN_EXTENSION,
NODE_SEA_UNKNOWN_FLAGS,
decodeNodeSeaBlob,
nodeSeaAssetCollision,
observeNodeSeaFuseMatches,
} from "./NodeSeaBlob.js";
import { buildNodeSeaBlob } from "./NodeSea.fixture.js";
@@ -31,44 +19,59 @@ const header = (flags: number, extension = 1): Buffer => {
};
it("rejects a blob that ends before the magic", async () => {
await expect(decodeNodeSeaBlob(Buffer.from([0x20, 0xda]))).rejects.toThrow(
NODE_SEA_TRUNCATED,
);
await expect(
decodeNodeSeaBlob(Buffer.from([0x20, 0xda])),
).rejects.toMatchObject({
reason: "format",
message: expect.stringMatching(/Node SEA.*truncated/iu),
});
});
it("rejects a blob whose magic is not 0x143da20", async () => {
await expect(decodeNodeSeaBlob(Buffer.alloc(9))).rejects.toThrow(
NODE_SEA_BAD_MAGIC,
);
await expect(decodeNodeSeaBlob(Buffer.alloc(9))).rejects.toMatchObject({
reason: "format",
message: expect.stringMatching(/magic.*not.*0x143da20/iu),
});
});
it("rejects a header that ends before the extension byte", async () => {
await expect(decodeNodeSeaBlob(header(0).subarray(0, 8))).rejects.toThrow(
NODE_SEA_TRUNCATED,
);
await expect(
decodeNodeSeaBlob(header(0).subarray(0, 8)),
).rejects.toMatchObject({
reason: "format",
message: expect.stringMatching(/Node SEA.*truncated/iu),
});
});
it("rejects unknown flag bits", async () => {
await expect(decodeNodeSeaBlob(header(1 << 5))).rejects.toThrow(
NODE_SEA_UNKNOWN_FLAGS,
);
await expect(decodeNodeSeaBlob(header(1 << 5))).rejects.toMatchObject({
reason: "format",
message: expect.stringMatching(/flags.*unknown bits/iu),
});
});
it("rejects an unknown exec argv extension", async () => {
await expect(decodeNodeSeaBlob(header(0, 9))).rejects.toThrow(
NODE_SEA_UNKNOWN_EXTENSION,
);
await expect(decodeNodeSeaBlob(header(0, 9))).rejects.toMatchObject({
reason: "format",
message: expect.stringMatching(/exec argv extension.*unknown/iu),
});
});
it("rejects an empty code path", async () => {
await expect(
decodeNodeSeaBlob(buildNodeSeaBlob({ codePath: "" })),
).rejects.toThrow(NODE_SEA_EMPTY_CODE_PATH);
).rejects.toMatchObject({
reason: "format",
message: expect.stringMatching(/code path.*empty/iu),
});
});
it("rejects a truncated length-prefixed field", async () => {
const bytes = Buffer.concat([header(0), Buffer.alloc(4)]);
await expect(decodeNodeSeaBlob(bytes)).rejects.toThrow(NODE_SEA_TRUNCATED);
await expect(decodeNodeSeaBlob(bytes)).rejects.toMatchObject({
reason: "format",
message: expect.stringMatching(/Node SEA.*truncated/iu),
});
});
it("rejects colliding asset names", async () => {
@@ -81,7 +84,10 @@ it("rejects colliding asset names", async () => {
],
}),
),
).rejects.toThrow(nodeSeaAssetCollision("shared"));
).rejects.toMatchObject({
reason: "format",
message: expect.stringMatching(/Node SEA asset name collides.*shared/iu),
});
});
it("retains an empty asset key as a raw Node SEA key", async () => {
@@ -91,6 +97,43 @@ it("retains an empty asset key as a raw Node SEA key", async () => {
expect(decoded.assets).toMatchObject([{ name: "", range: { length: 5 } }]);
});
it.each([
{
field: "code path",
build: (value: string) => buildNodeSeaBlob({ codePath: value }),
read: (blob: Awaited<ReturnType<typeof decodeNodeSeaBlob>>) =>
blob.codePath,
},
{
field: "asset name",
build: (value: string) =>
buildNodeSeaBlob({ assets: [{ name: value, body: "" }] }),
read: (blob: Awaited<ReturnType<typeof decodeNodeSeaBlob>>) =>
blob.assets[0]?.name,
},
{
field: "exec argument",
build: (value: string) => buildNodeSeaBlob({ execArgv: [value] }),
read: (blob: Awaited<ReturnType<typeof decodeNodeSeaBlob>>) =>
blob.execArgv[0],
},
])(
"rejects invalid UTF-8 in $field while preserving a literal replacement character",
async ({ build, read }) => {
const valid = await decodeNodeSeaBlob(build("\ufffd"));
expect(read(valid)).toBe("\ufffd");
const malformed = build("fixture-marker");
const offset = malformed.indexOf("fixture-marker");
expect(offset).toBeGreaterThanOrEqual(0);
malformed[offset] = 0xff;
await expect(decodeNodeSeaBlob(malformed)).rejects.toMatchObject({
reason: "format",
message: expect.stringMatching(/not valid UTF-8/iu),
});
},
);
const limitOf = async (bytes: Buffer): Promise<ArtifactReaderFailure> => {
try {
await decodeNodeSeaBlob(bytes);
@@ -110,7 +153,9 @@ it("refuses an asset table over its count bound before creating records", async
const failure = await limitOf(bytes);
expect(failure).toMatchObject({
reason: "limit",
message: NODE_SEA_ASSET_COUNT,
message: expect.stringMatching(
/asset count.*100000.*no runtime override/iu,
),
});
});
@@ -126,17 +171,52 @@ it("refuses asset names over the decoded name budget", async () => {
);
expect(failure).toMatchObject({
reason: "limit",
message: NODE_SEA_ASSET_NAME_BYTES,
message: expect.stringMatching(
/asset names.*8388608.*no runtime override/iu,
),
});
});
it.each([
{
label: "count",
accepted: () => Array.from({ length: 4096 }, () => "x"),
args: () => Array.from({ length: 4097 }, () => "x"),
boundary: "capacity",
warning: /exec argv count.*4096.*no runtime override/iu,
},
{
label: "text",
accepted: () => ["x".repeat(1024 * 1024)],
args: () => ["x".repeat(1024 * 1024 + 1)],
boundary: "memory",
warning: /exec argv text.*1048576.*no runtime override/iu,
},
])(
"admits exec argument $label at its bound and refuses more",
async ({ accepted, args, boundary, warning }) => {
const atLimit = accepted();
expect(
(await decodeNodeSeaBlob(buildNodeSeaBlob({ execArgv: atLimit })))
.execArgv,
).toEqual(atLimit);
const failure = await limitOf(buildNodeSeaBlob({ execArgv: args() }));
expect(failure).toMatchObject({
reason: "limit",
boundary,
message: expect.stringMatching(warning),
});
},
);
it("rejects non-zero trailing bytes and keeps zero padding", async () => {
const padded = buildNodeSeaBlob({ trailing: Buffer.alloc(8) });
expect((await decodeNodeSeaBlob(padded)).codePath).toBe("sea.js");
const dirty = Buffer.concat([padded, Buffer.from([1])]);
await expect(decodeNodeSeaBlob(dirty)).rejects.toThrow(
NODE_SEA_TRAILING_BYTES,
);
await expect(decodeNodeSeaBlob(dirty)).rejects.toMatchObject({
reason: "format",
message: expect.stringMatching(/trailing bytes/iu),
});
});
it("decodes flags, the main script, code cache, assets, and exec argv", async () => {
@@ -186,13 +266,15 @@ it("reports an absent, unset, set, or conflicting fuse sentinel", () => {
expect(observeNodeSeaFuseMatches([])).toMatchObject({
fuseState: "absent",
fuseSentinel: null,
fuseContradiction: NODE_SEA_FUSE_ABSENT,
fuseContradiction: expect.stringMatching(
/no NODE_SEA_FUSE sentinel.*observed/iu,
),
});
const unset = `NODE_SEA_FUSE_${"a".repeat(32)}:0`;
expect(observeNodeSeaFuseMatches([unset, unset])).toMatchObject({
fuseState: "unset",
fuseSentinel: unset,
fuseContradiction: NODE_SEA_FUSE_UNSET,
fuseContradiction: expect.stringMatching(/NODE_SEA_FUSE sentinel.*unset/iu),
});
const set = `NODE_SEA_FUSE_${"b".repeat(32)}:1`;
expect(observeNodeSeaFuseMatches([set])).toMatchObject({
@@ -203,7 +285,7 @@ it("reports an absent, unset, set, or conflicting fuse sentinel", () => {
expect(observeNodeSeaFuseMatches([unset, set])).toMatchObject({
fuseState: "conflicting",
fuseSentinel: null,
fuseContradiction: NODE_SEA_FUSE_CONFLICT,
fuseContradiction: expect.stringMatching(/fuse sentinels disagree/iu),
});
});
+15 -19
View File
@@ -30,13 +30,11 @@ const MAX_EXEC_ARGV = 4096;
const MAX_EXEC_ARGV_TEXT_BYTES = 1024 * 1024;
const NO_OVERRIDE = "This release has no runtime override.";
export const NODE_SEA_BAD_MAGIC = "Node SEA blob magic is not 0x143da20";
const NODE_SEA_BAD_MAGIC = "Node SEA blob magic is not 0x143da20";
export const NODE_SEA_TRUNCATED = "Node SEA blob is truncated";
export const NODE_SEA_UNKNOWN_FLAGS =
"Node SEA blob flags contain unknown bits";
export const NODE_SEA_UNKNOWN_EXTENSION =
"Node SEA exec argv extension is unknown";
export const NODE_SEA_TRAILING_BYTES = "Node SEA blob has trailing bytes";
const NODE_SEA_UNKNOWN_FLAGS = "Node SEA blob flags contain unknown bits";
const NODE_SEA_UNKNOWN_EXTENSION = "Node SEA exec argv extension is unknown";
const NODE_SEA_TRAILING_BYTES = "Node SEA blob has trailing bytes";
export const NODE_SEA_DUPLICATE_BLOB =
"Node SEA blob is declared more than once";
export const NODE_SEA_ELF_DESCRIPTOR_TRUNCATED =
@@ -49,22 +47,19 @@ export const NODE_SEA_MACHO_COMMANDS_TRUNCATED =
"Node SEA Mach-O load commands are truncated";
export const NODE_SEA_MACHO_SEGMENT_TRUNCATED =
"Node SEA Mach-O segment table is truncated";
export const NODE_SEA_FUSE_ABSENT =
const NODE_SEA_FUSE_ABSENT =
"Node SEA blob is present but no NODE_SEA_FUSE sentinel was observed";
export const NODE_SEA_FUSE_UNSET =
const NODE_SEA_FUSE_UNSET =
"Node SEA blob is present but the NODE_SEA_FUSE sentinel is unset";
export const NODE_SEA_FUSE_CONFLICT = "Node SEA fuse sentinels disagree";
export const NODE_SEA_EMPTY_CODE_PATH = "Node SEA code path is empty";
export const NODE_SEA_INVALID_UTF8 = "Node SEA string is not valid UTF-8";
export const NODE_SEA_ASSET_COUNT = `Node SEA asset count exceeds the ${String(MAX_ASSET_COUNT)}-asset bound. ${NO_OVERRIDE}`;
export const NODE_SEA_ASSET_NAME_BYTES = `Node SEA asset names exceed the ${String(MAX_ASSET_NAME_BYTES)}-byte name budget. ${NO_OVERRIDE}`;
export const NODE_SEA_EXEC_ARGV_COUNT = `Node SEA exec argv count exceeds the ${String(MAX_EXEC_ARGV)}-argument bound. ${NO_OVERRIDE}`;
export const NODE_SEA_EXEC_ARGV_TEXT = `Node SEA exec argv text exceeds the ${String(MAX_EXEC_ARGV_TEXT_BYTES)}-byte bound. ${NO_OVERRIDE}`;
const NODE_SEA_FUSE_CONFLICT = "Node SEA fuse sentinels disagree";
const NODE_SEA_EMPTY_CODE_PATH = "Node SEA code path is empty";
const NODE_SEA_INVALID_UTF8 = "Node SEA string is not valid UTF-8";
const NODE_SEA_ASSET_COUNT = `Node SEA asset count exceeds the ${String(MAX_ASSET_COUNT)}-asset bound. ${NO_OVERRIDE}`;
const NODE_SEA_ASSET_NAME_BYTES = `Node SEA asset names exceed the ${String(MAX_ASSET_NAME_BYTES)}-byte name budget. ${NO_OVERRIDE}`;
const NODE_SEA_EXEC_ARGV_COUNT = `Node SEA exec argv count exceeds the ${String(MAX_EXEC_ARGV)}-argument bound. ${NO_OVERRIDE}`;
const NODE_SEA_EXEC_ARGV_TEXT = `Node SEA exec argv text exceeds the ${String(MAX_EXEC_ARGV_TEXT_BYTES)}-byte bound. ${NO_OVERRIDE}`;
export const NODE_SEA_BLOB_TOO_LARGE = `Node SEA blob exceeds the ${String(NODE_SEA_MAX_BLOB_BYTES)}-byte in-memory bound`;
export const nodeSeaAssetCollision = (name: string): string =>
`Node SEA asset name collides: ${name}`;
const FUSE_PREFIX = "NODE_SEA_FUSE_";
export interface NodeSeaByteRange {
@@ -276,7 +271,8 @@ class BlobCursor {
assertArtifactReadActive(signal);
}
const name = this.text(true, NODE_SEA_INVALID_UTF8, charge);
if (seen.has(name)) throw formatFailure(nodeSeaAssetCollision(name));
if (seen.has(name))
throw formatFailure(`Node SEA asset name collides: ${name}`);
seen.add(name);
assets.push({ name, range: this.byteRange() });
}
@@ -3,6 +3,7 @@ import { join } from "node:path";
import { expect, it, vi } from "vitest";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { withOwnedRegularFile } from "../../filesystem/OwnedRegularFile.js";
import type { OwnedFileHandle } from "../../filesystem/OwnedFileHandle.js";
import { ArtifactEmbeddedFormatProbes } from "../ArtifactEmbeddedFormatProbes.js";
import { ArtifactResourceScope } from "../ArtifactResourceScope.js";
import { inventoryArtifact } from "../inventory/ArtifactInventory.js";
@@ -61,6 +62,7 @@ it.each([
}),
);
const resourceScope = new ArtifactResourceScope();
let rootOwner: OwnedFileHandle | undefined;
try {
const classified = await classifyAndHashRootForInventory(
path,
@@ -68,6 +70,7 @@ it.each([
await lstat(path),
{ resourceScope },
);
rootOwner = classified.rootSource?.owner;
const admission = classified.readerAdmission;
if (
admission === undefined ||
@@ -95,8 +98,23 @@ it.each([
await reader.close();
}
} finally {
await resourceScope.close();
try {
if (rootOwner !== undefined) {
const cleanup = await resourceScope.release({
kind: "file-handle",
resource: `root artifact descriptor for ${path}`,
handle: rootOwner,
});
if (cleanup.kind === "failed")
throw resourceScope.cleanupFailure({ kind: "completed" }, [
cleanup,
]);
}
} finally {
await resourceScope.close();
}
}
expect(rootOwner?.handle.fd).toBe(-1);
},
);
@@ -314,7 +332,7 @@ it("does not cache an incomplete tail read as an absent cookie and can retry", a
try {
await expect(probes.pyInstaller()).rejects.toMatchObject({
reason: "format",
message: "CArchive tail is truncated",
message: expect.stringMatching(/tail.*truncated/iu),
});
expect(calls).toBe(2);
expect(Object.hasOwn(probes.completedFacts(), "pyInstaller")).toBe(false);
@@ -463,7 +463,6 @@ it.each(["empty", "span"])(
: parsed.value.kind === "opaque"
? parsed.value.limitation
: undefined;
expect(message).toBeDefined();
expect(message?.length).toBeLessThan(256);
expect(message).toContain("table index 0");
});
+4 -2
View File
@@ -140,8 +140,10 @@ describe("PNG visual diff", () => {
}),
);
expect(result).toMatchObject({ status: "identical", changed_pixels: 0 });
expect(result.limitations).toContain(
"PNG tRNS transparency is accepted only as a six-byte RGB color key; tRNS on RGBA or with another length is rejected.",
expect(result.limitations).toContainEqual(
expect.stringMatching(
/tRNS.*only.*six-byte RGB.*RGBA.*another length.*rejected/iu,
),
);
});
+14 -4
View File
@@ -794,9 +794,13 @@ it.each(["[2001:db8::1]", "[::1]", "[::ffff:192.0.2.1]"])(
_extension: { serverIPAddress: address },
},
});
expect(decoded.records[0]?.limitations).toContain(
"This capture declares Playwright 1.58.2 and reports bracketed IPv6 serverIPAddress. Original spellings are retained; brackets were removed only from valid IPv6 values in the validation copy. Producer declarations are not authenticated.",
const limitation = decoded.records[0]?.limitations.find((line) =>
line.includes("Playwright 1.58.2"),
);
expect(limitation).toMatch(/bracketed IPv6.*serverIPAddress/iu);
expect(limitation).toMatch(/original spellings.*retained/iu);
expect(limitation).toMatch(/brackets.*only.*valid IPv6.*validation copy/iu);
expect(limitation).toMatch(/producer declarations.*not authenticated/iu);
expect(
decoded.records[1]?.limitations.some((limitation) =>
limitation.includes("bracketed IPv6 serverIPAddress"),
@@ -825,9 +829,15 @@ it.each(["[2620:0:860:ed1a::1]", "[::1]"])(
expect(decoded.records[0]).toMatchObject({
reported: { serverIPAddress: address },
});
expect(decoded.records[0]?.limitations).toContain(
"This capture declares Chromium DevTools (WebInspector 537.36) and reports bracketed IPv6 serverIPAddress. Original spellings are retained; brackets were removed only from valid IPv6 values in the validation copy. Producer declarations are not authenticated.",
const limitation = decoded.records[0]?.limitations.find((line) =>
line.includes("WebInspector 537.36"),
);
expect(limitation).toMatch(
/Chromium DevTools.*bracketed IPv6.*serverIPAddress/iu,
);
expect(limitation).toMatch(/original spellings.*retained/iu);
expect(limitation).toMatch(/brackets.*only.*valid IPv6.*validation copy/iu);
expect(limitation).toMatch(/producer declarations.*not authenticated/iu);
expect(
decoded.records[1]?.limitations.some((limitation) =>
limitation.includes("bracketed IPv6 serverIPAddress"),
@@ -51,11 +51,11 @@ it("imports real v10 recorder text with per-stream identity and resource presenc
expect(
report.events.find((row) => row.method === "click")?.selector,
).toContain("Load value");
expect(
report.events
.flatMap((row) => row.resource_references)
.every((row) => row.state === "present-unread"),
).toBe(true);
const references = report.events.flatMap((row) => row.resource_references);
expect(references).toHaveLength(8);
expect(references.every((row) => row.state === "present-unread")).toBe(
true,
);
expect((await readFile(fixture.path)).equals(bytes)).toBe(true);
} finally {
await resources.close();
@@ -128,10 +128,14 @@ it("keeps missing resource references explicit without turning page exhaustion i
if (!result.ok) throw result.error;
expect(result.value.summary.unavailable_references).toBe(8);
expect(result.value.source_coverage.interpretation).toBe("partial");
const references = result.value.events.flatMap(
(row) => row.resource_references,
);
expect(references).toHaveLength(8);
expect(
result.value.events
.flatMap((row) => row.resource_references)
.every((row) => row.state === "missing" && row.matching_members === 0),
references.every(
(row) => row.state === "missing" && row.matching_members === 0,
),
).toBe(true);
} finally {
await resources.close();
@@ -197,6 +201,7 @@ it.each([
if (!result.ok) throw result.error;
expect(result.value.events).toHaveLength(events);
expect(result.value.source_coverage.interpretation).toBe("partial");
expect(result.value.streams.length).toBeGreaterThan(0);
expect(
result.value.streams.every((stream) => stream.state === "unknown"),
).toBe(true);
@@ -128,8 +128,9 @@ it.each(["grpc-web", "grpc-web-text"] as const)(
status: "partial",
examined_bytes: trailer.length,
error: {
reason:
"gRPC-web trailer field contains opaque non-UTF-8 bytes; that field was omitted",
reason: expect.stringMatching(
/gRPC-web trailer.*non-UTF-8 bytes.*field was omitted/i,
),
},
trailers: [
{ name: "grpc-status", value: "0" },
+1 -1
View File
@@ -41,7 +41,7 @@ const RECORD_BOUNDARY_PATHS = [
] as const;
/** Recovery for every JSON Lines selection refusal; the analysis result itself is valid. */
export const JSON_LINES_SELECTION_REMEDIATION =
const JSON_LINES_SELECTION_REMEDIATION =
"Use --format json for the complete document, or select one record collection with --filter-output.";
interface Collection {
+1 -1
View File
@@ -179,7 +179,7 @@ export const snapshotSelectionOption = z
* Explicit interpretation for headerless native executable formats. Flash
* dumps take their offsets on the artifact commands instead.
*/
export const formatSelectionOption = executableFormatHintSchema
const formatSelectionOption = executableFormatHintSchema
.exclude(["esp-flash-image"])
.optional()
.describe(
+12 -8
View File
@@ -440,18 +440,22 @@ describe("configuration failure diagnostics", () => {
const result = parseConfig({ [setting]: value });
expect(result.ok).toBe(false);
if (result.ok) return;
const constraint = `${setting} must be absolute`;
expect(projectAnalysisError(result.error)).toMatchObject({
const projected = projectAnalysisError(result.error);
expect(projected).toMatchObject({
code: "configuration_invalid",
category: "invalid_input",
message: `REA configuration is invalid: ${constraint}.`,
remediation: { action: expect.stringContaining(`Correct ${setting} `) },
details: { settings: [{ setting, constraint }] },
message: expect.stringContaining(setting),
remediation: { action: expect.stringContaining(setting) },
details: {
settings: [
{ setting, constraint: expect.stringMatching(/absolute/iu) },
],
},
});
expect(projected.message).toMatch(/absolute/iu);
expect(projected.remediation.action).toMatch(/correct/iu);
// The rejected value itself is not echoed.
expect(JSON.stringify(projectAnalysisError(result.error))).not.toContain(
value,
);
expect(JSON.stringify(projected)).not.toContain(value);
},
);
+1 -1
View File
@@ -20,7 +20,7 @@ export const DEFAULT_ASAR_HEADER_ADMISSION: AsarHeaderAdmission = {
source: "default",
};
export const asarMaxHeaderBytesConstraint = `${ASAR_HEADER_ADMISSION_SETTING} must be a positive decimal integer of at most ${String(MAXIMUM_ASAR_MAX_HEADER_BYTES)} bytes (256 MiB); the default is ${String(DEFAULT_ASAR_MAX_HEADER_BYTES)} bytes (16 MiB).`;
const asarMaxHeaderBytesConstraint = `${ASAR_HEADER_ADMISSION_SETTING} must be a positive decimal integer of at most ${String(MAXIMUM_ASAR_MAX_HEADER_BYTES)} bytes (256 MiB); the default is ${String(DEFAULT_ASAR_MAX_HEADER_BYTES)} bytes (16 MiB).`;
/** Optional decimal byte count for the ASAR header/index admission. */
export const asarMaxHeaderBytesSchema = z
+9 -4
View File
@@ -5,7 +5,6 @@ import { parseEnvironment } from "./environment.js";
import { parseConfig } from "./parseConfig.js";
import {
BRIDGE_MAX_REQUEST_BYTES_SETTING,
bridgeMaxRequestBytesConstraint,
DEFAULT_BRIDGE_MAX_REQUEST,
DEFAULT_BRIDGE_MAX_REQUEST_BYTES,
MAXIMUM_BRIDGE_MAX_REQUEST_BYTES,
@@ -119,10 +118,16 @@ it.each([
const parsed = parseBridgeMaxRequestBytes(value);
expect(parsed.ok).toBe(false);
if (parsed.ok) return;
expect(parsed.error.message).toContain(BRIDGE_MAX_REQUEST_BYTES_SETTING);
expect(parsed.error.message).toMatch(/positive decimal integer/i);
expect(parsed.error.message).toMatch(/7464 to 536870887 bytes/i);
expect(parsed.error.message).toMatch(/default.*16777216 bytes/i);
expect(parsed.error.message).toMatch(/symbol.name/i);
expect(parsed.error.message).toMatch(/framing/i);
expect(parsed.error.settings).toEqual([
{
setting: BRIDGE_MAX_REQUEST_BYTES_SETTING,
constraint: bridgeMaxRequestBytesConstraint,
constraint: parsed.error.message,
},
]);
expect(projectAnalysisError(parsed.error).code).toBe("configuration_invalid");
@@ -134,12 +139,12 @@ it.each([
expect(startup.error._tag).toBe("ConfigurationError");
expect(startup.error.settings).toContainEqual({
setting: BRIDGE_MAX_REQUEST_BYTES_SETTING,
constraint: bridgeMaxRequestBytesConstraint,
constraint: parsed.error.message,
});
if (doctor.ok) throw new Error(`Doctor accepted ${JSON.stringify(value)}`);
expect(doctor.error._tag).toBe("ConfigurationError");
expect(doctor.error.settings).toContainEqual({
setting: BRIDGE_MAX_REQUEST_BYTES_SETTING,
constraint: bridgeMaxRequestBytesConstraint,
constraint: parsed.error.message,
});
});
+1 -1
View File
@@ -62,7 +62,7 @@ export const DEFAULT_BRIDGE_MAX_REQUEST: BridgeMaxRequestBytes = {
*/
export type BridgePeer = "Hopper" | "Cutter" | "Ghidra";
export const bridgeMaxRequestBytesConstraint = `${BRIDGE_MAX_REQUEST_BYTES_SETTING} must be a positive decimal integer from ${String(MINIMUM_BRIDGE_MAX_REQUEST_BYTES)} to ${String(MAXIMUM_BRIDGE_MAX_REQUEST_BYTES)} bytes; the default is ${String(DEFAULT_BRIDGE_MAX_REQUEST_BYTES)} bytes (16 MiB). A value below ${String(MINIMUM_BRIDGE_MAX_REQUEST_BYTES)} bytes cannot carry the largest admitted symbol-name command, and a larger value is refused so the encoded payload plus its framing LF stays one string under Node's single-string ceiling.`;
const bridgeMaxRequestBytesConstraint = `${BRIDGE_MAX_REQUEST_BYTES_SETTING} must be a positive decimal integer from ${String(MINIMUM_BRIDGE_MAX_REQUEST_BYTES)} to ${String(MAXIMUM_BRIDGE_MAX_REQUEST_BYTES)} bytes; the default is ${String(DEFAULT_BRIDGE_MAX_REQUEST_BYTES)} bytes (16 MiB). A value below ${String(MINIMUM_BRIDGE_MAX_REQUEST_BYTES)} bytes cannot carry the largest admitted symbol-name command, and a larger value is refused so the encoded payload plus its framing LF stays one string under Node's single-string ceiling.`;
/** Optional decimal byte count for the Hopper and Cutter request payload ceiling. */
export const bridgeMaxRequestBytesSchema = positiveDecimalIntegerSchema(
+21 -4
View File
@@ -19,7 +19,6 @@ import { parseEnvironment } from "./environment.js";
import { parseConfig } from "./parseConfig.js";
import {
CAPTURE_DECODER_OLD_GENERATION_SETTING,
captureDecoderHeapConstraint,
DEFAULT_CAPTURE_DECODER_HEAP,
parseCaptureDecoderHeap,
} from "./captureDecoderHeap.js";
@@ -105,6 +104,18 @@ it("uses a valid REA_CAPTURE_DECODER_OLD_GENERATION_MIB override", async () => {
);
expect(projected.remediation.action).toContain("update_analysis_settings");
expect(projected.remediation.action).toMatch(/restart REA/iu);
expect(projected.remediation.action).toContain(
"capture.decoder_old_generation_mib",
);
expect(projected.remediation.action).toMatch(/without a REA restart/iu);
expect(projected.remediation.action).toMatch(
/removes inherited NODE_OPTIONS/iu,
);
expect(projected.remediation.action).toMatch(/Bun.*V8 flags are ignored/iu);
expect(projected.remediation.action).toMatch(/Windows.*512 MiB reserve/iu);
expect(projected.remediation.action).toMatch(
/Other Bun hosts enforce no decoder heap limit/iu,
);
if (process.versions.bun === undefined)
expect(projected.details).toMatchObject({
limit: {
@@ -231,20 +242,26 @@ it.each([
const parsed = parseCaptureDecoderHeap(value);
if (parsed.ok) throw new Error(`Accepted ${JSON.stringify(value)}`);
expect(parsed.error).toBeInstanceOf(ConfigurationError);
expect(parsed.error.message).toContain(
CAPTURE_DECODER_OLD_GENERATION_SETTING,
);
expect(parsed.error.message).toMatch(/decimal integer/i);
expect(parsed.error.message).toMatch(/8 to 16384 MiB/i);
expect(parsed.error.message).toMatch(/default.*192 MiB/i);
expect(parsed.error.message).toMatch(/semi-space.*abort.*Node.*startup/i);
expect(parsed.error.settings).toEqual([
{
setting: CAPTURE_DECODER_OLD_GENERATION_SETTING,
constraint: captureDecoderHeapConstraint,
constraint: parsed.error.message,
},
]);
expect(parsed.error.message).toBe(captureDecoderHeapConstraint);
const startup = parseEnvironment({
REA_CAPTURE_DECODER_OLD_GENERATION_MIB: value,
});
if (startup.ok) throw new Error(`Startup accepted ${JSON.stringify(value)}`);
expect(startup.error.settings).toContainEqual({
setting: CAPTURE_DECODER_OLD_GENERATION_SETTING,
constraint: captureDecoderHeapConstraint,
constraint: parsed.error.message,
});
const config = parseConfig({
REA_CAPTURE_DECODER_OLD_GENERATION_MIB: value,
+1 -1
View File
@@ -22,7 +22,7 @@ export const DEFAULT_CAPTURE_DECODER_HEAP: CaptureDecoderHeap = {
source: "default",
};
export const captureDecoderHeapConstraint = `${CAPTURE_DECODER_OLD_GENERATION_SETTING} must be a decimal integer from ${String(CAPTURE_DECODER_SEMI_SPACE_MIB)} to ${String(MAXIMUM_CAPTURE_DECODER_OLD_GENERATION_MIB)} MiB; the default is ${String(DEFAULT_CAPTURE_DECODER_OLD_GENERATION_MIB)} MiB. An old generation below the decoder's ${String(CAPTURE_DECODER_SEMI_SPACE_MIB)} MiB semi-space aborts Node during startup instead of enforcing a limit, and a larger value is refused.`;
const captureDecoderHeapConstraint = `${CAPTURE_DECODER_OLD_GENERATION_SETTING} must be a decimal integer from ${String(CAPTURE_DECODER_SEMI_SPACE_MIB)} to ${String(MAXIMUM_CAPTURE_DECODER_OLD_GENERATION_MIB)} MiB; the default is ${String(DEFAULT_CAPTURE_DECODER_OLD_GENERATION_MIB)} MiB. An old generation below the decoder's ${String(CAPTURE_DECODER_SEMI_SPACE_MIB)} MiB semi-space aborts Node during startup instead of enforcing a limit, and a larger value is refused.`;
/** Optional decimal mebibyte count for the HAR/SAZ/qlog old-generation heap. */
export const captureDecoderOldGenerationMiBSchema =
+41 -17
View File
@@ -21,10 +21,6 @@ import {
DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING,
DEBUGGER_MAX_PENDING_COMMANDS_SETTING,
DEBUGGER_MAX_SESSIONS_SETTING,
debuggerCommandDeadlineConstraint,
debuggerMaxCommandOutputBytesConstraint,
debuggerMaxPendingCommandsConstraint,
debuggerMaxSessionsConstraint,
parseDebuggerSessionLimits,
resolveDebuggerSessionLimits,
} from "./debuggerSessionLimits.js";
@@ -93,30 +89,59 @@ it("accepts one in-range override for each shared debugger cap", () => {
});
it.each([
[DEBUGGER_COMMAND_DEADLINE_SETTING, "0", debuggerCommandDeadlineConstraint],
[DEBUGGER_MAX_SESSIONS_SETTING, "65", debuggerMaxSessionsConstraint],
[
DEBUGGER_COMMAND_DEADLINE_SETTING,
"0",
MINIMUM_DEBUGGER_COMMAND_DEADLINE_MS,
MAXIMUM_DEBUGGER_COMMAND_DEADLINE_MS,
120_000,
"ms",
],
[
DEBUGGER_MAX_SESSIONS_SETTING,
"65",
MINIMUM_DEBUGGER_MAX_SESSIONS,
MAXIMUM_DEBUGGER_MAX_SESSIONS,
8,
"sessions",
],
[
DEBUGGER_MAX_PENDING_COMMANDS_SETTING,
"00",
debuggerMaxPendingCommandsConstraint,
MINIMUM_DEBUGGER_MAX_PENDING_COMMANDS,
MAXIMUM_DEBUGGER_MAX_PENDING_COMMANDS,
16,
"commands",
],
[
DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING,
String(MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES + 1),
debuggerMaxCommandOutputBytesConstraint,
MINIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
16 * 1024 * 1024,
"bytes",
],
])(
"rejects invalid %s at the setting boundary",
(setting, value, constraint) => {
(setting, value, minimum, maximum, defaultValue, unit) => {
const parsed = parseDebuggerSessionLimits({ [setting]: value });
if (parsed.ok) throw new Error(`${setting} was accepted`);
expect(parsed.error).toBeInstanceOf(ConfigurationError);
expect(parsed.error.settings).toEqual([{ setting, constraint }]);
expect(parsed.error.settings).toHaveLength(1);
const detail = parsed.error.settings[0];
if (!detail) throw new Error("missing setting guidance");
expect(detail.setting).toBe(setting);
expect(detail.constraint).toContain(setting);
expect(detail.constraint).toMatch(/positive decimal integer/i);
expect(detail.constraint).toContain(
`from ${minimum} to ${maximum} ${unit}`,
);
expect(detail.constraint).toContain(`default is ${defaultValue} ${unit}`);
const startup = parseEnvironment({ [setting]: value });
if (startup.ok) throw new Error(`${setting} was accepted at startup`);
expect(projectAnalysisError(startup.error)).toMatchObject({
code: "configuration_invalid",
details: { settings: [{ setting, constraint }] },
details: { settings: [detail] },
});
},
);
@@ -229,6 +254,10 @@ it("refuses an invalid GDB deadline before a session starts", () => {
environment: { [DEBUGGER_COMMAND_DEADLINE_SETTING]: "0" },
}),
).toThrow(ConfigurationError);
const parsed = parseDebuggerSessionLimits({
[DEBUGGER_COMMAND_DEADLINE_SETTING]: "0",
});
if (parsed.ok) throw new Error("invalid deadline was accepted");
try {
new GdbSessionManager({
environment: { [DEBUGGER_COMMAND_DEADLINE_SETTING]: "0" },
@@ -238,12 +267,7 @@ it("refuses an invalid GDB deadline before a session starts", () => {
expect(projectAnalysisError(error)).toMatchObject({
code: "configuration_invalid",
details: {
settings: [
{
setting: DEBUGGER_COMMAND_DEADLINE_SETTING,
constraint: debuggerCommandDeadlineConstraint,
},
],
settings: parsed.error.settings,
},
});
}
+5 -12
View File
@@ -34,7 +34,7 @@ export const DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING =
export interface DebuggerAdmissionLimit {
readonly setting: string;
readonly value: number;
readonly source: "default" | "setting" | "runtime";
readonly source: "default" | "setting";
readonly defaultValue: number;
readonly minimum: number;
readonly maximum: number;
@@ -96,16 +96,16 @@ const constraintFor = (admission: DebuggerAdmissionLimit): string =>
: ""
}`;
export const debuggerCommandDeadlineConstraint = constraintFor(
const debuggerCommandDeadlineConstraint = constraintFor(
DEFAULT_DEBUGGER_COMMAND_DEADLINE,
);
export const debuggerMaxSessionsConstraint = constraintFor(
const debuggerMaxSessionsConstraint = constraintFor(
DEFAULT_DEBUGGER_MAX_SESSIONS_LIMIT,
);
export const debuggerMaxPendingCommandsConstraint = constraintFor(
const debuggerMaxPendingCommandsConstraint = constraintFor(
DEFAULT_DEBUGGER_MAX_PENDING_COMMANDS_LIMIT,
);
export const debuggerMaxCommandOutputBytesConstraint = constraintFor(
const debuggerMaxCommandOutputBytesConstraint = constraintFor(
DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_LIMIT,
);
@@ -243,10 +243,3 @@ export const debuggerControlReplyOutput: DebuggerAdmissionLimit = {
maximum: DEBUGGER_CONTROL_REPLY_BYTES,
unit: "bytes",
};
/** Test and caller seams replace the launch value and record that source. */
export const applyDebuggerAdmissionOverride = (
admission: DebuggerAdmissionLimit,
value: number | undefined,
): DebuggerAdmissionLimit =>
value === undefined ? admission : { ...admission, value, source: "runtime" };
+1 -1
View File
@@ -2,7 +2,7 @@ import { isAbsolute, basename } from "node:path";
import { z } from "zod";
/** Recovery for a packed Program selected where REA requires a project. */
export const GHIDRA_PACKED_PROGRAM_RECOVERY =
const GHIDRA_PACKED_PROGRAM_RECOVERY =
"A .gzf is a packed program, not a .gpr project. Import it into a new project with Ghidra analyzeHeadless <project-directory> <project-name> -import <file.gzf> -noanalysis; select its .gpr and, if needed, set REA_GHIDRA_PROGRAM_NAME to the imported program name.";
/** A single expanded local project marker; archives must be restored first. */
+11 -4
View File
@@ -6,7 +6,6 @@ import { runtimeNativeMatchingAllowance } from "./runtimeAnalysisSettings.js";
import {
DEFAULT_NATIVE_MATCHING_ALLOWANCE,
NATIVE_MATCHING_ALLOWANCE_SETTING,
nativeMatchingRepresentationBytesConstraint,
parseNativeMatchingAllowance,
} from "./nativeMatchingAllowance.js";
import { launchAnalysisSetting } from "./analysisSettings.js";
@@ -56,9 +55,17 @@ it("refuses invalid launch values instead of falling back to the default", () =>
expect(parsed.ok).toBe(false);
if (parsed.ok) continue;
expect(parsed.error).toBeInstanceOf(ConfigurationError);
expect(parsed.error.message).toBe(
nativeMatchingRepresentationBytesConstraint,
);
expect(parsed.error.message).toContain(NATIVE_MATCHING_ALLOWANCE_SETTING);
expect(parsed.error.message).toMatch(/positive decimal integer/i);
expect(parsed.error.message).toContain("1073741824");
expect(parsed.error.message).toContain("100663296");
expect(parsed.error.message).toMatch(/bytes/i);
expect(parsed.error.settings).toEqual([
{
setting: NATIVE_MATCHING_ALLOWANCE_SETTING,
constraint: parsed.error.message,
},
]);
expect(
launchAnalysisSetting("native.matching_representation_bytes", environment)
.ok,
+1 -1
View File
@@ -21,7 +21,7 @@ export const DEFAULT_NATIVE_MATCHING_ALLOWANCE: NativeMatchingAllowance = {
source: "default",
};
export const nativeMatchingRepresentationBytesConstraint = `${NATIVE_MATCHING_ALLOWANCE_SETTING} must be a positive decimal integer of at most ${String(MAXIMUM_NATIVE_MATCHING_REPRESENTATION_BYTES)} bytes (1 GiB); the default is ${String(DEFAULT_NATIVE_MATCHING_REPRESENTATION_BYTES)} bytes (96 MiB).`;
const nativeMatchingRepresentationBytesConstraint = `${NATIVE_MATCHING_ALLOWANCE_SETTING} must be a positive decimal integer of at most ${String(MAXIMUM_NATIVE_MATCHING_REPRESENTATION_BYTES)} bytes (1 GiB); the default is ${String(DEFAULT_NATIVE_MATCHING_REPRESENTATION_BYTES)} bytes (96 MiB).`;
/** Optional decimal byte count for the native function-matching allowance. */
const nativeMatchingRepresentationBytesSchema = z
+19 -4
View File
@@ -12,7 +12,6 @@ import {
DEFAULT_REGEX_MATCHING_DEADLINE,
parseRegexMatchingDeadline,
REGEX_MATCHING_DEADLINE_SETTING,
regexMatchingDeadlineConstraint,
regexMatchingDeadlineRecovery,
} from "./regexMatchingDeadline.js";
import { MAXIMUM_REGEX_MATCHING_DEADLINE_MS } from "../domain/analysisResourceLimits.js";
@@ -59,6 +58,18 @@ it("keeps the 5000 ms deadline when the setting is unset", () => {
},
});
expect(projected.remediation.action).toContain(recovery);
expect(projected.remediation.action).toContain(
REGEX_MATCHING_DEADLINE_SETTING,
);
expect(projected.remediation.action).toContain("5000 ms");
expect(projected.remediation.action).toContain("source default");
expect(projected.remediation.action).toContain("update_analysis_settings");
expect(projected.remediation.action).toContain("regex.matching_deadline_ms");
expect(projected.remediation.action).toMatch(/restart REA/i);
expect(projected.remediation.action).toMatch(/without a REA restart/i);
expect(projected.remediation.action).toMatch(
/heap does not raise this deadline/i,
);
});
it("uses a valid REA_REGEX_MATCHING_DEADLINE_MS override", () => {
@@ -123,18 +134,22 @@ it.each([
const parsed = parseRegexMatchingDeadline(value);
if (parsed.ok) throw new Error(`Accepted ${JSON.stringify(value)}`);
expect(parsed.error).toBeInstanceOf(ConfigurationError);
expect(parsed.error.message).toContain(REGEX_MATCHING_DEADLINE_SETTING);
expect(parsed.error.message).toMatch(/positive decimal integer/i);
expect(parsed.error.message).toMatch(/1 to 2147483647 ms/i);
expect(parsed.error.message).toMatch(/default.*5000 ms/i);
expect(parsed.error.message).toMatch(/Node.*1 ms/i);
expect(parsed.error.settings).toEqual([
{
setting: "REA_REGEX_MATCHING_DEADLINE_MS",
constraint: regexMatchingDeadlineConstraint,
constraint: parsed.error.message,
},
]);
expect(parsed.error.message).toBe(regexMatchingDeadlineConstraint);
const startup = parseEnvironment({ REA_REGEX_MATCHING_DEADLINE_MS: value });
if (startup.ok) throw new Error(`Startup accepted ${JSON.stringify(value)}`);
expect(startup.error.settings).toContainEqual({
setting: REGEX_MATCHING_DEADLINE_SETTING,
constraint: regexMatchingDeadlineConstraint,
constraint: parsed.error.message,
});
const config = parseConfig({ REA_REGEX_MATCHING_DEADLINE_MS: value });
if (config.ok)
+1 -1
View File
@@ -20,7 +20,7 @@ export const DEFAULT_REGEX_MATCHING_DEADLINE: RegexMatchingDeadline = {
source: "default",
};
export const regexMatchingDeadlineConstraint = `${REGEX_MATCHING_DEADLINE_SETTING} must be a positive decimal integer from 1 to ${String(MAXIMUM_REGEX_MATCHING_DEADLINE_MS)} ms; the default is ${String(DEFAULT_REGEX_MATCHING_DEADLINE_MS)} ms. Node schedules a delay above that maximum as 1 ms, so a larger value is refused.`;
const regexMatchingDeadlineConstraint = `${REGEX_MATCHING_DEADLINE_SETTING} must be a positive decimal integer from 1 to ${String(MAXIMUM_REGEX_MATCHING_DEADLINE_MS)} ms; the default is ${String(DEFAULT_REGEX_MATCHING_DEADLINE_MS)} ms. Node schedules a delay above that maximum as 1 ms, so a larger value is refused.`;
/** Optional decimal millisecond count for the supervised regex matching deadline. */
export const regexMatchingDeadlineMsSchema = positiveDecimalIntegerSchema(
+21 -4
View File
@@ -12,7 +12,6 @@ import {
DEFAULT_REGEX_WORKER_HEAP_REQUEST,
parseRegexWorkerHeapRequest,
REGEX_WORKER_HEAP_REQUEST_SETTING,
regexWorkerHeapRequestConstraint,
regexWorkerHeapRequestRecovery,
} from "./regexWorkerHeapRequest.js";
import {
@@ -66,6 +65,20 @@ it("keeps the 128 MiB request when the setting is unset", () => {
},
});
expect(projected.remediation.action).toContain(recovery);
expect(projected.remediation.action).toContain(
REGEX_WORKER_HEAP_REQUEST_SETTING,
);
expect(projected.remediation.action).toContain("128 MiB");
expect(projected.remediation.action).toContain("source default");
expect(projected.remediation.action).toContain("update_analysis_settings");
expect(projected.remediation.action).toContain(
"regex.worker_heap_request_mib",
);
expect(projected.remediation.action).toContain("--max-old-space-size");
expect(projected.remediation.action).toContain("NODE_OPTIONS");
expect(projected.remediation.action).toMatch(/restart REA/i);
expect(projected.remediation.action).toMatch(/without a REA restart/i);
expect(projected.remediation.action).toMatch(/overrides this request/i);
});
it("uses a valid REA_REGEX_WORKER_HEAP_REQUEST_MIB override", () => {
@@ -140,20 +153,24 @@ it.each([
const parsed = parseRegexWorkerHeapRequest(value);
if (parsed.ok) throw new Error(`Accepted ${JSON.stringify(value)}`);
expect(parsed.error).toBeInstanceOf(ConfigurationError);
expect(parsed.error.message).toContain(REGEX_WORKER_HEAP_REQUEST_SETTING);
expect(parsed.error.message).toMatch(/decimal integer/i);
expect(parsed.error.message).toMatch(/7 to 16384 MiB/i);
expect(parsed.error.message).toMatch(/default.*128 MiB/i);
expect(parsed.error.message).toMatch(/abort.*worker.*startup/i);
expect(parsed.error.settings).toEqual([
{
setting: "REA_REGEX_WORKER_HEAP_REQUEST_MIB",
constraint: regexWorkerHeapRequestConstraint,
constraint: parsed.error.message,
},
]);
expect(parsed.error.message).toBe(regexWorkerHeapRequestConstraint);
const startup = parseEnvironment({
REA_REGEX_WORKER_HEAP_REQUEST_MIB: value,
});
if (startup.ok) throw new Error(`Startup accepted ${JSON.stringify(value)}`);
expect(startup.error.settings).toContainEqual({
setting: REGEX_WORKER_HEAP_REQUEST_SETTING,
constraint: regexWorkerHeapRequestConstraint,
constraint: parsed.error.message,
});
const config = parseConfig({ REA_REGEX_WORKER_HEAP_REQUEST_MIB: value });
if (config.ok)
+1 -1
View File
@@ -22,7 +22,7 @@ export const DEFAULT_REGEX_WORKER_HEAP_REQUEST: RegexWorkerHeapRequest = {
source: "default",
};
export const regexWorkerHeapRequestConstraint = `${REGEX_WORKER_HEAP_REQUEST_SETTING} must be a decimal integer from ${String(MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} to ${String(MAXIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB; the default is ${String(DEFAULT_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB. A request below ${String(MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB aborts the worker during startup, and a larger value is refused.`;
const regexWorkerHeapRequestConstraint = `${REGEX_WORKER_HEAP_REQUEST_SETTING} must be a decimal integer from ${String(MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} to ${String(MAXIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB; the default is ${String(DEFAULT_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB. A request below ${String(MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB aborts the worker during startup, and a larger value is refused.`;
/** Optional decimal mebibyte count for the regex worker old-generation request. */
export const regexWorkerHeapRequestMiBSchema = positiveDecimalIntegerSchema(
+3 -15
View File
@@ -8,8 +8,6 @@ import {
WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
resolveWindowsChangeTimeSettle,
windowsChangeTimeSettingFailure,
windowsChangeTimeSettingMessage,
} from "./windowsChangeTimeSettle.js";
const requiredFacts = (
@@ -94,22 +92,12 @@ it.each([
[WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS, "0"],
[WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS, "1001"],
] as const)("rejects %s=%j", (bound, value) => {
const failure = windowsChangeTimeSettingFailure(bound, value);
expect(failure).toEqual({
setting: bound.setting,
currentValue: value,
source: "setting",
defaultValue: bound.defaultValue,
floor: bound.floor,
maximum: bound.maximum,
unit: bound.unit,
restartRequired: true,
});
const message = windowsChangeTimeSettingMessage(failure);
requiredFacts(message, failure);
const resolved = resolveWindowsChangeTimeSettle({ [bound.setting]: value });
if (resolved.ok) throw new Error(`Accepted ${bound.setting}=${value}`);
expect(resolved.error).toBeInstanceOf(ConfigurationError);
const message = resolved.error.settings[0]?.constraint;
if (message === undefined) throw new Error("Missing configuration guidance");
requiredFacts(message, { ...bound, currentValue: value, source: "setting" });
expect(resolved.error.settings).toEqual([
{ setting: bound.setting, constraint: message },
]);
+2 -2
View File
@@ -68,7 +68,7 @@ export interface WindowsChangeTimeSettleLimits {
readonly maxPollsSource: "default" | "setting";
}
export const windowsChangeTimeSettingFailure = (
const windowsChangeTimeSettingFailure = (
bound: WindowsChangeTimeBound,
currentValue: string,
): WindowsChangeTimeSettingFailure => ({
@@ -83,7 +83,7 @@ export const windowsChangeTimeSettingFailure = (
});
/** Name the rejected value, its source, the allowed range, and the required restart. */
export const windowsChangeTimeSettingMessage = (
const windowsChangeTimeSettingMessage = (
failure: WindowsChangeTimeSettingFailure,
): string =>
`The effective ${failure.setting} value is ${failure.currentValue} ${failure.unit} (source ${failure.source}; default ${String(failure.defaultValue)} ${failure.unit}, floor ${String(failure.floor)} ${failure.unit}, maximum ${String(failure.maximum)} ${failure.unit}). Set ${failure.setting} in the environment that starts REA to a positive decimal integer from ${String(failure.floor)} to ${String(failure.maximum)}, then restart REA.`;
+22 -11
View File
@@ -76,8 +76,9 @@ describe("workflow input contracts", () => {
issues: [
{
path: ["native_observations", 0],
message:
"Native observation Evidence must be distinct from managed boundary Evidence",
message: expect.stringMatching(
/native observation Evidence.*distinct.*managed boundary Evidence/i,
),
},
],
},
@@ -90,7 +91,9 @@ describe("workflow input contracts", () => {
issues: [
{
path: ["native_observations", 1],
message: "Native observation Evidence IDs must be unique",
message: expect.stringMatching(
/native observation Evidence IDs.*unique/i,
),
},
],
},
@@ -103,17 +106,21 @@ describe("workflow input contracts", () => {
issues: [
{
path: ["native_observations", 0],
message:
"Native observation Evidence must be distinct from managed boundary Evidence",
message: expect.stringMatching(
/native observation Evidence.*distinct.*managed boundary Evidence/i,
),
},
{
path: ["native_observations", 1],
message:
"Native observation Evidence must be distinct from managed boundary Evidence",
message: expect.stringMatching(
/native observation Evidence.*distinct.*managed boundary Evidence/i,
),
},
{
path: ["native_observations", 1],
message: "Native observation Evidence IDs must be unique",
message: expect.stringMatching(
/native observation Evidence IDs.*unique/i,
),
},
],
},
@@ -131,14 +138,18 @@ describe("workflow input contracts", () => {
native_observations: observations,
};
const contract = toolContract("verify_managed_native_boundaries");
let originalDiagnostics: unknown;
for (const schema of [
contract.inputSchema,
toolInputSchemaWithMetadata(contract),
]) {
const result = await schema["~standard"].validate(input);
expect(
result.issues?.map(({ path, message }) => ({ path, message })) ?? [],
).toEqual(issues);
const diagnostics =
result.issues?.map(({ path, message }) => ({ path, message })) ?? [];
expect(diagnostics).toEqual(issues);
if (originalDiagnostics === undefined)
originalDiagnostics = diagnostics;
else expect(diagnostics).toEqual(originalDiagnostics);
if (issues.length === 0) expect(result).toEqual({ value: input });
}
},
+13 -37
View File
@@ -4,40 +4,7 @@ import { setImmediate as tick } from "node:timers/promises";
import { describe, expect, it } from "vitest";
import {
SessionRequestQueue,
parseSessionRequest,
} from "./DesktopSessionRequests.js";
describe("parseSessionRequest", () => {
it("reads stop, unnamed and named checkpoints", () => {
expect(parseSessionRequest(" stop ")).toEqual({ kind: "stop" });
for (const line of ["", " ", "checkpoint", "checkpoint "])
expect(parseSessionRequest(line)).toEqual({
kind: "checkpoint",
name: undefined,
});
expect(parseSessionRequest("checkpoint saved file")).toEqual({
kind: "checkpoint",
name: "saved file",
});
expect(parseSessionRequest("typed")).toEqual({
kind: "checkpoint",
name: "typed",
});
});
it("keeps a name that only starts with the keyword", () => {
expect(parseSessionRequest("checkpoint-2")).toEqual({
kind: "checkpoint",
name: "checkpoint-2",
});
expect(parseSessionRequest("checkpoints")).toEqual({
kind: "checkpoint",
name: "checkpoints",
});
});
});
import { SessionRequestQueue } from "./DesktopSessionRequests.js";
describe("SessionRequestQueue", () => {
it("queues lines in order and treats the end of input as stop", async () => {
@@ -46,13 +13,22 @@ describe("SessionRequestQueue", () => {
createInterface({ input, terminal: false }),
);
input.end("checkpoint typed\n\n");
input.end(
" stop \n\n \ncheckpoint\ncheckpoint \ncheckpoint saved file\ntyped\ncheckpoint-2\ncheckpoints\n",
);
await tick();
await tick();
expect([queue.next(), queue.next(), queue.next(), queue.next()]).toEqual([
{ kind: "checkpoint", name: "typed" },
expect(Array.from({ length: 11 }, () => queue.next())).toEqual([
{ kind: "stop" },
{ kind: "checkpoint", name: undefined },
{ kind: "checkpoint", name: undefined },
{ kind: "checkpoint", name: undefined },
{ kind: "checkpoint", name: undefined },
{ kind: "checkpoint", name: "saved file" },
{ kind: "checkpoint", name: "typed" },
{ kind: "checkpoint", name: "checkpoint-2" },
{ kind: "checkpoint", name: "checkpoints" },
{ kind: "stop" },
undefined,
]);
+1 -1
View File
@@ -9,7 +9,7 @@ const QUEUE_RESUME = 4;
* `stop` ends the session. An empty line, `checkpoint` or `checkpoint <name>`
* requests a checkpoint; any other line names one.
*/
export const parseSessionRequest = (line: string): SessionRequest => {
const parseSessionRequest = (line: string): SessionRequest => {
const text = line.trim();
if (text === "stop") return { kind: "stop" };
const name = text.replace(/^checkpoint(?:\s+|$)/u, "");
+18 -2
View File
@@ -1207,6 +1207,10 @@ describe("analysis error projection: operational diagnostics", () => {
expect(exited.remediation.action).toBe(
analysisErrorRemediationAction(exitedError),
);
expect(exited.message).toMatch(
/Hopper exited.*search_strings.*exit code 7/iu,
);
expect(exited.remediation.action).toMatch(/close_binary.*open.*retry/iu);
const unknownError = new HopperProcessError(null, {
operation: "search_strings",
@@ -1225,6 +1229,12 @@ describe("analysis error projection: operational diagnostics", () => {
expect(unknown.remediation.action).toBe(
analysisErrorRemediationAction(unknownError),
);
expect(unknown.message).toMatch(
/could not determine.*busy, exited, or unreachable/iu,
);
expect(unknown.remediation.action).toMatch(
/provider_operation_health.*before retrying or restarting/iu,
);
const unreachableError = new HopperProcessError(null, {
operation: "search_strings",
@@ -1242,6 +1252,12 @@ describe("analysis error projection: operational diagnostics", () => {
message: analysisErrorUserMessage(unreachableError),
remediation: { action: analysisErrorRemediationAction(unreachableError) },
});
expect(unreachable.message).toMatch(
/unreachable.*process exit was not observed/iu,
);
expect(unreachable.remediation.action).toMatch(
/close_binary.*Evidence and Unknown history.*export_evidence_bundle/iu,
);
});
});
@@ -1415,8 +1431,8 @@ it("preserves ordinary configuration recovery and projects a producer-owned sess
remediationAction: "Repair the running session, then retry.",
}),
);
expect(original.remediation.action).toBe(
"Correct REA_EXAMPLE in the environment that starts REA (your shell or the MCP client's registration), then rerun.",
expect(original.remediation.action).toMatch(
/correct.*REA_EXAMPLE.*environment.*shell.*MCP.*registration.*rerun/iu,
);
expect(repairing).toEqual({
...original,
@@ -125,7 +125,7 @@ it.each(["node_count", "occurrence_count", "edge_count"] as const)(
{
path: ["source"],
reason: "invalid_value",
message: "Artifact inventory members exceed their manifest counts",
message: expect.stringMatching(/members.*manifest.*counts/iu),
},
],
},
@@ -260,8 +260,10 @@ it("templates uid literals, opaque tokens and identifier-like siblings when ever
properties: { overwrite: "boolean", title: "string" },
},
});
expect(projected.limitations).toContain(
"Opt-in template rules after-uid-literal, opaque-token, sibling-variance merge recorded literals by shape; merged parameters may conflate distinct resources.",
expect(projected.limitations).toContainEqual(
expect.stringMatching(
/after-uid-literal.*opaque-token.*sibling-variance.*may conflate distinct resources/iu,
),
);
});
@@ -824,8 +826,10 @@ it("selects the origin with the most JSON-like records and names it from the hos
status: "complete-within-view",
total: 0,
});
expect(empty.limitations).toContain(
"No retained record had a parseable origin; the manifest names no primary origin.",
expect(empty.limitations).toContainEqual(
expect.stringMatching(
/no retained record.*parseable origin.*no primary origin/iu,
),
);
});
@@ -1002,8 +1006,10 @@ it("does not compare routes beyond the sibling-variance segment limit and says s
`/api/items/b2/${tail}`,
"/api/things/{thing_id}",
]);
expect(projected.limitations).toContain(
"Routes with more than 32 segments are not compared by sibling-variance; they keep their grouped templates.",
expect(projected.limitations).toContainEqual(
expect.stringMatching(
/32 segments.*not compared.*sibling-variance.*grouped templates/iu,
),
);
});
@@ -1399,8 +1405,10 @@ it("shares no format-rule position and compares no route beyond the segment limi
"PATCH /api/teams/t3",
"GET /api/teams/{numeric}",
]);
expect(projected.limitations).toContain(
"Routes with more than 32 segments are not compared by prefix-variance; they keep their grouped templates.",
expect(projected.limitations).toContainEqual(
expect.stringMatching(
/32 segments.*not compared.*prefix-variance.*grouped templates/iu,
),
);
});
@@ -149,8 +149,10 @@ it("falls back to the observed time with no duration when a request never ended
});
expect(records.records[0]?.reported).not.toHaveProperty("time");
expect(records.records[1]?.reported).not.toHaveProperty("time");
expect(records.limitations).toContain(
"2 scenario request(s) had no unique reported duration; their duration is omitted and does not establish zero-length execution.",
expect(records.limitations).toContainEqual(
expect.stringMatching(
/2 scenario request.*no unique reported duration.*omitted.*does not establish zero-length execution/iu,
),
);
expect(records.records[2]?.reported).toMatchObject({
startedDateTime: "2026-10-17T18:00:02.000Z",
@@ -1217,8 +1217,10 @@ it("ends every type's state at a write whose URL was redacted, and says so", ()
expect(state.entities).toMatchObject([
{ fields: { title: "Granite Ledger" }, record_ordinals: [0] },
]);
expect(state.limitations).toContain(
"1 of the recorded writes had a redacted URL; each ends the initial state of every type, since its target is unknown.",
expect(state.limitations).toContainEqual(
expect.stringMatching(
/1.*writes.*redacted URL.*ends the initial state of every type.*target is unknown/iu,
),
);
});
@@ -1508,8 +1510,10 @@ it.each([
);
expect(capture).toEqual(before);
if (redactedWrite)
expect(state.limitations).toContain(
"1 of the recorded writes had a redacted URL; each ends the initial state of every type, since its target is unknown.",
expect(state.limitations).toContainEqual(
expect.stringMatching(
/1 .*writes.*redacted URL.*ends the initial state of every type.*target is unknown/iu,
),
);
},
);
+6 -2
View File
@@ -398,7 +398,9 @@ describe("Mach-O analysis views", () => {
exhausted: true,
});
});
});
describe("Mach-O view uncertainty", () => {
it("keeps encryption unknown for Evidence retained before it was reported", () => {
const { encryption: _omitted, ...legacy } = report;
const projected = projectAnalysisView(parent(legacy), { kind: "summary" });
@@ -411,8 +413,10 @@ describe("Mach-O analysis views", () => {
},
unknowns: [expect.stringContaining("exhaustive imports, encryption;")],
});
expect(projected.value.limitations).toContain(
"This inspect_macho Evidence predates encryption reporting; slice encryption status is unknown.",
expect(projected.value.limitations).toContainEqual(
expect.stringMatching(
/inspect_macho Evidence.*predates encryption reporting.*slice encryption status is unknown/iu,
),
);
});
@@ -166,8 +166,10 @@ describe("scenarioCaptureRecords", () => {
bodySize: -1,
},
});
expect(manifestLimitations(projected)).toContain(
"1 of 7 scenario transactions with a usable request line retained no response body; tools citing them replay only the recorded status and content type, either of which may be absent.",
expect(manifestLimitations(projected)).toContainEqual(
expect.stringMatching(
/1 of 7.*usable request line.*no response body.*status and content type.*may be absent/iu,
),
);
});
@@ -268,10 +268,17 @@ describe("deriveApkPosture v1 comparison", () => {
it("explains unknown identities on both sides without repeating reasons", () => {
const result = v1([null], [null, null]);
expect(result.posture.v1_signers_match_block_signers).toBeNull();
expect(result.notes).toEqual([
"v1_signers_match_block_signers is null: one or more v1 signer identities are unknown; inspect the v1 signer records for details.",
"v1_signers_match_block_signers is null: one or more v2 signer identities are unknown; inspect the block signer records for details.",
]);
expect(result.notes).toHaveLength(2);
expect(result.notes).toContainEqual(
expect.stringMatching(
/v1_signers_match_block_signers.*null.*v1 signer identities.*unknown.*inspect.*v1 signer records/iu,
),
);
expect(result.notes).toContainEqual(
expect.stringMatching(
/v1_signers_match_block_signers.*null.*v2 signer identities.*unknown.*inspect.*block signer records/iu,
),
);
expect(v1(["aa"], ["aa"]).notes).toEqual([]);
});
+4 -4
View File
@@ -492,8 +492,8 @@ describe("Apple bundle layouts and containers", () => {
status: "partial",
inventory_complete: true,
});
expect(result.limitations).toContain(
"The DMG's contents were not inventoried on this host; bundle absence is unknown.",
expect(result.limitations).toContainEqual(
expect.stringMatching(/DMG.*not inventoried.*absence is unknown/i),
);
});
@@ -691,8 +691,8 @@ describe("macOS application scope", () => {
[source, target].every((path) => path.startsWith("Fixture.app/")),
),
).toBe(true);
expect(result.limitations).toContain(
"4 inventoried entries outside every application root are not attributed to the application.",
expect(result.limitations).toContainEqual(
expect.stringMatching(/4 inventoried entries outside.*not attributed/i),
);
});
+2 -2
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { MAX_PLIST_DEPTH, plistNestingLimitMessage } from "./plistValue.js";
import { MAX_PLIST_DEPTH } from "./plistValue.js";
import { projectKeyedArchive } from "./keyedArchive.js";
import { UnknownPlistScalar } from "./unknownPlistScalar.js";
@@ -46,7 +46,7 @@ describe("projectKeyedArchive input bound", () => {
offset: 0,
limit: 1,
}),
).toThrow(plistNestingLimitMessage());
).toThrow(/128 nesting levels/iu);
});
});
+1 -1
View File
@@ -71,7 +71,7 @@ it.each(formats)(
ok: false,
error: {
kind: "malformed",
message: "Mach-O section header is truncated",
message: expect.stringContaining("truncated"),
},
});
},
+1 -1
View File
@@ -77,7 +77,7 @@ it.each(formats)(
ok: false,
error: {
kind: "malformed",
message: "Mach-O segment header is truncated",
message: expect.stringContaining("truncated"),
},
});
},
+1 -3
View File
@@ -19,9 +19,7 @@ const jsonScalarSchema = z.union([
export const MAX_PLIST_DEPTH = 128;
/** Agent-visible refusal for nesting past the JSON projection allowance. */
export const plistNestingLimitMessage = (
maximumDepth = MAX_PLIST_DEPTH,
): string =>
const plistNestingLimitMessage = (maximumDepth = MAX_PLIST_DEPTH): string =>
`Plist exceeds ${String(maximumDepth)} nesting levels. This release has no runtime override for this ${String(maximumDepth)}-level nesting allowance.`;
/** A source-value depth refusal, distinct from malformed decoder output. */
+3 -2
View File
@@ -92,8 +92,9 @@ it("rejects an inspection carrier whose Evidence subject disagrees with its nest
} catch (cause: unknown) {
expect(cause).toBeInstanceOf(EvidenceIntegrityError);
expect(cause).toMatchObject({
message:
"Artifact inspection Evidence subject does not match its root manifest",
message: expect.stringMatching(
/Evidence subject.*does not match.*root manifest/i,
),
cause: expect.any(ArtifactInventoryValidationError),
});
}
+2 -2
View File
@@ -308,8 +308,8 @@ it("reports a shared unknown redirect predecessor only once", () => {
for (const diff of diffs) {
expect(diff.status).toBe("unknown");
expect(diff.before_sha256).toBe(diff.after_sha256);
expect(diff.reason).toBe(
"Redirect predecessor evidence is missing; network transaction identity remains unknown.",
expect(diff.reason).toMatch(
/redirect predecessor evidence.*missing.*network transaction identity.*unknown/iu,
);
}
});
+9 -5
View File
@@ -139,8 +139,10 @@ describe("bundle comparison", () => {
]),
});
expect(first.changes).toHaveLength(3);
expect(first.limitations).toContain(
"EvidenceBundle has no inventory completeness marker; one-sided membership remains unknown.",
expect(first.limitations).toContainEqual(
expect.stringMatching(
/no inventory completeness marker.*membership remains unknown/i,
),
);
});
@@ -260,9 +262,11 @@ describe("bundle comparison history", () => {
expect.objectContaining({
classification: "unknown",
conclusion_kind: "unresolved_branch",
limitations: [
"EvidenceBundle has no inventory completeness marker; one-sided history membership remains unknown.",
],
limitations: expect.arrayContaining([
expect.stringMatching(
/no inventory completeness marker.*history membership remains unknown/i,
),
]),
}),
);
});
+6 -10
View File
@@ -59,21 +59,17 @@ describe("incremental canonical digest", () => {
[Number.NaN, "nan"],
[Number.POSITIVE_INFINITY, "infinity"],
[Number.NEGATIVE_INFINITY, "infinity"],
[cyclic, "circular"],
[recursiveJson, "circular"],
] as const) {
expect(() => digestCanonicalValue(value)).toThrow(CanonicalDigestError);
let failure: unknown;
try {
digestCanonicalValue(value);
} catch (cause) {
expect(cause).toMatchObject({ _tag: "CanonicalDigestError", code });
}
}
for (const value of [cyclic, recursiveJson]) {
expect(() => digestCanonicalValue(value)).toThrow(CanonicalDigestError);
try {
digestCanonicalValue(value);
} catch (cause) {
expect(cause).toMatchObject({ code: "circular" });
failure = cause;
}
expect(failure).toBeInstanceOf(CanonicalDigestError);
expect(failure).toMatchObject({ _tag: "CanonicalDigestError", code });
}
expect(() => digestCanonicalValue(1n)).toThrow();
});
Loaded 100 of 290 files, more files were not shown because too many files have changed in this diff. Show more