fix(ci): integrate main and repair inherited static checks

This commit is contained in:
hhang committed 2026-10-30 09:20:41 +08:00
commit afd4d571f6
543 files changed
+39569 -12128

No files matched your search

+1
View File
@@ -107,6 +107,7 @@ export const SCOPES = {
"tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts",
"tests/boundary/cli/nativeSignatureCli.test.ts",
"tests/boundary/cli/nativeMachoArm64_32Cli.test.ts",
"tests/boundary/cli/nativeMachoEncryptionCli.test.ts",
"tests/boundary/mcp/nativeSignatureTargetBindingMcp.test.ts",
"src/process/capture/FilesystemSnapshot.test.ts",
"tests/process-global/filesystemSnapshotFallbackCancellation.test.ts",
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<DebugType>none</DebugType>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.NET.HostModel">
<HintPath>$(MSBuildSDKsPath)/../Microsoft.NET.HostModel.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration><packageSources><clear /></packageSources></configuration>
+92
View File
@@ -0,0 +1,92 @@
using System.Runtime.InteropServices;
using System.Buffers.Binary;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.NET.HostModel.AppHost;
using Microsoft.NET.HostModel.Bundle;
// Trusted fixture generator only. Generated hosts are inspected, never executed.
if (args.Length is < 2 or > 3)
throw new ArgumentException("Expected apphost template, absent output directory, and optional windows/linux/osx target");
var targetOS = (args.ElementAtOrDefault(2) ?? "windows") switch
{
"windows" => OSPlatform.Windows,
"linux" => OSPlatform.Linux,
"osx" => OSPlatform.OSX,
_ => throw new ArgumentException("Target OS must be windows, linux or osx"),
};
var hostName = targetOS == OSPlatform.Windows ? "Probe.exe" : "Probe";
var template = Path.GetFullPath(args[0]);
var output = Path.GetFullPath(args[1]);
if (Directory.Exists(output)) throw new IOException("Output directory must be absent");
Directory.CreateDirectory(output);
var assembly = typeof(Program).Assembly.Location;
var resource = Path.Combine(output, "resource.txt");
File.WriteAllText(resource, string.Concat(Enumerable.Repeat("owned compressed fixture content\n", 4096)));
foreach (var (version, compress) in new[] { (new Version(3, 1), false), (new Version(5, 0), false), (new Version(8, 0), false), (new Version(8, 0), true) })
{
var label = $"net{version.Major}-{(compress ? "compressed" : "plain")}";
var directory = Path.Combine(output, label);
Directory.CreateDirectory(directory);
var host = Path.Combine(directory, targetOS == OSPlatform.Windows ? "template.exe" : "template");
HostWriter.CreateAppHost(template, host, "BundleFixture.dll");
var bundler = new Bundler(hostName, directory,
BundleOptions.BundleOtherFiles | (compress ? BundleOptions.EnableCompression : BundleOptions.None),
targetOS, Architecture.X64, version, appAssemblyName: "BundleFixture");
var inputs = new Dictionary<string, string>
{
["BundleFixture.dll"] = assembly,
["BundleFixture.deps.json"] = Path.ChangeExtension(assembly, ".deps.json"),
["BundleFixture.runtimeconfig.json"] = Path.ChangeExtension(assembly, ".runtimeconfig.json"),
["data/resource.txt"] = resource,
};
var generated = bundler.GenerateBundle(new[] {
new FileSpec(host, hostName),
new FileSpec(assembly, "BundleFixture.dll"),
new FileSpec(Path.ChangeExtension(assembly, ".deps.json"), "BundleFixture.deps.json"),
new FileSpec(Path.ChangeExtension(assembly, ".runtimeconfig.json"), "BundleFixture.runtimeconfig.json"),
new FileSpec(resource, "data/resource.txt"),
});
File.WriteAllText(Path.Combine(directory, "oracle.json"), JsonSerializer.Serialize(Observe(generated, inputs), new JsonSerializerOptions { WriteIndented = true }));
Console.WriteLine(generated);
}
// Independent positive-fixture oracle: .NET's BinaryReader owns its string/number framing.
static object Observe(string path, Dictionary<string, string> inputs)
{
var bytes = File.ReadAllBytes(path);
var signature = Convert.FromHexString("8b1202b96a612038727b930214d7a03213f5b9e6efae3318ee3b2dce24b36aae");
var marker = bytes.AsSpan().IndexOf(signature);
if (marker < 8) throw new InvalidDataException("Generated host has no bundle marker");
var header = BinaryPrimitives.ReadInt64LittleEndian(bytes.AsSpan(marker - 8, 8));
using var stream = new MemoryStream(bytes);
stream.Position = header;
using var reader = new BinaryReader(stream);
var major = reader.ReadUInt32();
var minor = reader.ReadUInt32();
var count = reader.ReadInt32();
var bundleId = reader.ReadString();
if (major >= 2) stream.Position += 40;
var rows = new List<object>();
var found = new HashSet<string>();
for (var index = 0; index < count; index++)
{
var offset = reader.ReadInt64();
var size = reader.ReadInt64();
var compressed = major == 6 ? reader.ReadInt64() : 0;
var kind = reader.ReadByte();
var name = reader.ReadString();
if (!inputs.TryGetValue(name, out var input) || !found.Add(name))
throw new InvalidDataException("Unexpected generated member: " + name);
using var stored = new MemoryStream(bytes, checked((int)offset), checked((int)(compressed == 0 ? size : compressed)));
using var expanded = compressed == 0 ? null : new DeflateStream(stored, CompressionMode.Decompress);
var digest = Convert.ToHexString(SHA256.HashData((Stream)expanded ?? stored)).ToLowerInvariant();
var expected = File.ReadAllBytes(input);
if (expected.LongLength != size || digest != Convert.ToHexString(SHA256.HashData(expected)).ToLowerInvariant())
throw new InvalidDataException("Generated member bytes differ: " + name);
rows.Add(new { name, offset, size, compressed_size = compressed, file_type = kind, sha256 = digest });
}
if (!found.SetEquals(inputs.Keys)) throw new InvalidDataException("Generated bundle omitted an input");
return new { source = "Microsoft.NET.HostModel + independent BinaryReader/DeflateStream", host = path, major, minor, header_offset = header, bundle_id = bundleId, sha256 = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(), files = rows };
}
+5
View File
@@ -0,0 +1,5 @@
#include <stdio.h>
__attribute__((noinline)) int target_fn(int x) { return x * 5; }
__attribute__((noinline)) int call_through(int (*fp)(int), int x) { return fp(x); }
__attribute__((noinline)) int call_direct(int x) { return target_fn(x); }
int main(void) { printf("%d %d\n", call_through(target_fn, 3), call_direct(4)); return 0; }
+5
View File
@@ -38,6 +38,11 @@ export const parseDevelopmentTestRequest = (mode, arguments_) => {
// Test-only outputs are declared beside the test planner rather than becoming
// runtime or typecheck prerequisites. Add a consumer here when it reads one.
const artifactConsumers = {
"artifacts:skills": [
"tests/boundary/cli/guidanceCli.test.ts",
"tests/boundary/guidanceSearch.test.ts",
"tests/boundary/mcp/registerGuidanceTools.test.ts",
],
"artifacts:mcp-catalog": [
"tests/boundary/mcp/deferredRuntime.test.ts",
"tests/boundary/mcp/contractPresentation.test.ts",
+18
View File
@@ -0,0 +1,18 @@
/** One static import or re-export specifier and its source line. */
export interface ImportSpecifierFact {
readonly specifier: string;
readonly line: number;
}
/** Static string specifier on one import or re-export node, when it has one. */
export function importSpecifier(node: {
readonly type: string;
}): string | undefined;
/**
* Static import and re-export specifiers in source order.
* Comments and string literals are not imports.
*/
export function readImportSpecifiers(
source: string,
): readonly ImportSpecifierFact[];
+43
View File
@@ -0,0 +1,43 @@
import { parse } from "@babel/parser";
import { traverseFast } from "@babel/types";
const literalValue = (node) => {
if (node?.type === "StringLiteral") return node.value;
if (node?.type === "TemplateLiteral" && node.expressions.length === 0)
return node.quasis[0]?.value.cooked ?? undefined;
return undefined;
};
/** Static string specifier on one import or re-export node, when it has one. */
export const importSpecifier = (node) => {
switch (node.type) {
case "ImportDeclaration":
case "ExportNamedDeclaration":
case "ExportAllDeclaration":
case "ImportExpression":
case "TSImportType":
return literalValue(node.source);
case "TSImportEqualsDeclaration":
return node.moduleReference.type === "TSExternalModuleReference"
? literalValue(node.moduleReference.expression)
: undefined;
default:
return undefined;
}
};
/**
* Static import and re-export specifiers in source order.
* Comments and string literals are not imports. Non-string dynamic imports
* are omitted. Callers decide which specifiers are in-repo edges.
*/
export const readImportSpecifiers = (source) => {
const specifiers = [];
const tree = parse(source, { sourceType: "module", plugins: ["typescript"] });
traverseFast(tree, (node) => {
const specifier = importSpecifier(node);
if (specifier === undefined) return;
specifiers.push({ specifier, line: node.loc?.start.line ?? 1 });
});
return specifiers;
};
+6 -42
View File
@@ -1,8 +1,8 @@
import { parse } from "@babel/parser";
import { traverseFast } from "@babel/types";
import { isAbsolute, relative, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { readImportSpecifiers } from "./import-specifiers.mjs";
const PURE_LAYERS = new Set(["domain", "contracts"]);
const MIGRATED_PROVIDER_ROOTS = new Set([
"sqlite",
@@ -27,30 +27,6 @@ const MIGRATED_PROVIDER_ROOTS = new Set([
const repositoryPath = (root, path) =>
relative(root, path).replaceAll("\\", "/");
const literalValue = (node) => {
if (node?.type === "StringLiteral") return node.value;
if (node?.type === "TemplateLiteral" && node.expressions.length === 0)
return node.quasis[0]?.value.cooked ?? undefined;
return undefined;
};
const importSpecifier = (node) => {
switch (node.type) {
case "ImportDeclaration":
case "ExportNamedDeclaration":
case "ExportAllDeclaration":
case "ImportExpression":
case "TSImportType":
return literalValue(node.source);
case "TSImportEqualsDeclaration":
return node.moduleReference.type === "TSExternalModuleReference"
? literalValue(node.moduleReference.expression)
: undefined;
default:
return undefined;
}
};
const sourceTarget = (file, specifier) => {
if (specifier.startsWith("file:")) return fileURLToPath(specifier);
if (isAbsolute(specifier)) return resolve(specifier);
@@ -115,26 +91,14 @@ const failedBoundary = (file, target) => {
export const inspectModuleBoundaries = (file, source, root) => {
const absoluteFile = resolve(root, file);
const importer = repositoryPath(root, absoluteFile);
const tree = parse(source, {
sourceType: "module",
plugins: ["typescript"],
});
const violations = [];
traverseFast(tree, (node) => {
const specifier = importSpecifier(node);
if (specifier === undefined) return;
for (const { specifier, line } of readImportSpecifiers(source)) {
const resolved = sourceTarget(absoluteFile, specifier);
if (resolved === undefined) return;
if (resolved === undefined) continue;
const target = repositoryPath(root, resolved);
const boundary = failedBoundary(importer, target);
if (boundary !== undefined)
violations.push({
file: importer,
line: node.loc?.start.line ?? 1,
specifier,
target,
boundary,
});
});
violations.push({ file: importer, line, specifier, target, boundary });
}
return violations;
};
+5 -5
View File
@@ -128,7 +128,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
);
const configurationError = cliConfigurationError(process.env);
let dispatchFailure;
let closeCliJavaScriptResources;
let closeCliResources;
// Incur can exit from the callback below before this function returns.
// That callback owns the status, including a closed-pipe exit of 141.
let exiting = false;
@@ -150,7 +150,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
writeStdout(versionOutput);
} else {
const cliRuntime = await import("../dist/cli-runtime/cli.js");
closeCliJavaScriptResources = cliRuntime.closeCliJavaScriptResources;
closeCliResources = cliRuntime.closeCliResources;
const { createCli } = cliRuntime;
const { createCliResultOutput } =
await import("../dist/cli-runtime/cliResultOutput.js");
@@ -176,10 +176,10 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
void (async () => {
let status = code;
try {
await closeCliJavaScriptResources();
await closeCliResources();
} catch (cleanup) {
process.stderr.write(
`Failed to close the JavaScript analysis worker: ${cleanupFailureText(cleanup)}\n`,
`Failed to close CLI resources: ${cleanupFailureText(cleanup)}\n`,
);
if (status === 0) status = 1;
}
@@ -225,7 +225,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
// Serve and output rendering can throw after a worker is parked. Stop it
// on that path too, and keep the dispatch failure when both fail.
try {
await closeCliJavaScriptResources?.();
await closeCliResources?.();
} catch (cleanup) {
// The exit callback reports this same shared failure and then exits.
if (!exiting) {
+4 -2
View File
@@ -61,8 +61,10 @@ try {
process.execPath,
resolve("scripts/run-turbo.mjs"),
"run",
...(plan.needsBuild ? ["artifacts:skills"] : []),
...plan.artifactTasks,
...new Set([
...(plan.needsBuild ? ["artifacts:skills"] : []),
...plan.artifactTasks,
]),
]);
if (prepared.code !== 0) process.exitCode = prepared.code;
}
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { PrivateRuntimeRoot } from "../dist/process/PrivateRuntimeRoot.js";
import { parseEvidence } from "../dist/domain/evidence.js";
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
import { createVerifierCommands } from "./lib/verify-commands.mjs";
const root = await PrivateRuntimeRoot.create({ prefix: "rea-bun-verifier-" });
const commands = await createVerifierCommands();
const run = (command, args, options = {}) =>
commands.exec(command, args, {
timeout: 120_000,
maxBuffer: 16 * 1024 * 1024,
...options,
});
// The root holds the compiled target and child temporary files. Release it
// only after every owned command family has settled.
let commandsClosed = false;
await withVerifierCleanup(async () => {
const source = join(root.path, "hello.ts");
await writeFile(source, 'console.log("REA_BUN_VERIFICATION");\n');
const docker = process.argv.includes("--docker");
const binary = join(root.path, "hello");
const compiler = docker
? "oven/bun:1.4.0"
: (await run("bun", ["--version"])).stdout.trim();
if (docker) {
await run("docker", [
"run",
"--rm",
"--network=none",
"-v",
`${root.path}:/work`,
"-w",
"/work",
"oven/bun:1.4.0",
"build",
"--compile",
"hello.ts",
"--outfile",
"hello",
]);
} else {
await run("bun", ["build", "--compile", source, "--outfile", binary]);
}
const cli = async (command) =>
JSON.parse(
(
await run(
process.execPath,
["scripts/rea.mjs", command, binary, "--json"],
{
env: {
...process.env,
TMPDIR: root.path,
TMP: root.path,
TEMP: root.path,
},
},
)
).stdout,
);
const inspected = parseEvidence(
await cli("inspect-artifact"),
).normalized_result;
const graph = inspected.substeps.find(
(step) => step.operation === "inventory_artifact",
)?.evidence.normalized_result;
assert.equal(graph.manifest.root_format, "bun-standalone");
const module = graph.occurrences.find(
(row) =>
row.format_metadata?.kind === "bun-standalone-entry" &&
row.format_metadata.region === "contents" &&
row.format_metadata.entry_point,
);
assert.ok(module);
const extracted = parseEvidence(
await cli("extract-artifact"),
).normalized_result;
assert.equal(extracted.containment_verified, true);
assert.match(
await readFile(join(extracted.output_root, module.logical_path), "utf8"),
/REA_BUN_VERIFICATION/u,
);
console.log(
JSON.stringify(
{
compiler,
host: `${process.platform}-${process.arch}`,
docker,
root_format: graph.manifest.root_format,
entry_point: module.logical_path,
sha256: graph.nodes.find(
(node) => node.artifact_id === module.artifact_id,
)?.sha256,
extraction_verified: true,
target_executed: false,
},
null,
2,
),
);
}, [
async () => {
await commands.close();
commandsClosed = true;
},
async () => {
if (commandsClosed) await root.close();
},
]);
+309
View File
@@ -0,0 +1,309 @@
// Run from the repository root after `npm run build:cached` on macOS arm64,
// with Hopper and the Xcode command-line tools installed. No target is executed.
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { constants } from "node:fs";
import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { promisify } from "node:util";
import { artifactMcpCall } from "./lib/artifact-e2e.mjs";
import { snapshotHopperRuntime } from "./lib/real-hopper-cleanup.mjs";
import { HOPPER_TARGET_LEASE_DIRECTORY } from "../dist/process/HopperLease.js";
assert.equal(process.platform, "darwin", "requires macOS");
assert.equal(
process.arch,
"arm64",
"requires macOS arm64 (fixture tail branch is ARM64)",
);
const root = process.cwd();
const require = createRequire(join(root, "package.json"));
const { Client } = await import(
pathToFileURL(require.resolve("@modelcontextprotocol/client"))
);
const { StdioClientTransport } = await import(
pathToFileURL(require.resolve("@modelcontextprotocol/client/stdio"))
);
const { parseConfig } = await import(
pathToFileURL(join(root, "dist/config/parseConfig.js"))
);
const config = parseConfig(process.env);
if (!config.ok) throw config.error;
await access(config.value.hopperLauncherPath, constants.X_OK);
const run = promisify(execFile);
const clang = (await run("xcrun", ["--find", "clang"])).stdout.trim();
const sdk = (
await run("xcrun", ["--sdk", "macosx", "--show-sdk-path"])
).stdout.trim();
const directory = await mkdtemp(join("/tmp", "rea-hopper-callers-verifier-"));
const fixture = join(directory, "tail-call");
const large = join(directory, "long-scan");
const source = fileURLToPath(
new URL("./fixtures/hopper-tail-call.c", import.meta.url),
);
const digest = async (path) =>
createHash("sha256")
.update(await readFile(path))
.digest("hex");
const sessionsBefore = await snapshotHopperRuntime(
"/tmp",
HOPPER_TARGET_LEASE_DIRECTORY,
);
const ownedProcessIds = new Set();
const client = new Client({ name: "hopper-callers-verifier", version: "1" });
const transport = new StdioClientTransport({
command: process.execPath,
args: ["dist/main.js"],
cwd: root,
env: {
...process.env,
REA_ANALYSIS_PROVIDER: "hopper",
REA_LOG_LEVEL: "silent",
REA_HOPPER_CALLER_SCAN_BUDGET_MS: "5000",
},
stderr: "pipe",
});
let stderr = "";
transport.stderr?.on("data", (chunk) => {
stderr += chunk.toString();
});
const call = async (name, args, options = {}) => {
process.stderr.write(`calling ${name}\n`);
const result = await artifactMcpCall(
client,
{ name, arguments: args },
{ timeout: 120000, ...options },
);
assert.notEqual(result.isError, true, `${name}: ${JSON.stringify(result)}`);
return (
result.structuredContent ??
JSON.parse(result.content.find((x) => x.type === "text").text)
);
};
const address = (result) => {
const match = result.normalized_result.match(/^(0x[0-9a-f]+):/mu);
assert(match, "Hopper assembly has no instruction address");
return match[1];
};
const matchingRef = (ref, site, from, to) =>
ref.source_address === site &&
ref.source_procedure?.address === from &&
ref.target_procedure?.address === to &&
ref.call?.classification === "direct";
try {
await run(clang, [
"-isysroot",
sdk,
"-arch",
"arm64",
"-O1",
source,
"-o",
fixture,
]);
const lines = ["#include <stdio.h>"];
for (let i = 0; i < 4000; i++) {
const id = String(i).padStart(4, "0");
lines.push(
`__attribute__((noinline, used)) int rea_progress_${id}(void) { puts("REA_PROGRESS_${id}"); return ${i}; }`,
);
}
lines.push("int main(void) { return rea_progress_0000(); }");
const largeSource = join(directory, "long-scan.c");
await writeFile(largeSource, lines.join("\n"));
await run(clang, [
"-isysroot",
sdk,
"-arch",
"arm64",
"-O1",
largeSource,
"-o",
large,
]);
const before = await digest(fixture),
beforeLarge = await digest(large);
await client.connect(transport);
if (transport.pid === null)
throw new Error("MCP process identity unavailable");
ownedProcessIds.add(transport.pid);
let notifications = 0;
const original = transport.onmessage;
transport.onmessage = (message) => {
if (message.method === "notifications/progress") notifications++;
original?.(message);
};
await call("open_binary", { path: fixture });
const assembly = await call("procedure_assembly", {
procedure: "_call_direct",
});
const branch = assembly.normalized_result.match(
/^(0x[0-9a-f]+):\s+b\s+_target_fn\s*$/mu,
);
assert(
branch,
`compiler did not produce expected tail branch: ${assembly.normalized_result}`,
);
const site = branch[1],
from = address(assembly);
const to = address(
await call("procedure_assembly", { procedure: "_target_fn" }),
);
assert(
(
await call("procedure_callees", { procedure: from })
).normalized_result.includes(to),
);
const callers = await call("procedure_callers", {
procedure: to,
detail: "complete",
});
assert(callers.normalized_result.includes(from), "tail caller missing");
assert.equal(callers.relationship_coverage.status, "unknown");
assert(
callers.raw_result.forward_observations.callee_inventory_callers.some(
(x) => x.address === from,
),
"forward caller provenance missing",
);
const refs = (
await call("procedure_references", { procedure: to, direction: "incoming" })
).normalized_result.references;
assert(
refs.some((x) => matchingRef(x, site, from, to)),
"exact incoming tail reference missing",
);
const dossier = (
await call("analyze_function", { procedure: to, detail: "complete" })
).normalized_result;
assert(
dossier.callers.some((x) => x.address === from),
"dossier caller missing",
);
assert(
dossier.incoming_references.some((x) => matchingRef(x, site, from, to)),
"dossier reference missing",
);
const graph = (
await call("get_call_graph", {
address: to,
direction: "backward",
detail: "complete",
})
).normalized_result.graph;
assert(
Object.values(graph)
.flat()
.some((x) => x.address === to && x.calls.includes(from)),
"backward graph edge missing",
);
const path = (
await call("trace_call_path", {
start: to,
goal: from,
direction: "backward",
})
).normalized_result;
assert.equal(path.goal_status, "reached");
assert(
path.edges.some(
(x) => x.source_address === from && x.target_address === to,
),
);
await call("close_binary", {});
assert.equal(await digest(fixture), before);
console.log(
JSON.stringify({
event: "tail_call_verified",
from,
site,
to,
sha256: before,
}),
);
await call("open_binary", { path: large });
await call("procedure_assembly", { procedure: "_rea_progress_0000" });
const query = {
address: "_rea_progress_0000",
direction: "backward",
detail: "complete",
};
const count = notifications;
await call("get_call_graph", query);
assert.equal(notifications, count, "unsolicited progress without token");
const progress = [];
await call("get_call_graph", query, { onprogress: (x) => progress.push(x) });
assert(
progress.every((x, i) => i === 0 || x.progress > progress[i - 1].progress),
"progress not monotonic",
);
assert(
progress.some((x) =>
x.message?.includes("hopper_caller_reconciliation: Examined"),
),
"missing examined-count progress",
);
const controller = new AbortController();
let partial = false;
let resolved = false;
try {
await artifactMcpCall(
client,
{ name: "get_call_graph", arguments: query },
{
timeout: 120000,
signal: controller.signal,
onprogress: (x) => {
const match = x.message?.match(
/hopper_caller_reconciliation: Examined (\d+) of (\d+) procedures/u,
);
if (match && Number(match[1]) < Number(match[2])) {
partial = true;
controller.abort("cancel during real scan");
}
},
},
);
resolved = true;
} catch (error) {
if (!controller.signal.aborted) throw error;
}
assert(
partial && !resolved,
"did not observe cancellation after partial scan progress; cancellation coverage unavailable on this host",
);
await client.ping();
const retry = await call("get_call_graph", query);
assert(retry.normalized_result.graph, "retry did not return graph");
await call("close_binary", {});
assert.equal(await digest(large), beforeLarge);
console.log(
JSON.stringify({
event: "progress_cancel_retry_verified",
progress,
sha256: beforeLarge,
}),
);
} finally {
await client.close();
await transport.close();
await new Promise((resolve) => setTimeout(resolve, 500));
const leaked = [
...(await snapshotHopperRuntime(
"/tmp",
HOPPER_TARGET_LEASE_DIRECTORY,
ownedProcessIds,
)),
].filter((path) => !sessionsBefore.has(path));
if (leaked.length === 0)
await rm(directory, { recursive: true, force: true });
if (stderr) process.stderr.write(stderr);
assert.deepEqual(
leaked,
[],
`Hopper resources remain: ${leaked.join(", ")}; fixture retained at ${directory}`,
);
}
+72
View File
@@ -10,10 +10,14 @@ import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceS
import { inspectAppleDispatchMetadata } from "../../../dist/native/AppleDispatchMetadata.js";
import {
artifactCliEvidence,
artifactMcpCall,
artifactMcpResult,
withArtifactMcp,
} from "../../lib/artifact-e2e.mjs";
import { nativeDispatchMetadataResultSchema } from "../../../dist/domain/native/objcSwiftMetadata.js";
import { analysisErrorProjectionSchema } from "../../../dist/contracts/errorSchemas.js";
import { cliOperationFailurePayload } from "../../../dist/cliLogging.js";
import { requireMcpToolError } from "../../lib/mcp-verifier-results.mjs";
if (process.platform !== "darwin")
throw new Error(
@@ -344,6 +348,74 @@ try {
commands,
target_sha256: cliResult.target_sha256,
});
for (const { section, size, reason } of [
{
section: "__objc_classlist",
size: 12,
reason: "Misaligned Objective-C class-list size",
},
{
section: "__swift5_types",
size: 6,
reason: "Misaligned Swift type section",
},
]) {
const bytes = Buffer.alloc(4096);
bytes.writeUInt32LE(0xfeedfacf);
bytes.writeUInt32LE(0x0100000c, 4);
bytes.writeUInt32LE(2, 12);
bytes.writeUInt32LE(1, 16);
bytes.writeUInt32LE(152, 20);
bytes.writeUInt32LE(0x19, 32); // LC_SEGMENT_64
bytes.writeUInt32LE(152, 36);
bytes.write("__DATA", 40);
bytes.writeBigUInt64LE(0x100000000n, 56);
bytes.writeBigUInt64LE(4096n, 64);
bytes.writeBigUInt64LE(4096n, 80);
bytes.writeUInt32LE(7, 88);
bytes.writeUInt32LE(5, 92);
bytes.writeUInt32LE(1, 96);
bytes.write(section, 104);
bytes.write("__DATA", 120);
bytes.writeBigUInt64LE(0x100000200n, 136);
bytes.writeBigUInt64LE(BigInt(size), 144);
bytes.writeUInt32LE(512, 152);
const path = join(root, `malformed-${section}.macho`);
await writeFile(path, bytes);
const target = await parseBinaryTarget(path);
assert.ok(
target.ok,
"Malformed section fixture must pass target admission",
);
let cliFailure;
try {
await artifactCliEvidence("inspect-native-dispatch-metadata", path);
assert.fail("Malformed metadata unexpectedly succeeded");
} catch (cause) {
assert.equal(cause.code, 1);
cliFailure = analysisErrorProjectionSchema.parse(
cliOperationFailurePayload(JSON.parse(cause.stdout)),
);
}
assert.equal(cliFailure.code, "unsupported_target");
assert.equal(cliFailure.category, "unsupported_target");
assert.deepEqual(cliFailure.details, {
operation: "inspect_native_dispatch_metadata",
path: target.value.path,
reason,
});
await withArtifactMcp(path, async (client) => {
const failure = requireMcpToolError(
await artifactMcpCall(client, {
name: "inspect_native_dispatch_metadata",
arguments: {},
}),
);
assert.equal(failure.code, cliFailure.code);
assert.deepEqual(failure.details, cliFailure.details);
});
results.push({ variant: `malformed-${section}`, reason });
}
for (const mode of LINK_MODES) {
const objcPath = join(root, `fixture-${mode.mode}`);
await exec("/usr/bin/xcrun", [
@@ -134,6 +134,15 @@ export async function verifyNativeProvenance(app, executable) {
]);
if (operation === "inspect_macho") {
const encryption = cli.normalized_result.encryption;
assert.equal(encryption.exhaustive, true);
assert.ok(encryption.total > 0);
assert.equal(encryption.items.length, encryption.total);
for (const slice of encryption.items) {
assert.equal(slice.status, "absent");
assert.deepEqual(slice.commands, []);
assert.equal(slice.range, null);
}
const delayed = cli.normalized_result.load_commands.items.find(
({ fields }) => fields.name === "@rpath/libdelay.dylib (offset 28)",
);
+3
View File
@@ -156,6 +156,9 @@ check(
files,
/`[^`\n]*\\0/,
[
// Bundle fingerprint: fixed packaged paths (no NUL/newline) and 64 hex
// digest characters per record, never guide contents. Preserve v1 locators.
"src/application/GuidanceAccess.ts",
"src/application/javascript/ElectronBoundaryAnalysis.ts",
"src/application/javascript/ElectronBoundaryGraphContext.ts",
"src/application/javascript/JavaScriptArtifactGraphContext.ts",