mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 13:56:47 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
765d00f552 | ||
|
|
f912a68417 | ||
|
|
6ca4e1368c | ||
|
|
1071ad867c | ||
|
|
c06b53644b | ||
|
|
352a846127 | ||
|
|
1e238241fd | ||
|
|
5679ed66f4 | ||
|
|
5ce9776a19 | ||
|
|
5b2bb57247 | ||
|
|
3fc27b45cd | ||
|
|
b9db66256a | ||
|
|
331f2dd827 | ||
|
|
2a2fdfc0db | ||
|
|
5fad84347a | ||
|
|
027aea38a2 | ||
|
|
f584290933 |
No files matched your search
@@ -325,7 +325,8 @@ on the first MCP call. It retains the complete observation and returns the
|
||||
original selection, text availability and length without the body. Use
|
||||
`normalized_result.parent_evidence_id` with `inspect_analysis_view` and
|
||||
`view: {kind: "native", facet: "pseudocode", offset: 0, limit: 20000}`;
|
||||
follow `coverage.next_offset` for later pages. Offsets and limits count UTF-16
|
||||
follow `coverage.next_offset` for later pages. Choose a smaller limit if one
|
||||
page exceeds the client or transport budget. Offsets and limits count UTF-16
|
||||
code units, not lines or tokens. Pages reuse the same observation without
|
||||
repeating decompilation. Null and empty text remain distinct. Omitted or
|
||||
`complete` detail preserves full inline delivery. Summary delivery requires a
|
||||
|
||||
@@ -876,6 +876,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
JsonArray blocks = new JsonArray();
|
||||
JsonArray skipped = new JsonArray();
|
||||
JsonArray notLoaded = new JsonArray();
|
||||
JsonArray overlays = new JsonArray();
|
||||
List<ByteSearchBlock> eligible = new ArrayList<>();
|
||||
for (MemoryBlock block : ordered) {
|
||||
monitor.checkCancelled();
|
||||
@@ -883,10 +884,16 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
Address high = block.getEnd();
|
||||
Address bound = start == null ? end : start;
|
||||
if (bound != null && !bound.getAddressSpace().equals(low.getAddressSpace())) continue;
|
||||
if (bound == null && (!block.isLoaded() || block.isOverlay())) {
|
||||
// Without bounds, search loaded non-overlay memory. Unloaded file
|
||||
// parts and loaded overlays stay reachable through explicit bounds.
|
||||
if (bound == null && !block.isLoaded()) {
|
||||
notLoaded.add(byteSearchCoverage(block, low, high));
|
||||
continue;
|
||||
}
|
||||
if (bound == null && block.isOverlay()) {
|
||||
overlays.add(byteSearchCoverage(block, low, high));
|
||||
continue;
|
||||
}
|
||||
if (start != null && low.compareTo(start) < 0) low = start;
|
||||
if (end != null && high.compareTo(end) > 0) high = end;
|
||||
if (low.compareTo(high) > 0) continue;
|
||||
@@ -960,6 +967,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
searched.addProperty("bytes", covered);
|
||||
searched.add("skipped_uninitialized", skipped);
|
||||
searched.add("skipped_not_loaded", notLoaded);
|
||||
searched.add("skipped_overlays", overlays);
|
||||
JsonObject result = new JsonObject();
|
||||
result.add("matches", matches);
|
||||
result.add("searched", searched);
|
||||
@@ -2376,7 +2384,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
if (!match.matched()) {
|
||||
continue;
|
||||
}
|
||||
JsonObject item = resultItem(entry.item, null);
|
||||
JsonObject item = resultItem(entry.item, "procedure");
|
||||
if (!match.aliases().isEmpty()) {
|
||||
JsonArray aliases = new JsonArray();
|
||||
for (String alias : match.aliases()) aliases.add(alias);
|
||||
@@ -2391,7 +2399,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
if (!matcher.matches(item.value)) {
|
||||
continue;
|
||||
}
|
||||
items.add(resultItem(item, null));
|
||||
items.add(resultItem(item, procedureSearch ? "procedure" : "string"));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ REA_KUNA_COMMAND=/absolute/path/to/kuna GHIDRA_INSTALL_DIR=/absolute/path/to/ghi
|
||||
npm run verify:kuna -- --targets DIR --functions inflate,crc32 --compare-ghidra
|
||||
```
|
||||
|
||||
The lane compiles a small source-owned fixture with the host C compiler and verifies the MCP workflow end to end. `--targets DIR --functions a,b` adds caller-built binaries. `--compare-ghidra` (with `GHIDRA_INSTALL_DIR`) runs those same functions through Ghidra; it requires `--targets` and `--functions` and refuses empty matrices or requested function names missing from every target. See [testing](development/testing.md#kuna-lane) for coverage.
|
||||
The lane compiles a small source-owned fixture with the host C compiler and verifies the MCP workflow end to end. `--targets DIR --functions a,b` adds caller-built binaries. `--compare-ghidra` (with `GHIDRA_INSTALL_DIR`) runs those same functions through Ghidra; it requires `--targets` and `--functions` and refuses empty matrices or requested function names missing from every target. The report lists examined caller binaries and their matching requested symbols, including inputs omitted from analysis. See [testing](development/testing.md#kuna-lane) for coverage.
|
||||
|
||||
Failed runs also report process-lineage evidence and exit unsuccessfully. If provider or transport cleanup is unconfirmed, the report says `cleanup: "incomplete"` and records `retained_workspace`; that directory is preserved for investigation and cleanup retries.
|
||||
|
||||
|
||||
@@ -89,6 +89,13 @@ does not prove text is absent. Both list and search Evidence carry this
|
||||
limitation. When an address is known, use `read_bytes` and `xrefs` to inspect
|
||||
bytes and references.
|
||||
|
||||
String searches and `trace_feature` retain each matched row's Ghidra-reported
|
||||
encoding, termination and byte length. These describe the database's defined
|
||||
data, not independent proof of the original source encoding. Procedure searches
|
||||
and trace matches likewise retain Ghidra's external and thunk classification.
|
||||
The Ghidra analysis profile identifies this search projection, so older
|
||||
snapshots with unqualified search rows cannot replay under the new profile.
|
||||
|
||||
## PE debug-symbol coverage
|
||||
|
||||
The post-analysis handshake reports Ghidra's PDB Program Information attributes:
|
||||
@@ -335,10 +342,17 @@ must be constrained.
|
||||
|
||||
Optional `start` and `end` addresses are inclusive and select one address space.
|
||||
A match must end at or before `end`, so `end` set inside a match excludes it.
|
||||
Without bounds, loaded memory blocks are eligible, in Ghidra address order.
|
||||
Blocks that are not loaded (file parts such as `.symtab`) and overlay blocks are
|
||||
listed in `searched.skipped_not_loaded` and are not searched; pass `start` or
|
||||
`end` in their address space to search them.
|
||||
Without bounds, loaded non-overlay memory blocks are eligible, in Ghidra address
|
||||
order, including across adjacent initialized blocks. Blocks that are not loaded
|
||||
(file parts such as `.symtab`) are listed in `searched.skipped_not_loaded`;
|
||||
loaded overlay blocks are listed separately in `searched.skipped_overlays`.
|
||||
Neither is searched by default; pass `start` or `end` in its address space to
|
||||
search it. A block that is both unloaded and an overlay is listed once under
|
||||
`skipped_not_loaded`.
|
||||
The separate overlay field changes the byte-search result contract and the
|
||||
Ghidra analysis profile. Refresh cached MCP schemas; snapshots from the older
|
||||
profile are not replayed. Previously exported Evidence remains importable as
|
||||
historical data with its original result shape.
|
||||
Overlapping matches are included, including across adjacent eligible initialized
|
||||
blocks in the same address space. Gaps and excluded or uninitialized blocks break
|
||||
a search run. Each match reports its address, starting segment, unique source file
|
||||
|
||||
+17
-3
@@ -448,7 +448,16 @@ caller-built functions through Ghidra (it refuses to run without `--targets` and
|
||||
reports latency, sampled process-tree CPU and peak memory, UTF-8 response size,
|
||||
and goto and warning-marker counts. Those counts are not a correctness judgement,
|
||||
and model tokens are not measured. Reference timings and response sizes are
|
||||
reported separately; reference caches expire when the server closes. `--report PATH` writes the JSON report.
|
||||
reported separately; reference caches expire when the server closes. The report's
|
||||
`caller_target_selection` retains the selected directory, the original functions
|
||||
argument, and each examined binary's requested names matched against `nm` text
|
||||
symbols. `symbol_scan_complete` means the scan of non-`.stripped` entries in
|
||||
the immediate flavor directories finished; it does not cover other directory
|
||||
entries. An `inspection_failed` entry and `symbol_scan_complete: false` retain
|
||||
partial discovery when symbol inspection fails. A missing symbol is not proof
|
||||
that code is absent, and selection does not establish that a stripped counterpart
|
||||
was verified; consult `results` for completed variants. `--report PATH` writes
|
||||
the JSON report.
|
||||
|
||||
The first matrix run used Kuna 1.833 and Ghidra 12.1.4 with JDK 21 on macOS
|
||||
arm64. It covered zlib 1.3.2 `minigzip`, coreutils 9.9 `sort` and the sqlite
|
||||
@@ -531,8 +540,13 @@ the entry changes without `provider_id`, preservation of a session annotation
|
||||
and the target/provider receipt after a rejected open, unchanged source bytes,
|
||||
and owned cleanup. Six-byte x86 and x86_64 controls also verify supported compiler
|
||||
specifications, independent mappings, unchanged bytes, and decompilation of a
|
||||
known return value. AArch64 remains covered by deterministic tests rather than
|
||||
this real-provider lane. See [raw binary analysis](providers/raw-binary.md).
|
||||
known return value. On a disposable Ghidra project, MCP `search_bytes` also
|
||||
checks a manually created loaded `overlay_test` block: an unbounded search lists
|
||||
it in `skipped_overlays` and not in `skipped_not_loaded`, and bounds in that
|
||||
overlay space find the pattern. This lane does not build an overlapping-load ELF,
|
||||
does not call CLI `search-bytes`, and does not page an overlay search across a
|
||||
snapshot reopen. AArch64 remains covered by deterministic tests rather than this
|
||||
real-provider lane. See [raw binary analysis](providers/raw-binary.md).
|
||||
|
||||
`npm run verify:ghidra:address-context` runs on Linux with a caller-installed
|
||||
Ghidra, full JDK, and an x86-64 ELF GCC toolchain (`CC` selects another compatible
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "./mcp-verifier-results.mjs";
|
||||
import { verifyGhidraBoundaries } from "./real-ghidra-boundaries.mjs";
|
||||
import { verifyGhidraProjectIdentity } from "./real-ghidra-project-identity.mjs";
|
||||
import { verifyGhidraSocketBoundary } from "./real-ghidra-socket-boundary.mjs";
|
||||
|
||||
const defaultEntrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
|
||||
const environment = () => ({
|
||||
@@ -355,6 +356,12 @@ async function verifyNativeValueAdapters(
|
||||
entrypoint,
|
||||
env,
|
||||
});
|
||||
const socketBoundary = await verifyGhidraSocketBoundary({
|
||||
target,
|
||||
procedure,
|
||||
entrypoint,
|
||||
env,
|
||||
});
|
||||
return {
|
||||
mocked: false,
|
||||
cli: true,
|
||||
@@ -363,6 +370,7 @@ async function verifyNativeValueAdapters(
|
||||
direct_function_comparison: true,
|
||||
boundaries,
|
||||
project_identity: projectIdentity,
|
||||
socket_boundary: socketBoundary,
|
||||
nodes: cliGraph.total_nodes,
|
||||
edges: cliGraph.total_edges,
|
||||
decompilations: cliGraph.decompilations,
|
||||
|
||||
@@ -23,6 +23,7 @@ import { verifyGhidraEntryAliases } from "./real-ghidra-entry-aliases.mjs";
|
||||
import { verifyGhidraNamespaceAnnotations } from "./real-ghidra-namespace-annotations.mjs";
|
||||
import { verifyGhidraBatchIdentity } from "./real-ghidra-batch-identity.mjs";
|
||||
import { verifyGhidraInstructionFlow } from "./real-ghidra-instruction-flow.mjs";
|
||||
import { verifyGhidraSearchFacts } from "./real-ghidra-search-facts.mjs";
|
||||
|
||||
/** Probe real Ghidra location, annotation and error contracts through public adapters. */
|
||||
export async function verifyGhidraBoundaries(
|
||||
@@ -410,6 +411,11 @@ export async function verifyGhidraBoundaries(
|
||||
(item) => item.value === "REA_GHIDRA_INVENTORY_ENTRY",
|
||||
);
|
||||
assert.ok(marker);
|
||||
await verifyGhidraSearchFacts(call, {
|
||||
strings,
|
||||
procedures,
|
||||
entryAddress: address,
|
||||
});
|
||||
assert.ok(
|
||||
(
|
||||
await call("search_strings", { pattern: marker.value.toLowerCase() })
|
||||
@@ -598,9 +604,7 @@ export async function verifyGhidraBoundaries(
|
||||
const longString = strings.find((item) => item.value === longLiteral);
|
||||
assert.ok(longString, "The provider must retain the full regression literal");
|
||||
const literalMatches = await call("search_strings", { pattern: longLiteral });
|
||||
assert.deepEqual(literalMatches, [
|
||||
{ address: longString.address, value: longLiteral },
|
||||
]);
|
||||
assert.deepEqual(literalMatches, [longString]);
|
||||
assert.deepEqual(await cli("search", longLiteral), literalMatches);
|
||||
await assert.rejects(
|
||||
cli("search", regexPattern, ["--mode", "regex"]),
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
/** Compare public searches and composed traces with their provider inventory rows. */
|
||||
export async function verifyGhidraSearchFacts(
|
||||
call,
|
||||
{ strings, procedures, entryAddress },
|
||||
) {
|
||||
const marker = strings.find(
|
||||
(row) => row.value === "REA_GHIDRA_INVENTORY_ENTRY",
|
||||
);
|
||||
const entry = procedures.find((row) => row.address === entryAddress);
|
||||
const external = procedures.find((row) => row.procedure.external);
|
||||
const thunk = procedures.find((row) => row.procedure.thunk);
|
||||
assert.ok(marker, "Missing source string fixture");
|
||||
assert.ok(entry, "Missing selected entry procedure");
|
||||
assert.ok(external, "Missing external procedure fixture");
|
||||
const cases = [
|
||||
{ type: "string", operation: "search_strings", row: marker },
|
||||
...[entry, external, ...(thunk === undefined ? [] : [thunk])].map(
|
||||
(row) => ({
|
||||
type: "procedure",
|
||||
operation: "search_procedures",
|
||||
row,
|
||||
}),
|
||||
),
|
||||
];
|
||||
for (const { type, operation, row } of cases) {
|
||||
const search = await call(operation, {
|
||||
pattern: row.value,
|
||||
mode: "literal",
|
||||
case_sensitive: true,
|
||||
});
|
||||
const match = search.find(
|
||||
(item) => item.address === row.address && item.value === row.value,
|
||||
);
|
||||
assert.deepEqual(
|
||||
match,
|
||||
row,
|
||||
`${operation} must preserve inventory facts for ${row.value}`,
|
||||
);
|
||||
const trace = await call("trace_feature", {
|
||||
query: row.value,
|
||||
case_sensitive: true,
|
||||
});
|
||||
const traced = trace.matches.find(
|
||||
(item) =>
|
||||
item.type === type &&
|
||||
item.address === row.address &&
|
||||
item.value === row.value,
|
||||
);
|
||||
assert.deepEqual(
|
||||
traced,
|
||||
{ ...row, type },
|
||||
`trace_feature must preserve ${operation} facts for ${row.value}`,
|
||||
);
|
||||
}
|
||||
return { compared: cases.length, thunk_observed: thunk !== undefined };
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
access,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
readdir,
|
||||
realpath,
|
||||
rm,
|
||||
stat,
|
||||
} from "node:fs/promises";
|
||||
import { basename, dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { withVerifierCleanup } from "./verifier-cleanup.mjs";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
|
||||
/** Exercise the OpenJDK Unix-socket boundary through real Ghidra and REA. */
|
||||
export async function verifyGhidraSocketBoundary({
|
||||
target,
|
||||
procedure,
|
||||
entrypoint,
|
||||
env,
|
||||
}) {
|
||||
if (process.platform !== "darwin" && process.platform !== "linux")
|
||||
return {
|
||||
status: "not-applicable",
|
||||
reason: "The Ghidra Unix-socket lane is not used on this host.",
|
||||
};
|
||||
const limit = process.platform === "darwin" ? 102 : 106;
|
||||
const before = createHash("sha256")
|
||||
.update(await readFile(target.path))
|
||||
.digest("hex");
|
||||
const root = await realpath(await mkdtemp("/tmp/rea-gsock-"));
|
||||
const suffixBytes = Buffer.byteLength("/rea-ghidra-XXXXXX/bridge.sock");
|
||||
let cleanupIncomplete = false;
|
||||
const samples = [];
|
||||
try {
|
||||
for (const { proposedBytes, unicode } of [
|
||||
{ proposedBytes: limit, unicode: false },
|
||||
{ proposedBytes: limit + 1, unicode: false },
|
||||
{ proposedBytes: limit + 2, unicode: false },
|
||||
{ proposedBytes: limit + 1, unicode: true },
|
||||
]) {
|
||||
const character = unicode ? "雪" : "";
|
||||
const padding =
|
||||
proposedBytes -
|
||||
suffixBytes -
|
||||
Buffer.byteLength(root) -
|
||||
1 -
|
||||
Buffer.byteLength(character);
|
||||
assert.ok(
|
||||
padding > 0,
|
||||
"Verifier workspace leaves no room for the boundary fixture",
|
||||
);
|
||||
const parent = join(root, "x".repeat(padding) + character);
|
||||
await mkdir(parent);
|
||||
assert.equal(Buffer.byteLength(parent) + suffixBytes, proposedBytes);
|
||||
if (unicode) assert.ok(Buffer.byteLength(parent) > parent.length);
|
||||
const sessionEnv = { ...env, TMPDIR: parent };
|
||||
const client = new Client({
|
||||
name: "rea-ghidra-socket-boundary",
|
||||
version: "1",
|
||||
});
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env: sessionEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
let connected = false;
|
||||
let opened = false;
|
||||
let runtimeRoot;
|
||||
let socketRoot;
|
||||
let endpoint;
|
||||
const cleanup = async (task) => {
|
||||
try {
|
||||
await task();
|
||||
} catch (cause) {
|
||||
cleanupIncomplete = true;
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
await withVerifierCleanup(async () => {
|
||||
await client.connect(transport);
|
||||
connected = true;
|
||||
const tools = new Map(
|
||||
(await client.listTools()).tools.map((tool) => [tool.name, tool]),
|
||||
);
|
||||
const call = async (name, arguments_ = {}) => {
|
||||
const reply = await client.callTool(
|
||||
{ name, arguments: arguments_ },
|
||||
{ timeout: 180000, toolDefinition: tools.get(name) },
|
||||
);
|
||||
assert.notEqual(
|
||||
reply.isError,
|
||||
true,
|
||||
`${name}: ${JSON.stringify(reply)}`,
|
||||
);
|
||||
return reply.structuredContent ?? JSON.parse(reply.content[0].text);
|
||||
};
|
||||
await call("open_binary", { path: target.path, provider_id: "ghidra" });
|
||||
opened = true;
|
||||
await call("list_documents");
|
||||
const roots = (await readdir(parent)).filter((name) =>
|
||||
name.startsWith("rea-ghidra-"),
|
||||
);
|
||||
assert.equal(roots.length, 1);
|
||||
runtimeRoot = join(parent, roots[0]);
|
||||
endpoint = JSON.parse(
|
||||
await readFile(join(runtimeRoot, "ownership.json"), "utf8"),
|
||||
).endpoint_path;
|
||||
assert.equal(
|
||||
Buffer.byteLength(join(runtimeRoot, "bridge.sock")),
|
||||
proposedBytes,
|
||||
);
|
||||
const fallback = dirname(endpoint) !== runtimeRoot;
|
||||
assert.equal(fallback, proposedBytes > limit);
|
||||
if (fallback) {
|
||||
socketRoot = dirname(endpoint);
|
||||
assert.match(basename(socketRoot), /^rea-ghidra-socket-/u);
|
||||
assert.equal((await stat(socketRoot)).mode & 0o777, 0o700);
|
||||
}
|
||||
samples.push({
|
||||
proposed_bytes: proposedBytes,
|
||||
unicode,
|
||||
endpoint_bytes: Buffer.byteLength(endpoint),
|
||||
fallback,
|
||||
});
|
||||
}, [
|
||||
() =>
|
||||
cleanup(async () => {
|
||||
if (!connected || !opened) return;
|
||||
const reply = await client.callTool(
|
||||
{ name: "close_binary", arguments: {} },
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
assert.notEqual(
|
||||
reply.isError,
|
||||
true,
|
||||
`close_binary: ${JSON.stringify(reply)}`,
|
||||
);
|
||||
}),
|
||||
() =>
|
||||
cleanup(async () => {
|
||||
await client.close();
|
||||
}),
|
||||
() =>
|
||||
cleanup(async () => {
|
||||
await transport.close();
|
||||
}),
|
||||
]);
|
||||
await assert.rejects(access(runtimeRoot), { code: "ENOENT" });
|
||||
if (socketRoot !== undefined)
|
||||
await assert.rejects(access(socketRoot), { code: "ENOENT" });
|
||||
if (proposedBytes === limit + 1 && !unicode) {
|
||||
const { stdout } = await exec(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"function",
|
||||
target.path,
|
||||
procedure,
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
],
|
||||
{ env: sessionEnv, timeout: 240000, maxBuffer: 72 * 1024 * 1024 },
|
||||
);
|
||||
assert.ok(JSON.parse(stdout).normalized_result.procedure);
|
||||
assert.deepEqual(
|
||||
(await readdir(parent)).filter((name) =>
|
||||
name.startsWith("rea-ghidra-"),
|
||||
),
|
||||
[],
|
||||
"CLI left a Ghidra runtime directory",
|
||||
);
|
||||
}
|
||||
}
|
||||
assert.equal(
|
||||
createHash("sha256")
|
||||
.update(await readFile(target.path))
|
||||
.digest("hex"),
|
||||
before,
|
||||
);
|
||||
return {
|
||||
status: "verified",
|
||||
samples,
|
||||
cli_boundary: true,
|
||||
target_unchanged: true,
|
||||
};
|
||||
} finally {
|
||||
// A failed cleanup retains its exact workspace for investigation and retry.
|
||||
if (!cleanupIncomplete) await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
@@ -760,7 +760,12 @@ async function verifyCrossBlockProject(target, targetSha256) {
|
||||
!all.matches.some((match) => match.address.startsWith("overlay_test:")),
|
||||
);
|
||||
assert.ok(
|
||||
all.searched.skipped_not_loaded.some(
|
||||
!all.searched.skipped_not_loaded.some(
|
||||
(block) => block.segment === "overlay_test",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
all.searched.skipped_overlays.some(
|
||||
(block) => block.segment === "overlay_test",
|
||||
),
|
||||
);
|
||||
|
||||
@@ -637,15 +637,37 @@ const measureGhidra = async (path, functions) => {
|
||||
return answers;
|
||||
};
|
||||
|
||||
/** Symbol-bearing caller binaries with the requested functions, beside their stripped copies. */
|
||||
const targetPairs = async (root, names) => {
|
||||
const describeFailure = (cause) => ({
|
||||
message: cause instanceof Error ? cause.message : String(cause),
|
||||
...(cause instanceof AggregateError
|
||||
? { causes: cause.errors.map(describeFailure) }
|
||||
: {}),
|
||||
});
|
||||
|
||||
/** Inspect symbol-bearing caller binaries, retaining both selected and unmatched inputs. */
|
||||
const targetPairs = async (root, names, selection) => {
|
||||
const pairs = [];
|
||||
for (const flavor of (await readdir(root)).sort()) {
|
||||
const directory = join(root, flavor);
|
||||
if (!(await stat(directory)).isDirectory()) continue;
|
||||
for (const file of (await readdir(directory)).sort()) {
|
||||
if (file.endsWith(".stripped")) continue;
|
||||
const symbols = await symbolAddresses(join(directory, file), names);
|
||||
let symbols;
|
||||
try {
|
||||
symbols = await symbolAddresses(join(directory, file), names);
|
||||
} catch (cause) {
|
||||
selection.examined.push({
|
||||
binary: `${flavor}/${file}`,
|
||||
status: "inspection_failed",
|
||||
error: describeFailure(cause),
|
||||
});
|
||||
throw cause;
|
||||
}
|
||||
selection.examined.push({
|
||||
binary: `${flavor}/${file}`,
|
||||
status: symbols.size > 0 ? "selected" : "no_matching_text_symbols",
|
||||
matched_functions: [...symbols.keys()],
|
||||
});
|
||||
if (symbols.size > 0) pairs.push({ flavor, directory, file, symbols });
|
||||
}
|
||||
}
|
||||
@@ -666,6 +688,18 @@ const verifyVariant = async ({ flavor, directory, symbols }, variant) => {
|
||||
const results = [];
|
||||
let timeoutRecovery;
|
||||
const failures = [];
|
||||
const callerTargetSelection =
|
||||
values.targets === undefined
|
||||
? undefined
|
||||
: {
|
||||
directory: values.targets,
|
||||
functions_argument: values.functions ?? null,
|
||||
requested_functions: (values.functions ?? "")
|
||||
.split(",")
|
||||
.filter(Boolean),
|
||||
symbol_scan_complete: false,
|
||||
examined: [],
|
||||
};
|
||||
let inspectionResourcesClean = true;
|
||||
try {
|
||||
const installation = await inspectKunaInstallation(
|
||||
@@ -678,13 +712,18 @@ try {
|
||||
);
|
||||
kunaVersion = `kuna ${installation.value.version}`;
|
||||
let callerPairs = [];
|
||||
if (values.targets !== undefined) {
|
||||
const names = (values.functions ?? "").split(",").filter(Boolean);
|
||||
if (callerTargetSelection !== undefined) {
|
||||
const names = callerTargetSelection.requested_functions;
|
||||
assert.ok(names.length > 0, "--targets requires --functions");
|
||||
callerPairs = await targetPairs(values.targets, names);
|
||||
callerPairs = await targetPairs(
|
||||
callerTargetSelection.directory,
|
||||
names,
|
||||
callerTargetSelection,
|
||||
);
|
||||
callerTargetSelection.symbol_scan_complete = true;
|
||||
assert.ok(
|
||||
callerPairs.length > 0,
|
||||
`No caller-built targets contain the requested functions in ${values.targets}`,
|
||||
`No caller-built targets contain the requested functions in ${callerTargetSelection.directory}`,
|
||||
);
|
||||
const found = new Set(
|
||||
callerPairs.flatMap(({ symbols }) => [...symbols.keys()]),
|
||||
@@ -772,18 +811,15 @@ if (cleanupComplete)
|
||||
cleanupComplete = false;
|
||||
failures.push(cause);
|
||||
}
|
||||
const describeFailure = (cause) => ({
|
||||
message: cause instanceof Error ? cause.message : String(cause),
|
||||
...(cause instanceof AggregateError
|
||||
? { causes: cause.errors.map(describeFailure) }
|
||||
: {}),
|
||||
});
|
||||
const report = {
|
||||
lane: "kuna",
|
||||
status: failures.length === 0 ? "passed" : "failed",
|
||||
kuna_version: kunaVersion,
|
||||
platform: `${process.platform}/${process.arch}`,
|
||||
verifier_run: lifecycle,
|
||||
...(callerTargetSelection === undefined
|
||||
? {}
|
||||
: { caller_target_selection: callerTargetSelection }),
|
||||
results,
|
||||
...(timeoutRecovery === undefined
|
||||
? {}
|
||||
|
||||
@@ -233,7 +233,6 @@ it.each([true, false])(
|
||||
{ timeoutMs: 100, maxOutputBytes: 1024, volatileOutput },
|
||||
undefined,
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected a capture allowance refusal");
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected.retryable).toBe(volatileOutput);
|
||||
@@ -263,7 +262,6 @@ it("reports the effective fixed adb deadline", async () => {
|
||||
{ timeoutMs: 600_000, maxOutputBytes: 1024 },
|
||||
undefined,
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected an adb deadline");
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected).toMatchObject({
|
||||
|
||||
@@ -87,7 +87,6 @@ const refusedBy = async (
|
||||
request: AdbRequest,
|
||||
): Promise<ReturnType<typeof projectAnalysisError>> => {
|
||||
const result = await provider.execute(request);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("expected a refusal");
|
||||
return projectAnalysisError(result.error);
|
||||
};
|
||||
@@ -577,7 +576,6 @@ it.each([
|
||||
await expect(lstat(packageDirectory)).rejects.toThrow();
|
||||
return;
|
||||
}
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw new Error(result.error.message);
|
||||
expect(
|
||||
(
|
||||
@@ -1463,7 +1461,6 @@ exit 1`,
|
||||
overwrite: false,
|
||||
},
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("expected snapshot cleanup to be reported");
|
||||
expect(result.error.cleanupResources).toHaveLength(1);
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
@@ -1523,7 +1520,6 @@ exit 1`,
|
||||
overwrite: false,
|
||||
},
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("expected a changed-artifact result");
|
||||
const evidence = parseEvidence(result.error.partialObservation);
|
||||
expect(evidence).toMatchObject({
|
||||
@@ -1868,7 +1864,6 @@ it("reads Android 16 focus declarations from displays when windows omits them",
|
||||
operation: "inspect_adb_window",
|
||||
input: { serial: "emulator-5554" },
|
||||
});
|
||||
expect(execution.ok).toBe(true);
|
||||
if (!execution.ok) throw execution.error;
|
||||
expect(execution.value.limitations).toContainEqual(
|
||||
expect.stringContaining("dumpsys window displays"),
|
||||
@@ -1891,7 +1886,6 @@ it("describes missing window focus as unknown after both sections omit declarati
|
||||
operation: "inspect_adb_window",
|
||||
input: { serial: "emulator-5554" },
|
||||
});
|
||||
expect(execution.ok).toBe(true);
|
||||
if (!execution.ok) throw execution.error;
|
||||
expect(execution.value.limitations).toContainEqual(
|
||||
expect.stringContaining("focus is unknown"),
|
||||
@@ -1915,7 +1909,6 @@ it.each(["windows", "displays"])(
|
||||
operation: "inspect_adb_window",
|
||||
input: { serial: "emulator-5554" },
|
||||
});
|
||||
expect(execution.ok).toBe(true);
|
||||
if (!execution.ok) throw execution.error;
|
||||
expect(execution.value.result).toMatchObject({
|
||||
focused_window: null,
|
||||
|
||||
@@ -114,7 +114,6 @@ it("applies the omitted 600000 ms decode deadline and a selected value on the ne
|
||||
expect(outputBudgets).toEqual([4 * 1024 * 1024]);
|
||||
|
||||
const timedOut = await decode(provider, path, 200);
|
||||
expect(timedOut.ok).toBe(false);
|
||||
if (timedOut.ok) throw new Error("Expected a decode deadline");
|
||||
const projected = projectAnalysisError(timedOut.error);
|
||||
expect(projected).toMatchObject({
|
||||
@@ -144,7 +143,6 @@ it("applies the omitted 600000 ms decode deadline and a selected value on the ne
|
||||
]);
|
||||
|
||||
const maximum = await decode(provider, path, MAX_APKTOOL_DECODE_TIMEOUT_MS);
|
||||
expect(maximum.ok).toBe(false);
|
||||
if (maximum.ok) throw new Error("Expected the maximum decode deadline");
|
||||
expect(projectAnalysisError(maximum.error).remediation.action).toContain(
|
||||
"do not increase it further",
|
||||
|
||||
@@ -97,7 +97,6 @@ const refusedBy = async (
|
||||
request: ApktoolRequest,
|
||||
): Promise<ReturnType<typeof projectAnalysisError>> => {
|
||||
const result = await provider.execute(request);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("expected a refusal");
|
||||
return projectAnalysisError(result.error);
|
||||
};
|
||||
@@ -583,7 +582,6 @@ it("still refuses invalid UTF-8 bytes in decoded strings", async () => {
|
||||
operation: "decode_android_resources",
|
||||
input: { path: apk.path, include_strings: true },
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("expected a refusal");
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "unreadable_output",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { parseOpenStep } from "plist";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
@@ -19,7 +18,6 @@ it.each([
|
||||
const executable = join(programs, "Fixture");
|
||||
const infoPlist = join(bundle, "Contents", "Info.plist");
|
||||
const source = `${prefix}{ CFBundleExecutable = Fixture; }`;
|
||||
expect(parseOpenStep(source)).toEqual({ CFBundleExecutable: "Fixture" });
|
||||
await mkdir(programs, { recursive: true });
|
||||
await writeFile(infoPlist, source);
|
||||
await writeFile(executable, "fixture - never executed");
|
||||
|
||||
@@ -286,7 +286,6 @@ it("retains completed bundle facts as bundle Evidence after cancellation", async
|
||||
total_analysis_timeout_ms: 1000,
|
||||
});
|
||||
const result = await analyzeWebBundle(browser, selected);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error).toBe(failure);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
@@ -313,7 +312,6 @@ it("preserves a prior caller reason without admitting browser work", async () =>
|
||||
input,
|
||||
{ signal: AbortSignal.abort(reason) },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancelled inventory");
|
||||
expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
expect(result.error.cause).toBe(reason);
|
||||
|
||||
@@ -48,7 +48,6 @@ const expectRetainedCapture = (
|
||||
result: Awaited<ReturnType<typeof runScenario>>,
|
||||
failure: AnalysisError,
|
||||
) => {
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected the retained capture");
|
||||
expect(result.error).toBe(failure);
|
||||
expect(parseEvidence(result.error.partialObservation)).toMatchObject({
|
||||
@@ -197,7 +196,6 @@ it("rejects a successful capture that is not a completed scenario", async () =>
|
||||
captureScenario: async () => ok({} as BrowserScenarioCapture),
|
||||
};
|
||||
const result = await captureBrowserScenario(provider, scenario);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected a completed-capture refusal");
|
||||
expect(result.error).toBeInstanceOf(AnalysisOutputError);
|
||||
expect(result.error.message).toContain(
|
||||
|
||||
@@ -28,7 +28,6 @@ describe("call-path traversal cancellation ownership", () => {
|
||||
input,
|
||||
controller.signal,
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
|
||||
@@ -59,7 +59,6 @@ describe("JSON client configuration repeated-member edits", () => {
|
||||
removeShadowedMembersAt: [path],
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw new Error("authorized repeats were refused");
|
||||
expect(parseStrictJson(result.value)).toEqual({
|
||||
mcpServers: {
|
||||
@@ -109,7 +108,6 @@ describe("JSON client configuration repeated-member edits", () => {
|
||||
"handles %s without losing peers",
|
||||
(_label, text, expectedServers, authority) => {
|
||||
const result = removeRea(text, "json", authority);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw new Error("non-shadowed removal was refused");
|
||||
expect(parseStrictJson(result.value)).toEqual({
|
||||
mcpServers: expectedServers,
|
||||
@@ -127,7 +125,6 @@ describe("JSON client configuration repeated-member edits", () => {
|
||||
removeShadowedMembersAt: [["mcpServers", "rea"]],
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw new Error("authorized escaped repeat was refused");
|
||||
expect(result.value).toContain(`"mcpServers":${inactive}`);
|
||||
expect(parseStrictJson(result.value)).toEqual({
|
||||
@@ -146,7 +143,6 @@ describe("JSON client configuration repeated-member edits", () => {
|
||||
removeShadowedMembersAt: [["mcp", "rea"]],
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw new Error("authorized JSONC repeats were refused");
|
||||
expect(result.value).toContain("// top comment");
|
||||
expect(result.value).toContain("// first");
|
||||
|
||||
@@ -42,7 +42,6 @@ for (const format of ["toml", "grok"] as const)
|
||||
format,
|
||||
source,
|
||||
);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw new Error("TOML edit was refused");
|
||||
for (const comment of [
|
||||
"# server header note",
|
||||
@@ -85,7 +84,6 @@ it("retains comments from dotted REA assignments and nested inline values", () =
|
||||
"toml",
|
||||
source,
|
||||
);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw new Error("TOML removal was refused");
|
||||
expect(result.value).toContain(
|
||||
source.includes("# dotted note")
|
||||
|
||||
@@ -6,7 +6,6 @@ it("preserves field diagnostics for rejected enhanced tool inputs", async () =>
|
||||
const execute = vi.fn();
|
||||
const tools = new EnhancedTools({ execute });
|
||||
const result = await tools.execute("trace_native_values", { procedure: "" });
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("expected an input failure");
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "invalid_request",
|
||||
@@ -18,7 +17,6 @@ it("preserves field diagnostics for rejected enhanced tool inputs", async () =>
|
||||
expect(execute).not.toHaveBeenCalled();
|
||||
|
||||
const sibling = await tools.execute("batch_decompile", { addresses: "bad" });
|
||||
expect(sibling.ok).toBe(false);
|
||||
if (sibling.ok) throw new Error("expected an input failure");
|
||||
expect(projectAnalysisError(sibling.error)).toMatchObject({
|
||||
details: { issues: [{ path: ["addresses"], reason: "invalid_type" }] },
|
||||
|
||||
@@ -444,7 +444,6 @@ describe("EnhancedTools cancellation ownership", () => {
|
||||
controller.signal,
|
||||
);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Pre-cancelled observation was accepted");
|
||||
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
|
||||
@@ -212,7 +212,6 @@ it("projects completed raw capture facts on failure through the caller's selecti
|
||||
record_ordinals: [0],
|
||||
sensitive_values: ["private-marker"],
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Cancellation must remain the primary result");
|
||||
expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
expect(result.error.cause).toBe(reason);
|
||||
@@ -270,7 +269,6 @@ it("keeps a redacted cleanup error linked to its live owner through service clos
|
||||
format: "har",
|
||||
sensitive_values: ["private-marker"],
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Retained cleanup must remain a failure");
|
||||
const returnedError = result.error;
|
||||
expect(returnedError.cleanupIncomplete).toBe(true);
|
||||
|
||||
@@ -703,7 +703,6 @@ it("reports a retained workflow and the committed prefix when Unknown publicatio
|
||||
? err(failure)
|
||||
: records.recordUnknown(input),
|
||||
});
|
||||
expect(outcome.ok).toBe(false);
|
||||
if (outcome.ok) throw new Error("expected failure");
|
||||
const projected = projectAnalysisError(outcome.error);
|
||||
expect(projected.details?.workflow_unknown_publication).toEqual({
|
||||
|
||||
@@ -53,7 +53,6 @@ it.each([
|
||||
it("rejects an unknown operation before any effect", async () => {
|
||||
const service = new AdbDeviceAnalysisService(refusedPort);
|
||||
const result = await service.execute("unknown_adb_operation" as never, {});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected an unknown operation refusal");
|
||||
expect(result.error).toBeInstanceOf(AnalysisInputError);
|
||||
if (!(result.error instanceof AnalysisInputError)) throw result.error;
|
||||
|
||||
@@ -211,7 +211,6 @@ it("keeps native JSON source excerpts internal while preserving the failure caus
|
||||
const resources = new ArtifactResourceScope();
|
||||
try {
|
||||
const result = await inspectApiContractEvidence({ path }, resources);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected a syntax refusal");
|
||||
expect(result.error.cause).toBeInstanceOf(SyntaxError);
|
||||
expect(JSON.stringify(result.error)).not.toContain("u:p@h");
|
||||
@@ -257,7 +256,6 @@ it.each([
|
||||
});
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(controller.signal.aborted).toBe(true);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected the parser failure");
|
||||
expect(result.error._tag).toBe(tag);
|
||||
expect(result.error.partialObservation).toEqual({
|
||||
|
||||
@@ -84,7 +84,6 @@ it.each(["path", "operations_path"] as const)(
|
||||
);
|
||||
await withScope(async (resources) => {
|
||||
const result = await inspectApiContractEvidence(input, resources);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected invalid UTF-8");
|
||||
expect(result.error._tag).toBe("AnalysisInputError");
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
@@ -148,7 +147,6 @@ it("attributes auxiliary file admission failures to operations_path and retains
|
||||
{ ...input, operations_path: join(input.path, "missing") },
|
||||
resources,
|
||||
);
|
||||
expect(missing.ok).toBe(false);
|
||||
if (missing.ok) throw new Error("Expected missing operation file");
|
||||
expect(projectAnalysisError(missing.error)).toMatchObject({
|
||||
details: { issues: [{ path: ["operations_path"] }] },
|
||||
|
||||
@@ -394,7 +394,6 @@ it.each([false, true])(
|
||||
releaseClose();
|
||||
|
||||
const result = await opening;
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected failed resource switch");
|
||||
expect(result.error.cleanupPrimary).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(result.error.cleanupPrimary?.cause).toBe(reason);
|
||||
@@ -480,7 +479,6 @@ it("keeps an earlier resource close failure primary when a later cleanup is canc
|
||||
releaseCandidateClose();
|
||||
|
||||
const result = await opening;
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected failed resource switch");
|
||||
expect(result.error).toBe(originalFailure);
|
||||
expect(result.error.cleanupPrimary).toBeUndefined();
|
||||
@@ -569,7 +567,6 @@ it("keeps cancellation primary when binary retirement fails before opening resou
|
||||
releaseClose();
|
||||
|
||||
const result = await opening;
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected failed resource open");
|
||||
expect(result.error.cleanupPrimary).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(result.error.cleanupPrimary?.cause).toBe(reason);
|
||||
@@ -661,7 +658,6 @@ it("keeps an earlier resource close failure primary when binary candidate cleanu
|
||||
releaseCandidateClose();
|
||||
|
||||
const result = await opening;
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected failed binary open");
|
||||
expect(result.selection).toEqual({ kind: "none" });
|
||||
expect(result.error).toBe(originalFailure);
|
||||
@@ -765,7 +761,6 @@ it("keeps an earlier binary close failure primary when candidate cleanup is canc
|
||||
releaseCandidateClose();
|
||||
|
||||
const result = await opening;
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected failed resource open");
|
||||
expect(result.error).toBe(originalFailure);
|
||||
expect(result.error.cleanupPrimary).toBeUndefined();
|
||||
@@ -866,7 +861,6 @@ it("cancels a binary switch during resource retirement and retains null-subject
|
||||
releaseResourceClose();
|
||||
|
||||
const result = await opening;
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancelled binary open");
|
||||
expect(result.error.cleanupPrimary).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(result.error.cleanupPrimary?.cause).toBe(reason);
|
||||
|
||||
@@ -97,7 +97,6 @@ it("reports the first mixed child failure before awaiting the other owner and re
|
||||
await Promise.all([nativeStarted, failureObserved]);
|
||||
releaseNativeClose();
|
||||
const first = await closing;
|
||||
expect(first.ok).toBe(false);
|
||||
if (first.ok) throw new Error("Expected mixed close failure");
|
||||
expect(first.error.cleanupIncomplete).toBe(true);
|
||||
|
||||
|
||||
@@ -113,7 +113,6 @@ it("retains every failed child resource across composite cleanup retries", async
|
||||
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
const closed = await client.close();
|
||||
expect(closed.ok).toBe(false);
|
||||
if (closed.ok) throw new Error("Expected composite cleanup failure");
|
||||
expect(closed.error).toMatchObject({
|
||||
cleanupIncomplete: true,
|
||||
@@ -158,7 +157,6 @@ it("preserves one child's typed cleanup error unchanged", async () => {
|
||||
|
||||
const closed = await client.close();
|
||||
|
||||
expect(closed.ok).toBe(false);
|
||||
if (closed.ok) throw new Error("Expected child cleanup failure");
|
||||
expect(closed.error).toBe(child.cleanupError);
|
||||
});
|
||||
@@ -306,7 +304,6 @@ it.each([1, 2])(
|
||||
release();
|
||||
}
|
||||
const first = await closing;
|
||||
expect(first.ok).toBe(false);
|
||||
if (first.ok) throw new Error("Expected retained child cleanup");
|
||||
expect(notifications).toEqual([
|
||||
...children.map((child) => child.cleanupError),
|
||||
@@ -318,7 +315,6 @@ it.each([1, 2])(
|
||||
|
||||
notifications.length = 0;
|
||||
const retried = await closeAnalysisClient(client, "composite", options);
|
||||
expect(retried.ok).toBe(false);
|
||||
if (retried.ok) throw new Error("Expected failed cleanup retry");
|
||||
expect(notifications).toEqual([
|
||||
...children.map((child) => child.cleanupError),
|
||||
|
||||
@@ -126,7 +126,6 @@ it.each(["288", "309 "])(
|
||||
identity,
|
||||
inspect: () => Promise.resolve(ok(malformed)),
|
||||
}).inspect({ path: report.artifact.path });
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected unsupported provider crash kind");
|
||||
expect(result.error._tag).toBe("AnalysisOutputError");
|
||||
},
|
||||
|
||||
@@ -169,7 +169,6 @@ it.each([
|
||||
},
|
||||
});
|
||||
const result = await service.inspect(null, { signal: controller.signal });
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("expected cancelled admission");
|
||||
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
|
||||
@@ -90,7 +90,6 @@ it("retains provider metadata in a late-cancellation observation", async () => {
|
||||
|
||||
const result = await service.inspect(input, { signal: controller.signal });
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected late cancellation");
|
||||
expect(result.error).toMatchObject({ _tag: "AnalysisCancelledError" });
|
||||
expect(result.error.cause).toBe(reason);
|
||||
|
||||
@@ -254,7 +254,6 @@ it.each([
|
||||
},
|
||||
});
|
||||
const result = await service.inspect(null, { signal: controller.signal });
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("expected cancelled admission");
|
||||
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
|
||||
@@ -171,7 +171,6 @@ it("preserves source Evidence and requested output choices when publication fail
|
||||
path: "/output/seeds.tsv",
|
||||
base: "0x2000",
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Failed publication accepted");
|
||||
expect(result.error).toBe(failure);
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
@@ -213,7 +212,6 @@ it("retains a published seed receipt if cancellation arrives after the atomic wr
|
||||
{ path: receipt.path },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Cancellation ignored");
|
||||
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
|
||||
@@ -449,7 +449,6 @@ describe("residual unknown registry resolution and history", () => {
|
||||
).toBe(true);
|
||||
const before = serializeEvidenceBundle(base.export());
|
||||
const conflict = base.import(branch.export());
|
||||
expect(conflict.ok).toBe(false);
|
||||
if (conflict.ok) throw new Error("Expected divergent history refusal");
|
||||
expectHistoryConflict(conflict.error, created.value.unknown_id);
|
||||
expect(base.get(source.evidence_id)?.caller_input_path).toBe("./original");
|
||||
|
||||
@@ -32,7 +32,6 @@ it.each([
|
||||
withEvidenceSources(cancelled ? err(primary) : ok("complete"), [source]),
|
||||
sourceOwner === undefined ? undefined : { sourceOwner },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected recording failure");
|
||||
expect(result.error).toBe(cancelled ? primary : failure);
|
||||
expect(primary.secondaryFailures).toEqual(cancelled ? [failure] : []);
|
||||
|
||||
@@ -75,7 +75,6 @@ it("preserves failed Inspector cleanup when cancellation occurs during observati
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected failed cleanup");
|
||||
expect(result.error).toBe(failure);
|
||||
expect(result.error.cleanupIncomplete).toBe(true);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { expect, it } from "vitest";
|
||||
import { graphForJavaScript as graphFor } from "../../../tests/fixtures/javascriptSemanticGraph.js";
|
||||
import { createJavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js";
|
||||
import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js";
|
||||
import { resolveJavaScriptSemanticEvidence } from "../../domain/javascript/javascriptSemanticGraph.js";
|
||||
import { resolveJavaScriptSemanticEvidence } from "../../../tests/support/javascriptSemanticGraphFixture.js";
|
||||
|
||||
const SHA256 = "a".repeat(64);
|
||||
const GRAPH_ID = `jag_${"b".repeat(64)}`;
|
||||
|
||||
@@ -74,7 +74,6 @@ it("retains transitive provenance without treating dependencies as native observ
|
||||
undefined,
|
||||
(id) => (id === dependency.evidence_id ? dependency : undefined),
|
||||
);
|
||||
expect(missing.ok).toBe(false);
|
||||
if (missing.ok) throw new Error("Missing ancestor accepted");
|
||||
expect(missing.error).toMatchObject({
|
||||
evidenceId: ancestor.evidence_id,
|
||||
@@ -933,7 +932,6 @@ it("enforces the caller-resolved representation allowance and names its setting"
|
||||
const lowered = await matchNativeFunctions(small, {
|
||||
representationAllowance: { maximumBytes: 64 * 1024, source: "runtime" },
|
||||
});
|
||||
expect(lowered.ok).toBe(false);
|
||||
if (lowered.ok) throw new Error("Lowered allowance admitted the request");
|
||||
expect(lowered.error).toMatchObject({
|
||||
_tag: "AnalysisResourceConstraintError",
|
||||
|
||||
@@ -164,7 +164,6 @@ describe("native value trace seed admission", () => {
|
||||
},
|
||||
{ procedure: "missing" },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected seed failure");
|
||||
expect(result.error).toBe(failure);
|
||||
expect(attempted).toEqual([["analyze_function", { procedure: "missing" }]]);
|
||||
|
||||
@@ -122,7 +122,6 @@ it("retains completed pyc Evidence and the opaque caller reason on late cancella
|
||||
{ path: report.artifact.path },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
|
||||
@@ -218,7 +218,6 @@ it.each([
|
||||
},
|
||||
});
|
||||
const result = await service.inspect(null, { signal: controller.signal });
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("expected cancelled admission");
|
||||
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
|
||||
@@ -83,7 +83,6 @@ it("retains provider metadata in a late-cancellation observation", async () => {
|
||||
|
||||
const result = await service.decode(input, { signal: controller.signal });
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected late cancellation");
|
||||
expect(result.error).toMatchObject({ _tag: "AnalysisCancelledError" });
|
||||
expect(result.error.cause).toBe(reason);
|
||||
|
||||
@@ -290,7 +290,6 @@ it("refuses two admitted inventories that together exceed the comparison cap", a
|
||||
right: evidence(right),
|
||||
scope: "both",
|
||||
});
|
||||
expect(combined.ok).toBe(false);
|
||||
if (combined.ok) throw new Error("Expected comparison allowance refusal");
|
||||
const projected = projectAnalysisError(combined.error);
|
||||
expect(projected.retryable).toBe(false);
|
||||
|
||||
@@ -84,7 +84,7 @@ it("stops a close hook even when retained cleanup is still failing", async () =>
|
||||
(cause: unknown) => cause,
|
||||
);
|
||||
expect(closeFailure).toMatchObject({
|
||||
message: "Artifact cleanup remains incomplete: stuck lease, close hook",
|
||||
message: expect.stringContaining("stuck lease, close hook"),
|
||||
failedOwners: [owner],
|
||||
cleanup: {
|
||||
reason: "still owned; hook still stopping",
|
||||
@@ -305,7 +305,7 @@ it("retains the same failed reader owner in a retry failure and retries before a
|
||||
|
||||
await expect(scope.run(async () => "must not run")).rejects.toMatchObject({
|
||||
reason: "unavailable",
|
||||
message: "Artifact cleanup remains incomplete: fixture reader",
|
||||
message: expect.stringContaining("fixture reader"),
|
||||
failedOwners: [owner],
|
||||
});
|
||||
expect(closeAttempts).toBe(2);
|
||||
|
||||
@@ -72,7 +72,6 @@ it.skipIf(process.platform === "win32")(
|
||||
max_entries: 32,
|
||||
max_probe_bytes: 4096,
|
||||
});
|
||||
expect(invalid.ok).toBe(false);
|
||||
if (invalid.ok) throw new Error("expected ENOTDIR refusal");
|
||||
expect(projectAnalysisError(invalid.error)).toMatchObject({
|
||||
code: "target_unavailable",
|
||||
@@ -83,7 +82,6 @@ it.skipIf(process.platform === "win32")(
|
||||
max_entries: 32,
|
||||
max_probe_bytes: 4096,
|
||||
});
|
||||
expect(valid.ok).toBe(true);
|
||||
if (!valid.ok) throw valid.error;
|
||||
expect(
|
||||
desktopApplicationSchema
|
||||
|
||||
@@ -295,7 +295,7 @@ it("fails /images followed by a malformed structure, and leaves a plain DTB a fi
|
||||
scanCanonicalArtifactInventory(await writeFit("broken.itb", bytes)),
|
||||
).rejects.toMatchObject({
|
||||
reason: "format",
|
||||
message: "FIT is malformed: Unknown FDT token 10",
|
||||
message: expect.stringMatching(/FDT.*token 10/iu),
|
||||
});
|
||||
const plain = await scanCanonicalArtifactInventory(
|
||||
await writeFit("board.dtb", devicetreeFixture()),
|
||||
|
||||
@@ -445,7 +445,7 @@ describe("GodotPckArtifactReader carrier truncation", () => {
|
||||
try {
|
||||
await expect(text(await reader.open(entry))).rejects.toMatchObject({
|
||||
reason: "integrity",
|
||||
message: "Godot PCK entry ended early: member.bin",
|
||||
message: expect.stringContaining("member.bin"),
|
||||
});
|
||||
expect(calls).toBe(2);
|
||||
} finally {
|
||||
|
||||
@@ -308,8 +308,7 @@ it.each([false, true])(
|
||||
if (sea)
|
||||
await expect(located).rejects.toMatchObject({
|
||||
reason: "limit",
|
||||
message:
|
||||
"Node SEA PE inspection exceeds the 512 MiB PE parser input ceiling",
|
||||
message: expect.stringMatching(/PE.*512 MiB/iu),
|
||||
});
|
||||
else await expect(located).resolves.toBeUndefined();
|
||||
} finally {
|
||||
@@ -659,7 +658,7 @@ it("rejects missing Mach-O load commands after a confirmed SEA section", async (
|
||||
scanCanonicalArtifactInventory(await writeCarrier("commands", bytes)),
|
||||
).rejects.toMatchObject({
|
||||
reason: "format",
|
||||
message: "Node SEA Mach-O load commands are truncated",
|
||||
message: expect.stringMatching(/Mach-O.*load commands.*truncated/iu),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -708,7 +707,7 @@ it("reports a member that ends after the carrier recheck as an integrity failure
|
||||
try {
|
||||
await expect(buffer(await reader.open(entry))).rejects.toMatchObject({
|
||||
reason: "integrity",
|
||||
message: "Node SEA entry ended early: sea.js",
|
||||
message: expect.stringContaining("sea.js"),
|
||||
});
|
||||
expect(calls).toBe(2);
|
||||
} finally {
|
||||
|
||||
@@ -558,8 +558,7 @@ it.each(["reported carutil", "tab-indented carutil", "observed assetutil"])(
|
||||
expect(error.cause).toBe(cause);
|
||||
expect(projectAnalysisError(error)).toMatchObject({
|
||||
code: "artifact_operation_failed",
|
||||
message:
|
||||
"Artifact is invalid or has changed. Get a fresh copy and try again.",
|
||||
message: expect.stringMatching(/invalid.*changed.*fresh copy/iu),
|
||||
details: {
|
||||
operation: "inspect_asset_catalog",
|
||||
reason: "format",
|
||||
|
||||
@@ -8,10 +8,8 @@ import { describe, expect, it } from "vitest";
|
||||
import { analyzeInterfaceBuilderBundle } from "./InterfaceBuilderAnalysis.js";
|
||||
import { interfaceBuilderDecodeBudgetFailure } from "./InterfaceBuilderDecodeBudget.js";
|
||||
import { preflightPropertyListDecode } from "./PropertyListDecodeBudget.js";
|
||||
import { unknownDateLimitation } from "../../domain/apple/plistValue.js";
|
||||
import { parseXmlPropertyList } from "../../domain/propertyListKeys.js";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import { scanBinaryPlistScalars } from "./PlistScalars.js";
|
||||
import {
|
||||
readBinaryPlistObjectOffset,
|
||||
readBinaryPlistObjectTableLayout,
|
||||
@@ -181,8 +179,13 @@ it("retains explicit date uncertainty for an overlapping binary plist object", a
|
||||
"com.apple.ibtool.document.classes": {},
|
||||
}),
|
||||
);
|
||||
const date = scanBinaryPlistScalars(bytes).dates[0];
|
||||
if (date === undefined) throw new Error("fixture lost its date");
|
||||
// The fixture date is 978307200 seconds before the 2001 plist epoch.
|
||||
const encodedDate = Buffer.alloc(9);
|
||||
encodedDate[0] = 0x33;
|
||||
encodedDate.writeDoubleBE(-978307200, 1);
|
||||
const dateOffset = bytes.indexOf(encodedDate);
|
||||
expect(dateOffset).toBeGreaterThanOrEqual(0);
|
||||
expect(bytes.indexOf(encodedDate, dateOffset + 1)).toBe(-1);
|
||||
const table = readBinaryPlistObjectTableLayout(bytes);
|
||||
let dataObjectId: number | undefined;
|
||||
for (let objectId = 0; objectId < table.objectCount; objectId += 1) {
|
||||
@@ -192,11 +195,11 @@ it("retains explicit date uncertainty for an overlapping binary plist object", a
|
||||
if (dataObjectId === undefined)
|
||||
throw new Error("fixture lost its empty data");
|
||||
// Keep both objects reachable through a recognized archive while sharing source bytes.
|
||||
bytes.writeDoubleBE(0.0005, date.offset + 1);
|
||||
bytes[date.offset + 2] = 0x40;
|
||||
writeBinaryPlistObjectOffset(bytes, table, dataObjectId, date.offset + 2);
|
||||
bytes.writeDoubleBE(0.0005, dateOffset + 1);
|
||||
bytes[dateOffset + 2] = 0x40;
|
||||
writeBinaryPlistObjectOffset(bytes, table, dataObjectId, dateOffset + 2);
|
||||
expect(readBinaryPlistObjectOffset(bytes, table, dataObjectId)).toBe(
|
||||
date.offset + 2,
|
||||
dateOffset + 2,
|
||||
);
|
||||
expect(
|
||||
preflightPropertyListDecode(bytes, 1024 * 1024, undefined, {
|
||||
@@ -204,7 +207,7 @@ it("retains explicit date uncertainty for an overlapping binary plist object", a
|
||||
}),
|
||||
).toMatchObject({
|
||||
datePrecisionOverlapCount: 1,
|
||||
reachableDateOffsets: [date.offset],
|
||||
reachableDateOffsets: [dateOffset],
|
||||
});
|
||||
await writeFile(join(bundle, "Panel.nib"), bytes);
|
||||
|
||||
@@ -306,8 +309,10 @@ describe("binary Interface Builder plist dates", () => {
|
||||
const offset = submillisecond.indexOf(marker);
|
||||
submillisecond.writeDoubleBE(0.1005, offset + 1);
|
||||
const unknown = await nib(submillisecond, "subms");
|
||||
expect(unknown.limitations).toContain(
|
||||
`Panel.nib: ${unknownDateLimitation(1)}`,
|
||||
expect(unknown.limitations).toContainEqual(
|
||||
expect.stringMatching(
|
||||
/Panel\.nib.*\b1 date value.*decoder could not represent.*timestamps/iu,
|
||||
),
|
||||
);
|
||||
expect(JSON.stringify(unknown)).not.toContain("2001-01-01T00:00:00.100Z");
|
||||
|
||||
|
||||
@@ -95,7 +95,6 @@ it.skipIf(isRoot)(
|
||||
format: "plist",
|
||||
})
|
||||
.execute("inspect_keyed_archive", {});
|
||||
expect(executed.ok).toBe(false);
|
||||
if (executed.ok) throw new Error("expected a denied read");
|
||||
expect(executed.error).toBeInstanceOf(AnalysisAccessDeniedError);
|
||||
expect(projectAnalysisError(executed.error)).toMatchObject({
|
||||
|
||||
@@ -10,13 +10,9 @@ import {
|
||||
decodeKeyedArchiveBytes,
|
||||
inspectBundleKeyedArchive,
|
||||
} from "./KeyedArchiveReader.js";
|
||||
import {
|
||||
PlistNestingLimitError,
|
||||
unknownDateLimitation,
|
||||
} from "../../domain/apple/plistValue.js";
|
||||
import { PlistNestingLimitError } from "../../domain/apple/plistValue.js";
|
||||
import { deeplyNestedXmlPlist } from "../../../tests/fixtures/keyedArchiveXmlNesting.js";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import { scanBinaryPlistScalars } from "./PlistScalars.js";
|
||||
import { embedSubmillisecondDateInWideIntegerPayload } from "../../../tests/fixtures/binaryPlistScalarAlias.js";
|
||||
import {
|
||||
readBinaryPlistObjectOffset,
|
||||
@@ -53,6 +49,17 @@ const binaryArchiveWithDate = (): Buffer =>
|
||||
}),
|
||||
);
|
||||
|
||||
// Locate the known binary date independently of the scanner under test.
|
||||
const fixtureDateOffset = (bytes: Buffer, seconds: number): number => {
|
||||
const encoded = Buffer.alloc(9);
|
||||
encoded[0] = 0x33;
|
||||
encoded.writeDoubleBE(seconds, 1);
|
||||
const offset = bytes.indexOf(encoded);
|
||||
if (offset < 0 || bytes.indexOf(encoded, offset + 1) !== -1)
|
||||
throw new Error("binary archive fixture must contain exactly one date");
|
||||
return offset;
|
||||
};
|
||||
|
||||
const binaryArchiveWithOverlappingDate = (): Buffer => {
|
||||
const bytes = Buffer.from(
|
||||
buildBinary({
|
||||
@@ -65,19 +72,17 @@ const binaryArchiveWithOverlappingDate = (): Buffer => {
|
||||
$top: { root: { UID: 1 } },
|
||||
}),
|
||||
);
|
||||
const date = scanBinaryPlistScalars(bytes).dates[0];
|
||||
if (date === undefined)
|
||||
throw new Error("binary archive fixture lost its date");
|
||||
bytes.writeDoubleBE(0.0005, date.payloadOffset);
|
||||
const dateOffset = fixtureDateOffset(bytes, 0);
|
||||
bytes.writeDoubleBE(0.0005, dateOffset + 1);
|
||||
const objectTable = readBinaryPlistObjectTableLayout(bytes);
|
||||
for (let objectId = 0; objectId < objectTable.objectCount; objectId += 1) {
|
||||
const offset = readBinaryPlistObjectOffset(bytes, objectTable, objectId);
|
||||
if (offset !== date.offset && bytes[offset] === 0x40) {
|
||||
if (offset !== dateOffset && bytes[offset] === 0x40) {
|
||||
writeBinaryPlistObjectOffset(
|
||||
bytes,
|
||||
objectTable,
|
||||
objectId,
|
||||
date.payloadOffset + 1,
|
||||
dateOffset + 2,
|
||||
);
|
||||
return bytes;
|
||||
}
|
||||
@@ -87,14 +92,12 @@ const binaryArchiveWithOverlappingDate = (): Buffer => {
|
||||
|
||||
const binaryArchiveDateCrossingOffsetTable = (): Buffer => {
|
||||
const bytes = Buffer.from(binaryArchiveWithDate());
|
||||
const date = scanBinaryPlistScalars(bytes).dates[0];
|
||||
if (date === undefined)
|
||||
throw new Error("binary archive fixture lost its date");
|
||||
const dateOffset = fixtureDateOffset(bytes, 0.1);
|
||||
const objectTable = readBinaryPlistObjectTableLayout(bytes);
|
||||
let dateObjectId = -1;
|
||||
for (let objectId = 0; objectId < objectTable.objectCount; objectId += 1) {
|
||||
if (
|
||||
readBinaryPlistObjectOffset(bytes, objectTable, objectId) === date.offset
|
||||
readBinaryPlistObjectOffset(bytes, objectTable, objectId) === dateOffset
|
||||
)
|
||||
dateObjectId = objectId;
|
||||
}
|
||||
@@ -585,6 +588,9 @@ const dateScalar = (iso8601: string | null) => ({
|
||||
value: { $plist_type: "date", iso8601 },
|
||||
});
|
||||
|
||||
const oneUnknownDate = /\b1 date value.*could not represent.*timestamps/iu;
|
||||
const twoUnknownDates = /\b2 date value.*could not represent.*timestamps/iu;
|
||||
|
||||
describe("binary keyed archive dates", () => {
|
||||
const selection = { offset: 0, limit: 10 };
|
||||
|
||||
@@ -594,7 +600,11 @@ describe("binary keyed archive dates", () => {
|
||||
expect(graph.objects[1]).toMatchObject(
|
||||
dateScalar("2001-01-01T00:00:00.100Z"),
|
||||
);
|
||||
expect(graph.limitations).not.toContain(unknownDateLimitation(1));
|
||||
expect(graph.limitations).not.toContainEqual(
|
||||
expect.stringMatching(
|
||||
/date value.*decoder could not represent.*timestamps/iu,
|
||||
),
|
||||
);
|
||||
expect(Buffer.compare(bytes, pythonBinaryDateControl)).toBe(0);
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe(
|
||||
"39f08db8341bd74220352dfd559485515cce1e6035e08f27a59ab8f856374a30",
|
||||
@@ -609,7 +619,11 @@ describe("binary keyed archive dates", () => {
|
||||
expect(inspected.objects[1]).toMatchObject(
|
||||
dateScalar("2001-01-01T00:00:00.100Z"),
|
||||
);
|
||||
expect(inspected.limitations).not.toContain(unknownDateLimitation(1));
|
||||
expect(inspected.limitations).not.toContainEqual(
|
||||
expect.stringMatching(
|
||||
/date value.*decoder could not represent.*timestamps/iu,
|
||||
),
|
||||
);
|
||||
expect(
|
||||
Buffer.compare(await readFile(join(root, "archive.plist")), bytes),
|
||||
).toBe(0);
|
||||
@@ -664,7 +678,9 @@ describe("binary keyed archive dates", () => {
|
||||
expect(JSON.stringify(graph.objects[1]?.value)).not.toContain(
|
||||
"2001-01-01T00:00:00.100Z",
|
||||
);
|
||||
expect(graph.limitations).toContain(unknownDateLimitation(1));
|
||||
expect(graph.limitations).toContainEqual(
|
||||
expect.stringMatching(oneUnknownDate),
|
||||
);
|
||||
expect(graph.limitations).not.toContainEqual(
|
||||
expect.stringMatching(/number metadata.*could not.*observed/iu),
|
||||
);
|
||||
@@ -680,7 +696,9 @@ describe("binary keyed archive dates", () => {
|
||||
parameters: { path: "archive.plist" },
|
||||
});
|
||||
expect(inspected.objects[1]).toMatchObject(dateScalar(null));
|
||||
expect(inspected.limitations).toContain(unknownDateLimitation(1));
|
||||
expect(inspected.limitations).toContainEqual(
|
||||
expect.stringMatching(oneUnknownDate),
|
||||
);
|
||||
expect(
|
||||
Buffer.compare(
|
||||
await readFile(join(root, "archive.plist")),
|
||||
@@ -720,7 +738,9 @@ describe("binary keyed archive dates", () => {
|
||||
{ $plist_type: "date", iso8601: null },
|
||||
{ $plist_type: "date", iso8601: null },
|
||||
]);
|
||||
expect(mixedGraph.limitations).toContain(unknownDateLimitation(2));
|
||||
expect(mixedGraph.limitations).toContainEqual(
|
||||
expect.stringMatching(twoUnknownDates),
|
||||
);
|
||||
expect(Buffer.compare(mixed, mixedOriginal)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -352,7 +352,9 @@ describe("Mach-O encryption reader: segment mapping", () => {
|
||||
range: {
|
||||
mapping: {
|
||||
status: "unknown",
|
||||
reason: "load command 0 LC_SEGMENT_64 appears in a 32-bit image",
|
||||
reason: expect.stringMatching(
|
||||
/load command 0\b.*LC_SEGMENT_64.*appears.*32-bit/iu,
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -486,7 +488,9 @@ describe("Mach-O encryption reader: defects are never absent", () => {
|
||||
wide: false,
|
||||
commands: [encryption({ cryptoff: 0, cryptsize: 0, cryptid: 0 })],
|
||||
}),
|
||||
reason: "load command 0 LC_ENCRYPTION_INFO_64 appears in a 32-bit image",
|
||||
reason: expect.stringMatching(
|
||||
/load command 0\b.*LC_ENCRYPTION_INFO_64.*appears.*32-bit/iu,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "an unexpected cmdsize",
|
||||
@@ -496,8 +500,9 @@ describe("Mach-O encryption reader: defects are never absent", () => {
|
||||
encryption({ cryptoff: 0, cryptsize: 0, cryptid: 0 }, true, 32),
|
||||
],
|
||||
}),
|
||||
reason:
|
||||
"load command 0 LC_ENCRYPTION_INFO_64 has cmdsize 32, expected 24",
|
||||
reason: expect.stringMatching(
|
||||
/load command 0\b.*LC_ENCRYPTION_INFO_64.*cmdsize 32\b.*expected 24\b/iu,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "a range beyond the slice",
|
||||
@@ -669,12 +674,15 @@ describe("Mach-O encryption reader: universal files", () => {
|
||||
},
|
||||
{
|
||||
status: "malformed",
|
||||
reason:
|
||||
"FAT architecture 1 CPU type/subtype disagrees with its Mach-O header",
|
||||
reason: expect.stringMatching(
|
||||
/FAT architecture 1\b.*CPU type\/subtype.*disagree.*Mach-O header/iu,
|
||||
),
|
||||
},
|
||||
{
|
||||
status: "malformed",
|
||||
reason: "FAT architecture 2 slice range extends beyond the file",
|
||||
reason: expect.stringMatching(
|
||||
/FAT architecture 2\b.*slice range.*beyond.*file/iu,
|
||||
),
|
||||
},
|
||||
{ status: "encrypted", reason: null },
|
||||
]);
|
||||
@@ -745,7 +753,9 @@ describe("Mach-O encryption reader: universal files", () => {
|
||||
{
|
||||
cpu_type: CPU.arm64.type,
|
||||
status: "malformed",
|
||||
reason: "FAT architecture 1 slice range extends beyond the file",
|
||||
reason: expect.stringMatching(
|
||||
/FAT architecture 1\b.*slice range.*beyond.*file/iu,
|
||||
),
|
||||
commands: [],
|
||||
},
|
||||
],
|
||||
@@ -794,7 +804,9 @@ describe("Mach-O encryption reader: large universal files", () => {
|
||||
{
|
||||
container_file_offset: size,
|
||||
status: "malformed",
|
||||
reason: "FAT architecture 1 slice range extends beyond the file",
|
||||
reason: expect.stringMatching(
|
||||
/FAT architecture 1\b.*slice range.*beyond.*file/iu,
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -802,7 +814,9 @@ describe("Mach-O encryption reader: large universal files", () => {
|
||||
// unresolved JVM identity.
|
||||
expect(await readMachoImage(sparseReadAt, size)).toEqual({
|
||||
status: "malformed",
|
||||
reason: "FAT architecture 1 slice range extends beyond the file",
|
||||
reason: expect.stringMatching(
|
||||
/FAT architecture 1\b.*slice range.*beyond.*file/iu,
|
||||
),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -909,8 +923,9 @@ describe("Mach-O encryption reader: mapping budget", () => {
|
||||
]);
|
||||
expect(facts.slices[1]?.range?.mapping).toEqual({
|
||||
status: "unknown",
|
||||
reason:
|
||||
"mapping needs more than the 5 segment and section records shared by all slices",
|
||||
reason: expect.stringMatching(
|
||||
/mapping.*more than.*5 segment and section records.*slices/iu,
|
||||
),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1889,7 +1889,9 @@ describe("Mach-O load command reader failures", () => {
|
||||
view.setUint32(4, 8, true);
|
||||
expect(await read(machoImage({ commands: [short] }))).toMatchObject({
|
||||
status: "malformed",
|
||||
reason: "load command 0 is too short to hold a string offset",
|
||||
reason: expect.stringMatching(
|
||||
/load command 0\b.*too short.*string offset/iu,
|
||||
),
|
||||
});
|
||||
view.setUint32(0, LC.CODE_SIGNATURE, true);
|
||||
expect(await read(machoImage({ commands: [short] }))).toMatchObject({
|
||||
@@ -1943,7 +1945,9 @@ describe("Mach-O load command reader failures", () => {
|
||||
const fat = fatImage([{ cpu: CPU.arm64, bytes: machoImage({}) }]);
|
||||
expect(await read(fat.subarray(0, 4100))).toMatchObject({
|
||||
status: "malformed",
|
||||
reason: "FAT architecture 0 slice range extends beyond the file",
|
||||
reason: expect.stringMatching(
|
||||
/FAT architecture 0\b.*slice range.*beyond.*file/iu,
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1968,7 +1972,9 @@ describe("Mach-O load command reader defect precedence", () => {
|
||||
new DataView(unsized.buffer).setUint32(0, LC.RPATH, true);
|
||||
const decodeFirst = {
|
||||
status: "malformed",
|
||||
reason: "load command 0 is too short to hold a string offset",
|
||||
reason: expect.stringMatching(
|
||||
/load command 0\b.*too short.*string offset/iu,
|
||||
),
|
||||
};
|
||||
// An earlier command's decode defect wins over a later framing defect,
|
||||
expect(await read(machoImage({ commands: [shortRpath, unsized] }))).toEqual(
|
||||
@@ -2019,7 +2025,9 @@ describe("Mach-O load command reader defect precedence", () => {
|
||||
new DataView(ranged.buffer).setUint32(8 + 20 + 12, ranged.length, false);
|
||||
expect(await read(ranged)).toEqual({
|
||||
status: "unsupported",
|
||||
reason: "FAT architecture 0 alignment exponent 53 is unsupported",
|
||||
reason: expect.stringMatching(
|
||||
/FAT architecture 0\b.*alignment exponent 53\b.*unsupported/iu,
|
||||
),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -540,8 +540,12 @@ describe("binary plist numeric association", () => {
|
||||
9007199254740992, 9007199254740992,
|
||||
]);
|
||||
expect(numbers.limitations()).toEqual([
|
||||
'1 observed integer literal(s) in the original archive exceed the exact range of a JSON number. Decoded numbers may lose integer precision unless represented as { "$plist_type": "integer", "decimal": "<exact digits>" }; the original archive bytes retain those literals.',
|
||||
"2 number(s) beyond the exact integer range remain as decoded because their source integer/real type or reference marker cannot be associated unambiguously; integer precision loss remains possible.",
|
||||
expect.stringMatching(
|
||||
/\b1 observed integer literal.*original archive.*JSON number.*lose integer precision.*\$plist_type.*integer.*decimal.*original archive bytes retain/iu,
|
||||
),
|
||||
expect.stringMatching(
|
||||
/\b2 number.*integer range.*source integer\/real type.*cannot be associated unambiguously.*precision loss/iu,
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ it("refuses an XML plist whose DOM exceeds the native decode budget", () => {
|
||||
const parsed = parsePlistXmlSource(
|
||||
xmlPlist(`<array>\n${"<true/>\n".repeat(100_000)}</array>\n`),
|
||||
);
|
||||
expect(parsed.ok).toBe(false);
|
||||
if (parsed.ok) throw new Error("Expected a decode budget refusal");
|
||||
expect(parsed.error).toBeInstanceOf(AnalysisResourceConstraintError);
|
||||
expect(parsed.error.message).toContain("native plist decode budget");
|
||||
@@ -28,7 +27,6 @@ it.each(["é", "_", ":", "雪", "\u{1d4b3}"])(
|
||||
const parsed = parsePlistXmlSource(
|
||||
xmlPlist(`<array>${`<${name}/>`.repeat(100_000)}</array>`),
|
||||
);
|
||||
expect(parsed.ok).toBe(false);
|
||||
if (parsed.ok) throw new Error("Expected a decode budget refusal");
|
||||
expect(parsed.error).toBeInstanceOf(AnalysisResourceConstraintError);
|
||||
},
|
||||
@@ -38,7 +36,6 @@ it("refuses text whose entity references exceed the decode budget", () => {
|
||||
const parsed = parsePlistXmlSource(
|
||||
xmlPlist(`<array><string>${"&".repeat(1_000_000)}</string></array>`),
|
||||
);
|
||||
expect(parsed.ok).toBe(false);
|
||||
if (parsed.ok) throw new Error("Expected a decode budget refusal");
|
||||
expect(parsed.error).toBeInstanceOf(AnalysisResourceConstraintError);
|
||||
});
|
||||
|
||||
@@ -3,13 +3,13 @@ import { join } from "node:path";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import {
|
||||
locateBunStandaloneGraph,
|
||||
probeBunStandaloneGraph,
|
||||
bunModuleCountLimit,
|
||||
BUN_GRAPH_SUFFIX_BYTES,
|
||||
} from "./BunModuleGraphLayout.js";
|
||||
import { decodeBunModuleGraph } from "./BunModuleGraphMembers.js";
|
||||
import {
|
||||
locateBunGraphFixture,
|
||||
bunModuleGraphPayload,
|
||||
bunPrefixedPayload,
|
||||
machoBunSection,
|
||||
@@ -25,7 +25,7 @@ async function locate(bytes: Buffer) {
|
||||
await writeFile(path, bytes);
|
||||
const handle = await open(path, "r");
|
||||
try {
|
||||
const result = await locateBunStandaloneGraph(handle, bytes.length);
|
||||
const result = await locateBunGraphFixture(handle, bytes.length);
|
||||
if (result.status === "present" && result.graph !== undefined) {
|
||||
const graph = result.graph;
|
||||
return {
|
||||
|
||||
@@ -124,17 +124,6 @@ interface SectionWindow {
|
||||
readonly truncated: boolean;
|
||||
}
|
||||
|
||||
/** Probe, then read the one claimed payload. */
|
||||
export const locateBunStandaloneGraph = async (
|
||||
handle: FileHandle,
|
||||
fileSize: number,
|
||||
signal?: AbortSignal,
|
||||
): Promise<BunStandaloneRecognition> => {
|
||||
const probed = await probeBunStandaloneGraph(handle, fileSize, signal);
|
||||
if (probed.status === "absent" || probed.claim === undefined) return probed;
|
||||
return loadBunStandaloneGraph(handle, probed.claim, signal);
|
||||
};
|
||||
|
||||
/**
|
||||
* Locate at most one graph claim with bounded reads of headers, length
|
||||
* prefixes, and suffixes. Payload data is not read. Multiple real claims are
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
import type { FileHandle } from "node:fs/promises";
|
||||
import {
|
||||
BUN_OFFSETS_BYTES,
|
||||
BUN_RECORD_BYTES_1_3_3,
|
||||
BUN_RECORD_BYTES_1_3_9,
|
||||
BUN_TRAILER,
|
||||
loadBunStandaloneGraph,
|
||||
probeBunStandaloneGraph,
|
||||
type BunLayoutRevision,
|
||||
type BunStandaloneRecognition,
|
||||
type BunStringPointer,
|
||||
} from "./BunModuleGraphLayout.js";
|
||||
|
||||
/** Exercise the same two-stage probe and payload read as the artifact reader. */
|
||||
export const locateBunGraphFixture = async (
|
||||
handle: FileHandle,
|
||||
fileSize: number,
|
||||
): Promise<BunStandaloneRecognition> => {
|
||||
const probed = await probeBunStandaloneGraph(handle, fileSize);
|
||||
if (probed.status === "absent" || probed.claim === undefined) return probed;
|
||||
return loadBunStandaloneGraph(handle, probed.claim);
|
||||
};
|
||||
|
||||
export interface BunFixtureModule {
|
||||
readonly name?: string | Buffer;
|
||||
readonly contents?: Buffer;
|
||||
|
||||
@@ -194,7 +194,6 @@ it.each(["inventory_artifact", "inspect_artifact"] as const)(
|
||||
{},
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected provider cancellation");
|
||||
expect(projectAnalysisError(result.error).code).toBe("cancelled");
|
||||
expect(result.error.cause).toBe(reason);
|
||||
|
||||
@@ -83,7 +83,7 @@ it.each([
|
||||
ok: false,
|
||||
error: {
|
||||
reason: "format",
|
||||
message: "PYZ marshal string is not valid UTF-8",
|
||||
message: expect.stringContaining("UTF-8"),
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -127,7 +127,7 @@ it.each(["6c0200000003000000", "6cfeffffff03000000"])(
|
||||
ok: false,
|
||||
error: {
|
||||
reason: "format",
|
||||
message: "PYZ marshal long is not normalized",
|
||||
message: expect.stringMatching(/long.*not normalized/iu),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -922,7 +922,7 @@ it("retains the descriptor owner when admission cleanup fails", async () => {
|
||||
reader.entries()[Symbol.asyncIterator]().next(),
|
||||
).rejects.toMatchObject({
|
||||
reason: "format",
|
||||
message: `PyInstaller source is not a regular file: ${directory}`,
|
||||
message: expect.stringContaining(directory),
|
||||
cleanup: {
|
||||
reason: closeFailure.message,
|
||||
resources: [directory],
|
||||
|
||||
@@ -311,7 +311,7 @@ it.each(["ENOENT", "ENOTDIR"] as const)(
|
||||
);
|
||||
expect(cause).toMatchObject({
|
||||
reason: "integrity",
|
||||
message: `PyInstaller source identity changed: ${path}`,
|
||||
message: expect.stringContaining(path),
|
||||
});
|
||||
expect(
|
||||
projectAnalysisError(
|
||||
@@ -321,7 +321,7 @@ it.each(["ENOENT", "ENOTDIR"] as const)(
|
||||
category: "integrity_mismatch",
|
||||
details: {
|
||||
reason: "integrity",
|
||||
detail: `PyInstaller source identity changed: ${path}`,
|
||||
detail: expect.stringContaining(path),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -700,7 +700,6 @@ for (const cancel of [false, true]) {
|
||||
release.resolve();
|
||||
const result = await reading;
|
||||
if (failClose || cancel) {
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected read failure");
|
||||
expect(result.cause).toMatchObject(
|
||||
failClose
|
||||
@@ -709,7 +708,6 @@ for (const cancel of [false, true]) {
|
||||
);
|
||||
if (failClose) expect(owner?.handle.fd).toBeGreaterThanOrEqual(0);
|
||||
} else {
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw result.cause;
|
||||
expect(
|
||||
Buffer.compare(result.value.subarray(16), Buffer.from("body")),
|
||||
|
||||
@@ -20,7 +20,7 @@ it("classifies a source replacement during admission as an integrity failure", a
|
||||
await expect(reader.prepareContainer("0".repeat(64))).rejects.toMatchObject(
|
||||
{
|
||||
reason: "integrity",
|
||||
message: `PyInstaller source changed during admission: ${path}`,
|
||||
message: expect.stringContaining(path),
|
||||
},
|
||||
);
|
||||
expect(openRegularFile).toHaveBeenCalledWith(path, {
|
||||
|
||||
@@ -176,7 +176,10 @@ it("reports a truncated table before deferred signature rejection", async () =>
|
||||
header.writeUInt32BE(12, 8);
|
||||
expect(await readImage(header, 16)).toMatchObject({
|
||||
ok: false,
|
||||
error: { reason: "format", message: "PYZ table is truncated" },
|
||||
error: {
|
||||
reason: "format",
|
||||
message: expect.stringMatching(/table.*truncated/iu),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -185,7 +188,10 @@ it("reports the signature after completing a readable table and image hash", asy
|
||||
image.write("BAD\0");
|
||||
expect(await readImage(image)).toMatchObject({
|
||||
ok: false,
|
||||
error: { reason: "format", message: "PYZ archive magic does not match" },
|
||||
error: {
|
||||
reason: "format",
|
||||
message: expect.stringMatching(/magic.*match/iu),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ it("shares a capture limit when browser providers use one admission", async () =
|
||||
const pending = first.analyzeBundle(input);
|
||||
const blocked = await second.analyzeBundle(input);
|
||||
try {
|
||||
expect(blocked.ok).toBe(false);
|
||||
if (blocked.ok) throw new Error("expected the shared admission to refuse");
|
||||
expect(projectAnalysisError(blocked.error)).toMatchObject({
|
||||
code: "resource_constraint",
|
||||
@@ -52,7 +51,6 @@ it("does not share a capture limit across browser providers", async () => {
|
||||
const other = second.analyzeBundle(input);
|
||||
try {
|
||||
const blocked = await other;
|
||||
expect(blocked.ok).toBe(false);
|
||||
if (blocked.ok) throw new Error("expected capture to fail closed");
|
||||
expect(projectAnalysisError(blocked.error).code).not.toBe(
|
||||
"resource_constraint",
|
||||
|
||||
@@ -203,7 +203,6 @@ it.each(
|
||||
target_id: "duplicate",
|
||||
observation_ms: 0,
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected ambiguous target rejection");
|
||||
expect(result.error).toBeInstanceOf(BrowserObservationError);
|
||||
expect(result.error.userMessage).toContain("ambiguous");
|
||||
|
||||
@@ -140,7 +140,6 @@ it("retains Inspector scripts and contexts when final ASAR authorization cannot
|
||||
};
|
||||
try {
|
||||
const control = await observeJavaScriptRuntime(provider, input);
|
||||
expect(control.ok).toBe(true);
|
||||
if (!control.ok) throw control.error;
|
||||
expect(control.value.normalized_result).toMatchObject({
|
||||
scripts: { observed_total: 1 },
|
||||
@@ -149,7 +148,6 @@ it("retains Inspector scripts and contexts when final ASAR authorization cannot
|
||||
await prependAsarJsonWhitespace(archive, " ");
|
||||
handleState.failCloseCount = 1;
|
||||
const result = await observeJavaScriptRuntime(provider, input);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected descriptor close failure");
|
||||
expect(result.error.cleanupIncomplete).toBe(true);
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
|
||||
@@ -63,7 +63,6 @@ it("preserves completed prefix and suffix facts when one script times out", asyn
|
||||
expect(calls).toEqual(
|
||||
captured.scripts.items.map(({ script_key }) => script_key),
|
||||
);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(
|
||||
result.value.observations.chunks.edges.map(({ specifier }) => specifier),
|
||||
@@ -114,7 +113,6 @@ it("marks the unstarted suffix when the aggregate deadline expires", async () =>
|
||||
expect(calls).toEqual(
|
||||
captured.scripts.items.slice(0, 2).map(({ script_key }) => script_key),
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected total deadline failure");
|
||||
expect(result.error._tag).toBe("AnalysisTimeoutError");
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
@@ -176,7 +174,6 @@ it("stops after a middle-script result-capacity failure and retains prefix facts
|
||||
expect(calls).toEqual(
|
||||
captured.scripts.items.slice(0, 2).map(({ script_key }) => script_key),
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected result-capacity failure");
|
||||
expect(result.error).toBe(primary);
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
@@ -234,7 +231,6 @@ it("carries the exact caller cancellation reason and completed prefix facts", as
|
||||
const result = await pending;
|
||||
analyzer.releaseReservation();
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
expect(result.error.cause).toBe(exactReason);
|
||||
@@ -276,7 +272,6 @@ it("retains all completed facts when cancellation arrives during worker close",
|
||||
const result = await pending;
|
||||
analyzer.releaseReservation();
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancellation during finalization");
|
||||
expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
@@ -316,7 +311,6 @@ it("retains cleanup ownership after mixed worker/close failure and resolves it o
|
||||
expect((await analyzer.reserve()).ok).toBe(true);
|
||||
|
||||
const result = await analyzer.analyze(captured, noSourceMaps, input);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected mixed failure");
|
||||
expect(result.error.cleanupIncomplete).toBe(true);
|
||||
expect(result.error.cleanupPrimary).toBe(primary);
|
||||
@@ -347,7 +341,6 @@ it("shares one capture reservation when analyzers use one admission", async () =
|
||||
const second = new IsolatedWebBundleAnalyzer(() => inertWorker(), admission);
|
||||
expect((await first.reserve()).ok).toBe(true);
|
||||
const blocked = await second.reserve();
|
||||
expect(blocked.ok).toBe(false);
|
||||
if (blocked.ok) throw new Error("Expected shared admission refusal");
|
||||
expect(projectAnalysisError(blocked.error)).toMatchObject({
|
||||
code: "resource_constraint",
|
||||
|
||||
@@ -318,7 +318,6 @@ it("retains the partial capture without taskkill when Windows lineage is unavail
|
||||
application_root: dirname(process.execPath),
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected unverified cleanup to fail");
|
||||
expect(terminateTree).not.toHaveBeenCalled();
|
||||
expect(result.error).toMatchObject({
|
||||
@@ -379,7 +378,6 @@ it("retains the initial snapshot when the final state read fails", async () => {
|
||||
application_root: dirname(process.execPath),
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected final state capture to fail");
|
||||
const partialObservation = result.error.partialObservation;
|
||||
expect(partialObservation).toMatchObject({
|
||||
@@ -412,7 +410,6 @@ it("rejects active capture before launch when EnableNodeCliInspectArguments fuse
|
||||
application_root: root,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected capture to fail");
|
||||
expect(launchSpy).not.toHaveBeenCalled();
|
||||
expect(result.error).toMatchObject({
|
||||
@@ -509,7 +506,6 @@ it("cancels a windowless wait action and cleans up the owned application", async
|
||||
expect(application.firstWindow).toHaveBeenCalledOnce();
|
||||
expect(application.close).toHaveBeenCalledOnce();
|
||||
expect(cleanupSpies.some((spy) => spy.mock.calls.length > 0)).toBe(true);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected the capture to be cancelled");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "BrowserObservationError",
|
||||
@@ -621,7 +617,6 @@ it("cancels a pending initial state read and cleans up the owned application", a
|
||||
|
||||
expect(application.close).toHaveBeenCalledOnce();
|
||||
expect(cleanupSpies.some((spy) => spy.mock.calls.length > 0)).toBe(true);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected the capture to be cancelled");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "BrowserObservationError",
|
||||
@@ -666,7 +661,6 @@ it("observes an initial state read started after cancellation and rejected by cl
|
||||
expect(application.close).toHaveBeenCalledOnce();
|
||||
expect(cleanupSpies.some((spy) => spy.mock.calls.length > 0)).toBe(true);
|
||||
expect(unhandled).toEqual([]);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected the capture to be cancelled");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "BrowserObservationError",
|
||||
@@ -703,7 +697,6 @@ it("keeps a failure observed with cancellation as the cancellation cause", async
|
||||
|
||||
const result = await captureWithController(controller, application, []);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected the capture to be cancelled");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "BrowserObservationError",
|
||||
@@ -742,7 +735,6 @@ it("reports an expired final state grace period as the caller's cancellation", a
|
||||
await vi.advanceTimersByTimeAsync(5_000);
|
||||
const result = await capture;
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected the capture to be cancelled");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "BrowserObservationError",
|
||||
@@ -795,7 +787,6 @@ it("cancels a completed capture when cancellation arrives during application clo
|
||||
const result = await captureWithController(controller, application, []);
|
||||
|
||||
expect(application.close).toHaveBeenCalledOnce();
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected the capture to be cancelled");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "BrowserObservationError",
|
||||
@@ -838,7 +829,6 @@ it("cancels a completed capture when cancellation arrives during process cleanup
|
||||
|
||||
const result = await captureWithController(controller, application, []);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected the capture to be cancelled");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "BrowserObservationError",
|
||||
@@ -873,7 +863,6 @@ it("keeps cleanup failure precedence over cancellation observed during cleanup",
|
||||
|
||||
const result = await captureWithController(controller, application, []);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cleanup to fail");
|
||||
expect(result.error).toMatchObject({
|
||||
cleanupIncomplete: true,
|
||||
@@ -925,7 +914,6 @@ it.each(["process", "transport"] as const)(
|
||||
|
||||
const result = await captureWithController(controller, application, []);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected incomplete cleanup");
|
||||
expect(result.error).toMatchObject({
|
||||
cleanupIncomplete: true,
|
||||
|
||||
@@ -1019,7 +1019,6 @@ it("preserves literal replacement characters in valid UTF-8 worker facts", async
|
||||
makeScript("fetch('/literal-�');"),
|
||||
10_000,
|
||||
);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw new Error(result.error.error.message);
|
||||
if (result.value.status !== "analyzed")
|
||||
throw new Error("Expected analyzed worker facts");
|
||||
|
||||
@@ -48,7 +48,6 @@ it("refuses before spawning or reading source when no host result capacity remai
|
||||
|
||||
try {
|
||||
const result = await worker.analyze(script, 10_000);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected result-capacity refusal");
|
||||
const refusal = result.error.error;
|
||||
expect(refusal).toBeInstanceOf(AnalysisResourceConstraintError);
|
||||
|
||||
@@ -144,7 +144,6 @@ it("retains completed facts and resolves a result-file lease through another cle
|
||||
makeScript("fetch('/retained');"),
|
||||
10_000,
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected retained result cleanup failure");
|
||||
expect(result.error.completed).toMatchObject({
|
||||
status: "analyzed",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
import { analysisErrorProjectionSchema } from "../../contracts/errorSchemas.js";
|
||||
import { inspectWebNetworkCaptureInputSchema } from "../../domain/webNetworkCapture.js";
|
||||
import { historicalCaptureFailure } from "./CaptureFailures.js";
|
||||
import { NODE_CAPTURE_PROCESS_MEMORY_LIMIT_BYTES } from "./CaptureRelease.js";
|
||||
import { NODE_CAPTURE_PROCESS_MEMORY_LIMIT_BYTES } from "../../../tests/fixtures/captureDecoderLimits.js";
|
||||
import { redactExplicitFailure } from "../../domain/explicitSensitiveFailure.js";
|
||||
import { OwnedCommandFailure } from "../../process/OwnedCommand.js";
|
||||
|
||||
|
||||
@@ -24,10 +24,4 @@ export const captureDecoderProcessMemoryLimitBytes = (
|
||||
export const NODE_CAPTURE_HEAP_ENFORCED =
|
||||
javaScriptRuntime(process.versions).id === "node";
|
||||
|
||||
/** Commit limit for the default heap. A configured heap uses `captureDecoderProcessMemoryLimitBytes`. */
|
||||
export const NODE_CAPTURE_PROCESS_MEMORY_LIMIT_BYTES =
|
||||
captureDecoderProcessMemoryLimitBytes(
|
||||
NODE_CAPTURE_HEAP_LIMITS.oldGenerationMiB,
|
||||
);
|
||||
|
||||
/** Fixed offline interpretation profile, not an authenticated producer claim. */
|
||||
@@ -217,7 +217,6 @@ it("refuses other declared trace versions with verified archive identity", async
|
||||
const result = await new PlaywrightTraceDecoder(resources).inspect(
|
||||
fixture.path,
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected version refusal");
|
||||
expect(result.error._tag).toBe("AnalysisUnsupportedTargetError");
|
||||
expect(
|
||||
@@ -239,7 +238,6 @@ it("preserves opaque pre-cancellation without reading or claiming a source snaps
|
||||
"/unused.zip",
|
||||
controller.signal,
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
expect(result.error.cause).toBe(reason);
|
||||
@@ -307,7 +305,6 @@ it("refuses oversized physical lines before parsing their representation", async
|
||||
const result = await new PlaywrightTraceDecoder(resources).inspect(
|
||||
fixture.path,
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected line capacity refusal");
|
||||
expect(result.error._tag).toBe("AnalysisResourceConstraintError");
|
||||
expect(resources.hasPending).toBe(false);
|
||||
@@ -329,7 +326,6 @@ it("never promotes facts from a stream whose ZIP CRC fails", async () => {
|
||||
const result = await new PlaywrightTraceDecoder(resources).inspect(
|
||||
fixture.path,
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected CRC refusal");
|
||||
expect(result.error._tag).toBe("AnalysisInputError");
|
||||
const partial = playwrightTraceSchema.parse(
|
||||
@@ -354,7 +350,6 @@ it("rejects a sparse over-budget archive before hashing or expansion", async ()
|
||||
const result = await new PlaywrightTraceDecoder(resources).inspect(
|
||||
fixture.path,
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected archive admission refusal");
|
||||
expect(result.error._tag).toBe("AnalysisResourceConstraintError");
|
||||
expect(result.error.partialObservation).toBeUndefined();
|
||||
@@ -378,7 +373,6 @@ it("bounds JSON node expansion even when many tokens fit inside a small physical
|
||||
const result = await new PlaywrightTraceDecoder(resources).inspect(
|
||||
fixture.path,
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected node admission refusal");
|
||||
expect(result.error._tag).toBe("AnalysisResourceConstraintError");
|
||||
expect(resources.hasPending).toBe(false);
|
||||
|
||||
@@ -26,7 +26,6 @@ it("preserves only fully verified streams when cancellation arrives before the n
|
||||
const result = await new PlaywrightTraceService(
|
||||
new PlaywrightTraceDecoder(resources),
|
||||
).inspect({ path: fixture.path }, controller.signal);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
expect(result.error.cause).toBe(reason);
|
||||
@@ -61,7 +60,6 @@ it("retains complete Evidence and the exact failed reader owner when abort arriv
|
||||
const result = await new PlaywrightTraceService(
|
||||
new PlaywrightTraceDecoder(resources),
|
||||
).inspect({ path: fixture.path }, controller.signal);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected composed failure");
|
||||
expect(result.error.cleanupIncomplete).toBe(true);
|
||||
expect(result.error.cleanupPrimary?._tag).toBe("AnalysisCancelledError");
|
||||
@@ -101,7 +99,6 @@ it("keeps completed Evidence on cancellation during the outer descriptor close",
|
||||
const result = await new PlaywrightTraceService(
|
||||
new PlaywrightTraceDecoder(resources),
|
||||
).inspect({ path: fixture.path }, controller.signal);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
expect(result.error.cause).toBe(reason);
|
||||
@@ -136,7 +133,6 @@ it("keeps an existing unsupported-version failure ahead of cancellation during c
|
||||
fixture.path,
|
||||
controller.signal,
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected version refusal");
|
||||
expect(controller.signal.aborted).toBe(true);
|
||||
expect(result.error._tag).toBe("AnalysisUnsupportedTargetError");
|
||||
@@ -166,7 +162,6 @@ it("retains independent reader and file owners until both cleanup retries comple
|
||||
const result = await new PlaywrightTraceService(
|
||||
new PlaywrightTraceDecoder(resources),
|
||||
).inspect({ path: fixture.path });
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cleanup failure");
|
||||
expect(result.error.cleanupIncomplete).toBe(true);
|
||||
expect(resources.cleanupResources).toHaveLength(2);
|
||||
@@ -216,7 +211,6 @@ it.skipIf(process.platform === "win32")(
|
||||
const result = await new PlaywrightTraceDecoder(resources).inspect(
|
||||
fixture.path,
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected source identity refusal");
|
||||
expect(result.error._tag).toBe("AnalysisArtifactChangedError");
|
||||
expect((await readFile(held)).equals(original)).toBe(true);
|
||||
@@ -248,7 +242,6 @@ it("bounds active imports without an unbounded queue and releases admission afte
|
||||
try {
|
||||
await reached.promise;
|
||||
const second = await port.inspect(fixture.path);
|
||||
expect(second.ok).toBe(false);
|
||||
if (second.ok) throw new Error("Expected capacity refusal");
|
||||
expect(second.error._tag).toBe("AnalysisResourceConstraintError");
|
||||
release.resolve();
|
||||
|
||||
@@ -227,18 +227,6 @@ export const delimitedOutputInputError = (
|
||||
{ path, reason: "invalid_value", message },
|
||||
]);
|
||||
|
||||
/** Render a validated row page, preceded by a versioned metadata record. */
|
||||
export const formatDelimitedRows = (
|
||||
rows: unknown,
|
||||
format: "csv" | "tsv",
|
||||
options: CliDelimitedOutputOptions,
|
||||
): Result<string, AnalysisInputError> => {
|
||||
const prepared = prepareDelimitedOutput(rows, format, options);
|
||||
return prepared.ok
|
||||
? ok([...formatDelimitedOutputParts(prepared.value)].join(""))
|
||||
: prepared;
|
||||
};
|
||||
|
||||
/** Stream a page after its rows and cells have been validated and snapshotted. */
|
||||
export function* formatDelimitedOutputParts({
|
||||
encodedHeaders,
|
||||
|
||||
@@ -295,7 +295,7 @@ const registerXrefsCommand = (
|
||||
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.xrefs, {
|
||||
description: `List bounded references to an analyzed address. ${namedAddressGuidance} ${equalSelectorGuidance}`,
|
||||
description: `List bounded, untyped references to an analyzed address. For typed incoming references, use inspect-native-instruction at the target address where supported. ${namedAddressGuidance} ${equalSelectorGuidance}`,
|
||||
args: z.object({
|
||||
path: localPathStringSchema.describe("App or program path"),
|
||||
address: z
|
||||
|
||||
@@ -51,15 +51,26 @@ export const registerCoreNativeCommands = (
|
||||
): void => {
|
||||
registerAnnotationCommand(cli, logger, runDirectAnalysis);
|
||||
cli.command(CLI_COMMANDS.searchBytes, {
|
||||
description: "Find masked byte patterns in initialized provider memory",
|
||||
description:
|
||||
"Find masked bytes; unbounded searches skip unloaded blocks and overlays",
|
||||
args: z.object({
|
||||
path: localPathStringSchema.describe("Local executable path"),
|
||||
pattern: bytePatternFields.pattern,
|
||||
}),
|
||||
options: z.object({
|
||||
mask: bytePatternFields.mask,
|
||||
start: z.string().optional().describe("Inclusive start address"),
|
||||
end: z.string().optional().describe("Inclusive end address"),
|
||||
start: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Inclusive start address; use an overlay address space to search it",
|
||||
),
|
||||
end: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Inclusive end address; use an overlay address space to search it",
|
||||
),
|
||||
cursor: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -312,7 +323,7 @@ export const registerCoreNativeCommands = (
|
||||
cli.command(command, {
|
||||
description:
|
||||
operation === "inspect_native_instruction"
|
||||
? "Inspect one native instruction with decoded operand facts"
|
||||
? "Inspect an exact code or data address for decoded instructions and typed incoming references where supported"
|
||||
: "Resolve one static native call site",
|
||||
args: z.object({
|
||||
path: localPathStringSchema.describe(
|
||||
|
||||
@@ -38,7 +38,6 @@ it.each([
|
||||
REA_GHIDRA_CHECKPOINT_DIR: "/tmp/checkpoints",
|
||||
[setting]: value,
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("conflicting inputs accepted");
|
||||
expect(result.error.message).toContain("checkpoints");
|
||||
expect(result.error).toMatchObject({
|
||||
@@ -70,7 +69,6 @@ it.each([
|
||||
parseEnvironmentValues(environment),
|
||||
parseConfig(environment),
|
||||
]) {
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected invalid environment");
|
||||
expect(result.error.settings).toHaveLength(1);
|
||||
expect(result.error.settings[0]?.constraint).toContain("NUL");
|
||||
|
||||
@@ -20,7 +20,6 @@ import { parseConfig } from "./parseConfig.js";
|
||||
import {
|
||||
CAPTURE_DECODER_OLD_GENERATION_SETTING,
|
||||
captureDecoderHeapConstraint,
|
||||
captureDecoderHeapRecovery,
|
||||
DEFAULT_CAPTURE_DECODER_HEAP,
|
||||
parseCaptureDecoderHeap,
|
||||
} from "./captureDecoderHeap.js";
|
||||
@@ -97,9 +96,15 @@ it("uses a valid REA_CAPTURE_DECODER_OLD_GENERATION_MIB override", async () => {
|
||||
code: "resource_constraint",
|
||||
details: { resource: "memory" },
|
||||
});
|
||||
expect(projected.remediation.action).toContain(
|
||||
captureDecoderHeapRecovery(configured(256)),
|
||||
expect(projected.remediation.action).toMatch(
|
||||
/REA_CAPTURE_DECODER_OLD_GENERATION_MIB.*256 MiB.*source setting/iu,
|
||||
);
|
||||
expect(projected.remediation.action).toContain("--max-old-space-size=256");
|
||||
expect(projected.remediation.action).toMatch(
|
||||
/floor 8 MiB.*maximum 16384 MiB/iu,
|
||||
);
|
||||
expect(projected.remediation.action).toContain("update_analysis_settings");
|
||||
expect(projected.remediation.action).toMatch(/restart REA/iu);
|
||||
if (process.versions.bun === undefined)
|
||||
expect(projected.details).toMatchObject({
|
||||
limit: {
|
||||
@@ -163,16 +168,19 @@ it("reports the heap the selected adapter launches when decoder environment diff
|
||||
if (!reported.ok)
|
||||
throw new Error(projectAnalysisError(reported.error).message);
|
||||
const limitations = reported.value.limitations.join("\n");
|
||||
expect(limitations).toContain(captureDecoderHeapRecovery(launched.heap));
|
||||
expect(limitations).toMatch(
|
||||
/REA_CAPTURE_DECODER_OLD_GENERATION_MIB.*256 MiB.*source setting/iu,
|
||||
);
|
||||
|
||||
vi.spyOn(launched, "command").mockRejectedValue(heapExhaustion());
|
||||
const exhausted = await decoder.inspect(input);
|
||||
if (exhausted.ok) throw new Error("Heap exhaustion must fail inspection");
|
||||
const projected = projectAnalysisError(exhausted.error);
|
||||
expect(projected.code).toBe("resource_constraint");
|
||||
expect(projected.remediation.action).toContain(
|
||||
captureDecoderHeapRecovery(launched.heap),
|
||||
expect(projected.remediation.action).toMatch(
|
||||
/REA_CAPTURE_DECODER_OLD_GENERATION_MIB.*256 MiB.*source setting/iu,
|
||||
);
|
||||
expect(projected.remediation.action).toContain("--max-old-space-size=256");
|
||||
if (process.versions.bun === undefined)
|
||||
expect(projected.details).toMatchObject({
|
||||
limit: {
|
||||
|
||||
@@ -34,7 +34,6 @@ it.each([
|
||||
"9007199254740992",
|
||||
])("refuses invalid allowance %s without disabling admission", (value) => {
|
||||
const parsed = parseMcpInputBudget(value);
|
||||
expect(parsed.ok).toBe(false);
|
||||
if (parsed.ok) throw new Error("Invalid admission setting was accepted");
|
||||
expect(parsed.error).toBeInstanceOf(ConfigurationError);
|
||||
expect(parsed.error.settings).toEqual([
|
||||
|
||||
@@ -5,10 +5,7 @@ import { KunaProvider } from "../kuna/KunaProvider.js";
|
||||
import { silentLogger } from "../logger.js";
|
||||
import { ConfigurationError } from "../domain/configurationErrors.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import {
|
||||
regexMatchingDeadlineError,
|
||||
launchRegexMatchingLimitation,
|
||||
} from "../process/RegexMatcher.js";
|
||||
import { regexMatchingDeadlineError } from "../process/RegexMatcher.js";
|
||||
import { parseEnvironment } from "./environment.js";
|
||||
import { parseConfig } from "./parseConfig.js";
|
||||
import {
|
||||
@@ -85,12 +82,7 @@ it("uses a valid REA_REGEX_MATCHING_DEADLINE_MS override", () => {
|
||||
.capabilities()
|
||||
.find((capability) => capability.operation === "search_procedures")
|
||||
?.limitations.join(" ");
|
||||
expect(hopperText).toContain(
|
||||
launchRegexMatchingLimitation(
|
||||
parsed.value.regexMatchingDeadline,
|
||||
parsed.value.regexWorkerHeapRequest,
|
||||
),
|
||||
);
|
||||
expect(hopperText).toMatch(/10000 ms matching deadline.*source setting/iu);
|
||||
|
||||
vi.stubEnv(REGEX_MATCHING_DEADLINE_SETTING, "1");
|
||||
try {
|
||||
@@ -99,12 +91,7 @@ it("uses a valid REA_REGEX_MATCHING_DEADLINE_MS override", () => {
|
||||
.capabilities()
|
||||
.find((capability) => capability.operation === "search_strings")
|
||||
?.limitations.join(" ");
|
||||
expect(kunaText).toContain(
|
||||
launchRegexMatchingLimitation(
|
||||
parsed.value.regexMatchingDeadline,
|
||||
parsed.value.regexWorkerHeapRequest,
|
||||
),
|
||||
);
|
||||
expect(kunaText).toMatch(/10000 ms matching deadline.*source setting/iu);
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
}
|
||||
@@ -115,7 +102,9 @@ it("uses a valid REA_REGEX_MATCHING_DEADLINE_MS override", () => {
|
||||
.capabilities()
|
||||
.find((capability) => capability.operation === "search_procedures")
|
||||
?.limitations,
|
||||
).toContain(launchRegexMatchingLimitation());
|
||||
).toContainEqual(
|
||||
expect.stringMatching(/5000 ms matching deadline.*source default/iu),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -5,10 +5,7 @@ import { KunaProvider } from "../kuna/KunaProvider.js";
|
||||
import { silentLogger } from "../logger.js";
|
||||
import { ConfigurationError } from "../domain/configurationErrors.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import {
|
||||
regexWorkerHeapError,
|
||||
launchRegexMatchingLimitation,
|
||||
} from "../process/RegexMatcher.js";
|
||||
import { regexWorkerHeapError } from "../process/RegexMatcher.js";
|
||||
import { parseEnvironment } from "./environment.js";
|
||||
import { parseConfig } from "./parseConfig.js";
|
||||
import {
|
||||
@@ -99,12 +96,7 @@ it("uses a valid REA_REGEX_WORKER_HEAP_REQUEST_MIB override", () => {
|
||||
.capabilities()
|
||||
.find((capability) => capability.operation === "search_procedures")
|
||||
?.limitations.join(" ");
|
||||
expect(hopperText).toContain(
|
||||
launchRegexMatchingLimitation(
|
||||
parsed.value.regexMatchingDeadline,
|
||||
parsed.value.regexWorkerHeapRequest,
|
||||
),
|
||||
);
|
||||
expect(hopperText).toMatch(/256 MiB worker heap request.*source setting/iu);
|
||||
|
||||
vi.stubEnv(REGEX_WORKER_HEAP_REQUEST_SETTING, "8");
|
||||
try {
|
||||
@@ -115,12 +107,7 @@ it("uses a valid REA_REGEX_WORKER_HEAP_REQUEST_MIB override", () => {
|
||||
.capabilities()
|
||||
.find((capability) => capability.operation === "search_strings")
|
||||
?.limitations.join(" ");
|
||||
expect(kunaText).toContain(
|
||||
launchRegexMatchingLimitation(
|
||||
parsed.value.regexMatchingDeadline,
|
||||
parsed.value.regexWorkerHeapRequest,
|
||||
),
|
||||
);
|
||||
expect(kunaText).toMatch(/256 MiB worker heap request.*source setting/iu);
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
}
|
||||
@@ -131,7 +118,9 @@ it("uses a valid REA_REGEX_WORKER_HEAP_REQUEST_MIB override", () => {
|
||||
.capabilities()
|
||||
.find((capability) => capability.operation === "search_procedures")
|
||||
?.limitations,
|
||||
).toContain(launchRegexMatchingLimitation());
|
||||
).toContainEqual(
|
||||
expect.stringMatching(/128 MiB worker heap request.*source default/iu),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -4,12 +4,10 @@ import { ConfigurationError } from "../domain/configurationErrors.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { parseEnvironment } from "./environment.js";
|
||||
import {
|
||||
DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_LIMITS,
|
||||
WINDOWS_CHANGE_TIME_DRIFT_MS,
|
||||
WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
|
||||
WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
|
||||
resolveWindowsChangeTimeSettle,
|
||||
windowsChangeTimeSettleRecovery,
|
||||
windowsChangeTimeSettingFailure,
|
||||
windowsChangeTimeSettingMessage,
|
||||
} from "./windowsChangeTimeSettle.js";
|
||||
@@ -37,7 +35,14 @@ const requiredFacts = (
|
||||
it("keeps today's settle defaults when the settings are unset", () => {
|
||||
expect(resolveWindowsChangeTimeSettle({})).toEqual({
|
||||
ok: true,
|
||||
value: DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_LIMITS,
|
||||
value: {
|
||||
driftMs: 2_000,
|
||||
driftSource: "default",
|
||||
pollMs: 25,
|
||||
pollSource: "default",
|
||||
maxPolls: 40,
|
||||
maxPollsSource: "default",
|
||||
},
|
||||
});
|
||||
const startup = parseEnvironment({});
|
||||
if (!startup.ok) throw new Error("unset settle settings were rejected");
|
||||
@@ -46,17 +51,6 @@ it("keeps today's settle defaults when the settings are unset", () => {
|
||||
expect(
|
||||
startup.value.REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
|
||||
).toBeUndefined();
|
||||
requiredFacts(
|
||||
windowsChangeTimeSettleRecovery(DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_LIMITS),
|
||||
{
|
||||
setting: WINDOWS_CHANGE_TIME_DRIFT_MS.setting,
|
||||
currentValue: "2000",
|
||||
source: "default",
|
||||
defaultValue: 2_000,
|
||||
floor: 1,
|
||||
maximum: 60_000,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("uses valid settle overrides", () => {
|
||||
|
||||
@@ -68,16 +68,6 @@ export interface WindowsChangeTimeSettleLimits {
|
||||
readonly maxPollsSource: "default" | "setting";
|
||||
}
|
||||
|
||||
export const DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_LIMITS: WindowsChangeTimeSettleLimits =
|
||||
{
|
||||
driftMs: WINDOWS_CHANGE_TIME_DRIFT_MS.defaultValue,
|
||||
driftSource: "default",
|
||||
pollMs: WINDOWS_CHANGE_TIME_SETTLE_POLL_MS.defaultValue,
|
||||
pollSource: "default",
|
||||
maxPolls: WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS.defaultValue,
|
||||
maxPollsSource: "default",
|
||||
};
|
||||
|
||||
export const windowsChangeTimeSettingFailure = (
|
||||
bound: WindowsChangeTimeBound,
|
||||
currentValue: string,
|
||||
@@ -183,17 +173,3 @@ export const resolveWindowsChangeTimeSettle = (
|
||||
maxPollsSource: maxPolls === undefined ? "default" : "setting",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Describe the effective budget. Admission still returns the last observation
|
||||
* when the poll budget runs out; these settings only change how long it waits.
|
||||
*/
|
||||
export const windowsChangeTimeSettleRecovery = (
|
||||
limits: WindowsChangeTimeSettleLimits,
|
||||
): string =>
|
||||
[
|
||||
`The effective ${WINDOWS_CHANGE_TIME_DRIFT_MS.setting} value is ${String(limits.driftMs)} ms (source ${limits.driftSource}; default ${String(WINDOWS_CHANGE_TIME_DRIFT_MS.defaultValue)} ms, floor ${String(WINDOWS_CHANGE_TIME_DRIFT_MS.floor)} ms, maximum ${String(WINDOWS_CHANGE_TIME_DRIFT_MS.maximum)} ms).`,
|
||||
`The effective ${WINDOWS_CHANGE_TIME_SETTLE_POLL_MS.setting} value is ${String(limits.pollMs)} ms (source ${limits.pollSource}; default ${String(WINDOWS_CHANGE_TIME_SETTLE_POLL_MS.defaultValue)} ms, floor ${String(WINDOWS_CHANGE_TIME_SETTLE_POLL_MS.floor)} ms, maximum ${String(WINDOWS_CHANGE_TIME_SETTLE_POLL_MS.maximum)} ms).`,
|
||||
`The effective ${WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS.setting} value is ${String(limits.maxPolls)} polls (source ${limits.maxPollsSource}; default ${String(WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS.defaultValue)} polls, floor ${String(WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS.floor)} polls, maximum ${String(WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS.maximum)} polls).`,
|
||||
"Set them in the environment that starts REA, then restart REA. They do not change Windows ChangeTime itself.",
|
||||
].join(" ");
|
||||
@@ -11,7 +11,7 @@ export const HOPPER_MEMORY_TOOL_DEFINITIONS = [
|
||||
{
|
||||
name: "search_bytes",
|
||||
description:
|
||||
"Find masked byte signatures in initialized analyzed memory. Hex byte pairs and ?? wildcards are supported; an optional equal-length hex mask selects bits. Bounds are inclusive. Matches are ordered by provider address and may span adjacent eligible initialized blocks; gaps and excluded blocks end a contiguous range. A match’s segment and file offset describe its starting address. Coverage describes this page; reuse the same query with next_cursor to continue. Uninitialized blocks are listed separately. Ghidra only; Hopper is unavailable.",
|
||||
"Find masked byte signatures in initialized analyzed memory. Hex byte pairs and ?? wildcards are supported; an optional equal-length hex mask selects bits. Bounds are inclusive. Without bounds, unloaded blocks and loaded overlays are skipped and listed separately; select an overlay address space with start or end to search it. Matches are ordered by provider address and may span adjacent eligible initialized blocks; gaps and excluded blocks end a contiguous range. A match’s segment and file offset describe its starting address. Coverage describes this page; reuse the same query with next_cursor to continue. Uninitialized blocks are listed separately. Ghidra only; Hopper is unavailable.",
|
||||
inputSchema: z
|
||||
.strictObject({
|
||||
...bytePatternFields,
|
||||
|
||||
@@ -231,13 +231,13 @@ export const OFFICIAL_TOOL_CONTRACTS = [
|
||||
),
|
||||
official(
|
||||
"search_procedures",
|
||||
`Search every analyzed procedure name, including secondary names exact lookup accepts, using literal matching by default or regex when requested. Each procedure appears once under its primary name; matched_aliases lists matching secondary names. Results are deterministic and complete. For Ghidra case-insensitive literal searches, raw_result contains unfiltered candidates; normalized_result contains matches. ${hopperRegexDeadline} Select detail summary for counts and retained native-inventory pages; complete remains the default.`,
|
||||
`Search every analyzed procedure name, including secondary names exact lookup accepts, using literal matching by default or regex when requested. Each procedure appears once under its primary name; matched_aliases lists matching secondary names. Results are deterministic and complete. Ghidra rows retain their reported external and thunk classification. For Ghidra case-insensitive literal searches, raw_result contains unfiltered candidates; normalized_result contains matches. ${hopperRegexDeadline} Select detail summary for counts and retained native-inventory pages; complete remains the default.`,
|
||||
z.object({ ...nativeSearchInput, detail: analysisDetailSchema }),
|
||||
{ evidenceParameters: "omit-detail" },
|
||||
),
|
||||
official(
|
||||
"search_strings",
|
||||
`Search every provider-reported string object or candidate using literal matching by default or regex when requested. Results are deterministic and complete over that provider inventory, not every text sequence in artifact bytes. Kuna scans mapped sequences of at least five characters, including unterminated fragments, without computing references. Ghidra may omit packed or undefined literals, including Rust strings; an empty result does not prove text is absent. For known encoded bytes, Ghidra search_bytes can find byte matches; a hit does not establish a string or use. If an address is known, xrefs can look for references; read_bytes can inspect bytes where supported (not available with Kuna). Hopper retains typed byte decoding, native display, and explicit decoding unknowns. Adjacent long-literal fragments match independently. For Ghidra case-insensitive literal searches, raw_result contains unfiltered candidates; normalized_result contains matches. ${hopperRegexDeadline} Select detail summary for counts and retained native-inventory pages; complete remains the default.`,
|
||||
`Search every provider-reported string object or candidate using literal matching by default or regex when requested. Results are deterministic and complete over that provider inventory, not every text sequence in artifact bytes. Kuna scans mapped sequences of at least five characters, including unterminated fragments, without computing references. Ghidra reports each defined string's encoding, byte length and termination; it may omit packed or undefined literals, including Rust strings; an empty result does not prove text is absent. For known encoded bytes, Ghidra search_bytes can find byte matches; a hit does not establish a string or use. If an address is known, xrefs can look for references; read_bytes can inspect bytes where supported (not available with Kuna). Hopper retains typed byte decoding, native display, and explicit decoding unknowns. Adjacent long-literal fragments match independently. For Ghidra case-insensitive literal searches, raw_result contains unfiltered candidates; normalized_result contains matches. ${hopperRegexDeadline} Select detail summary for counts and retained native-inventory pages; complete remains the default.`,
|
||||
z.object({ ...nativeSearchInput, detail: analysisDetailSchema }),
|
||||
{ evidenceParameters: "omit-detail" },
|
||||
),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { managedNativeVerificationInputSchema } from "../domain/managed/managedNativeVerificationSchemas.js";
|
||||
import { toolContract } from "./toolContracts.js";
|
||||
import { toolInputSchemaWithMetadata } from "./toolSchemaMetadata.js";
|
||||
|
||||
@@ -133,7 +132,6 @@ describe("workflow input contracts", () => {
|
||||
};
|
||||
const contract = toolContract("verify_managed_native_boundaries");
|
||||
for (const schema of [
|
||||
managedNativeVerificationInputSchema,
|
||||
contract.inputSchema,
|
||||
toolInputSchemaWithMetadata(contract),
|
||||
]) {
|
||||
|
||||
@@ -355,11 +355,11 @@ it("projects a published file receipt alongside retained cleanup failure", () =>
|
||||
);
|
||||
expect(projected).toMatchObject({
|
||||
code: "cleanup_incomplete",
|
||||
message:
|
||||
"The output file was published, but staging cleanup remains incomplete.",
|
||||
message: expect.stringMatching(/published.*staging cleanup.*incomplete/iu),
|
||||
remediation: {
|
||||
action:
|
||||
"Use the published file at details.published_path and resolve the reported staging cleanup failure.",
|
||||
action: expect.stringMatching(
|
||||
/published.*details\.published_path.*cleanup/iu,
|
||||
),
|
||||
},
|
||||
details: {
|
||||
operation: "write",
|
||||
@@ -1070,8 +1070,9 @@ describe("analysis error projection: caller contract", () => {
|
||||
category: "unavailable",
|
||||
retryable: false,
|
||||
remediation: {
|
||||
action:
|
||||
"Check the current process's read access to the selected path. Retry with a readable local file.",
|
||||
action: expect.stringMatching(
|
||||
/read access.*selected path.*readable local file/iu,
|
||||
),
|
||||
},
|
||||
details: {
|
||||
path: "/local/targets/app",
|
||||
@@ -1141,11 +1142,11 @@ describe("analysis error projection: caller contract", () => {
|
||||
projectAnalysisError(new UnknownRegistryError("not-found")),
|
||||
).toMatchObject({
|
||||
code: "execution_failure",
|
||||
message:
|
||||
"The requested residual unknown does not exist in this session. Check the unknown_id and try again.",
|
||||
message: expect.stringMatching(
|
||||
/residual unknown.*does not exist.*unknown_id/iu,
|
||||
),
|
||||
remediation: {
|
||||
action:
|
||||
"Check that the unknown_id belongs to this session, then retry.",
|
||||
action: expect.stringMatching(/unknown_id.*session.*retry/iu),
|
||||
},
|
||||
details: { reason: "not-found" },
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
} from "../javascript/javascriptApplicationGraph.js";
|
||||
import {
|
||||
createJavaScriptSemanticGraph,
|
||||
createImmutableJavaScriptSemanticGraphSteps,
|
||||
sealTransferredJavaScriptSemanticGraphSteps,
|
||||
} from "../javascript/javascriptSemanticGraph.js";
|
||||
import { sourceMapReference } from "../javascript/javascriptApplicationGraph.fixture.js";
|
||||
import { createArtifactInspection } from "../artifactInspection.js";
|
||||
@@ -1093,7 +1093,6 @@ it("rejects ambiguous JavaScript module paths with candidate node ids", () => {
|
||||
collection: "modules",
|
||||
selector: { path: "renderer.js" },
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("expected ambiguous path");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "AnalysisInputError",
|
||||
@@ -1282,7 +1281,7 @@ it("reuses authenticated graphs while rejecting malformed metadata and imported
|
||||
);
|
||||
const { graph_id: _semanticId, ...semantic } = analysis.semantic_graph;
|
||||
const semanticGraph = complete(
|
||||
createImmutableJavaScriptSemanticGraphSteps({
|
||||
sealTransferredJavaScriptSemanticGraphSteps({
|
||||
...semantic,
|
||||
application_graph_id: graph.graph_id,
|
||||
}),
|
||||
|
||||
@@ -787,7 +787,6 @@ it.each(["__proto__", "constructor"])(
|
||||
generator().generate,
|
||||
);
|
||||
for (const result of [created, updated]) {
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
expect(
|
||||
Object.getOwnPropertyDescriptor(result.value, field)?.value,
|
||||
|
||||
@@ -266,7 +266,6 @@ it("returns a typed retained-parent resource error when even a summary identity
|
||||
collection: "summary",
|
||||
}),
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected a resource error");
|
||||
expect(result.error.message).toContain("cannot fit");
|
||||
});
|
||||
@@ -288,7 +287,6 @@ it("explains how to recover an oversized route row without changing the parent",
|
||||
...selection,
|
||||
}),
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected an oversized-row refusal");
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected).toMatchObject({
|
||||
@@ -338,7 +336,6 @@ it.each(["summary", "routes"] as const)(
|
||||
...selection,
|
||||
}),
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected a sealed-page resource error");
|
||||
expect(result.error.message).toContain("cannot fit");
|
||||
},
|
||||
|
||||
@@ -412,7 +412,6 @@ it("returns typed AnalysisResourceConstraintError when a single record exceeds 1
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected resource error");
|
||||
expect(result.error._tag).toBe("AnalysisResourceConstraintError");
|
||||
expect(result.error.message).toContain("byte budget");
|
||||
@@ -492,7 +491,6 @@ it("rejects non-web-bundle parent operation with invalid_value", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected failure");
|
||||
if (!(result.error instanceof AnalysisInputError)) throw result.error;
|
||||
expect(result.error.issues[0]!.message).toContain(
|
||||
|
||||
@@ -232,7 +232,6 @@ it.each(["api-surface", "environment-manifest", "environment-state"])(
|
||||
},
|
||||
analysisViewRequestSchema.parse({ kind }),
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected parent admission to fail");
|
||||
expect(result.error.cause).toMatchObject({ name: "ZodError" });
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
|
||||
@@ -225,7 +225,6 @@ it("retains partial source metadata and source-located neutral diagnostics on sc
|
||||
const result = parse(
|
||||
'type Query { field: "https://name:[email protected]" }',
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected syntax refusal");
|
||||
expect(result.error._tag).toBe("AnalysisInputError");
|
||||
const partial = graphqlContractSchema.parse(result.error.partialObservation);
|
||||
@@ -256,7 +255,6 @@ it("refuses deep ASTs through a typed resource result with retained schema facts
|
||||
"type Query { child: Query }",
|
||||
"{ child ".repeat(90) + "}".repeat(90),
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected nesting admission refusal");
|
||||
expect(result.error._tag).toBe("AnalysisResourceConstraintError");
|
||||
expect(
|
||||
|
||||
Loaded 100 of 296 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user