fix(reverse): reconcile cleanup ownership and mutation diagnostics (#2799)

* feat(process): add owned debugger shutdown lifecycle

* feat(reverse): add local inspection commands

* feat(gdb): add persistent debugger integration

* feat(cutter): add Cutter bridge integration

* test(reverse): enforce complete integration coverage

* fix(process): preserve debugger shutdown ownership

* fix(reverse): retain evidence across interrupted commands

* fix(gdb): honor captured provider environment

* test(process): cover debugger shutdown retries

* fix(reverse): retain evidence for interrupted providers

* test(process): cover debugger shutdown retries

* fix(reverse): retain evidence for interrupted providers

* fix(server): snapshot provider environment at creation

* fix(reverse): close review gaps with complete coverage

* fix(reverse): address inspection review findings

* fix(gdb): bound sessions and retain cleanup ownership

* fix(ci): require reverse-tools quality lane

* fix(process): unref inherited debugger output pipes

* fix(reverse): address remaining PR 1960 findings

* fix(gdb): address remaining PR 1961 findings

* fix(reverse): account for unrestricted provider execution

* fix(process): retain debugger ownership until safe shutdown

* fix(process): preserve replacement debugger sessions

* fix(reverse): preserve admission and output provenance

* fix(process): retain exited debugger until output settles

* fix(mcp): bound provider admission and debugger cleanup

* fix(reverse): preserve failure provenance and classify E2BIG

* fix(gdb): preserve startup cleanup and detach safety

* fix(process): retain debugger ownership after detach failure

* fix(reverse): preserve mutation and failure provenance

* fix(gdb): address debugger review findings

* fix(reverse): rebind sessions after external mutation

* fix(process): cap debugger output after exit

* fix(gdb): protect one-shot detach verification

* feat(debugger): add reverse tool coverage gate

* test(debugger): cover integrated failure paths

* fix(gdb): bound and preserve session observations

* fix(gdb): address review regressions

* style: format merged integration files

* fix(reverse): preserve session and output evidence

* refactor(gdb): share repeated test and partial result setup

* fix(gdb): invalidate TTY proof for Python aliases

* fix(reverse): retain failure output byte counts

* test(process): synchronize late debugger output

* fix(gdb): invalidate TTY proof for wrapped commands

* fix(gdb): recognize wit command abbreviation

* test(reverse): wait for cancelled descendant readiness

* test(reverse): bound descendant cancellation cleanup

* style(reverse): format integration tests and adapters

* style: format reverse engineering integration files

* test(reverse): remove duplicate environment harness

* test(gdb): share session startup assertions

* fix(gdb): invalidate output proof for command abbreviations

* test(reverse): share composition edge case scenarios

* fix(ci): recognize verification and mutation tool entries

* test(rizin): cover successful fake inferior detach

* test(cli): align full output failure envelope

* test(cli): isolate reverse command lifecycle service

* test(process): share debugger output fixture setup

* fix(binary): preserve high-bit Windows signature bytes

* style(binary): format Windows signature parser

* test(mcp): preserve call coverage in unchanged comparison

* fix(ci): include reverse tools Vitest configs in lint cache

* fix(mcp): preserve target content on delivery errors

* ci: align duplication ceiling with expanded suite

* test(reverse): allow signal admission mutants

* fix(reverse): bound retained cleanup failures to process owners

* fix(test): replace drifting mutation ranges with owned selectors

* fix(reverse): admit persistent commands and retain cleanup ownership

* fix(cutter): recover timed-out commands from exact receipts

* fix(cutter): reject unsupported bridge request kinds

* fix(ci): repair client format and shared verification fixtures

* fix(native): preserve debugger cleanup ownership and bridge authentication

* test(native): retain diagnostics without perfect coverage requirements

* chore(licenses): refresh locked dependency inventory and notices

* fix(test): exclude transient command locks from mutation sandboxes

* fix(gdb): admit only platforms with safe detach verification

* fix(cutter): retain bounded completion receipts until acknowledgement

* fix(cutter): acknowledge commands rejected before UI dispatch

* test(reverse): fail cancellation checks before fixture teardown

* test(gdb): clean up the retained broken-stdin fixture owner

* docs(test): clarify focused provider verification scope

* fix(rizin): retain close cleanup failures

* fix(gdb): wait for inferior stop and bound MI buffering

* fix(cutter): retain failed receipt acknowledgements

* fix(rizin): return retained session identity on rebind failure

* fix(gdb): preserve startup cancellation and lossy status

* fix(rizin): retain launch provenance and assemble frames once

* fix(cutter): recover receipts across callers and bound discovery

* docs: describe debugger evidence and bridge recovery

* fix(process): keep cleanup incomplete while writers remain

Stopping a debugger no longer reports success when inherited output
streams stay open, so BinarySession cannot reopen a binary a child is
still changing. Concurrent Cutter cleanup retries share one receipt
acknowledgement instead of treating a consumed receipt as failure.

* fix(debuggers): recover cleanup with captured process identities

* fix(analysis): retire sessions after unrestricted commands

* fix(cli): preserve completed evidence after cleanup recovery

* fix(cutter): bound frame collection and coalesce cleanup

* fix(windows): resolve selected provider paths absolutely

* fix(gdb): report decoding loss in failed startup evidence

* fix(rizin): scope decoding loss to collected command frames

* test(mcp): cover snapshot retirement in reverse tools CI

---------

Co-authored-by: Daniel Gonçalves Araujo <47118053+dandgabr@users.noreply.github.com>
Co-authored-by: REA Maintainers <noreply@github.com>
Co-authored-by: OpenAI Codex <codex@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
authored and GitHub committed 2026-10-14 04:47:34 +08:00
1 parent 2ad2a044d4
commit 4aa51bfe19
168 files changed
+32703 -238

No files matched your search

+5
View File
@@ -62,6 +62,10 @@ function classifyPackage(base, head) {
]),
].filter((key) => beforeScripts?.[key] !== afterScripts?.[key]);
for (const script of changed) {
if (/^test:(coverage|mutation):reverse-tools$/.test(script)) {
selected.reverse_tools = true;
continue;
}
if (script === "test:sqlite" || script === "test:sqlite:run") {
selected.sqlite = true;
continue;
@@ -162,6 +166,7 @@ if (tier === "reused") {
.split("\0")
.filter(Boolean);
for (const file of files) {
if (matchesAny(file, SCOPES.reverse_tools)) selected.reverse_tools = true;
if (
/^(README(?:_[^/]+)?\.md|AGENTS\.md|CONTRIBUTING\.md|CHANGELOG\.md|SECURITY\.md|native\/windows\/README\.md|\.github\/pull_request_template\.md)$/.test(
file,
+1
View File
@@ -31,6 +31,7 @@ if (outputs.tier === "full" && selectedScopes.length !== CI_FLAGS.length)
const required = reused ? ["changes"] : ["changes", "static", "test"];
const lanes = {
code: ["build", "test-shard"],
reverse_tools: ["reverse-tools-quality"],
runtime: ["windows-native-build", "package-e2e"],
package: ["windows-curated"],
sqlite: ["sqlite-runtime"],
+56
View File
@@ -2,6 +2,62 @@
// Shared build inputs are classified once in plan.mjs rather than repeated in
// every workflow.
export const SCOPES = {
reverse_tools: [
"src/application/reverse/**",
"src/application/binary/BinarySession.ts",
"src/cutter/**",
"src/gdb/**",
"src/frida/frida-bindings.d.ts",
"src/objdump/**",
"src/cli/reverseEngineeringCommands.ts",
"src/process/ownedDebuggerSession.ts",
"src/rizin/**",
"src/contracts/*ToolContracts.ts",
"src/server/registerCutterTools.ts",
"src/server/externalMutationAdmission.ts",
"src/server/registerGdbTools.ts",
"src/server/registerReverseEngineeringTools.ts",
"src/server/providerEnvironment.ts",
"src/process/ExecFileOutput.ts",
"src/process/ExecFileOutput.test.ts",
"tests/composition/analysis-sessions/binarySession.test.ts",
"tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts",
"tests/boundary/filesystem/reverseToolsMutationConfig.test.ts",
"src/server/registerRizinDebugTools.ts",
"tests/boundary/providers/CutterBridge*.test.ts",
"tests/boundary/providers/cutterBridgeProducer.test.ts",
"tests/boundary/providers/GdbSession*.test.ts",
"tests/boundary/providers/GdbCommandQueue.test.ts",
"tests/boundary/providers/gdbTestHelpers.ts",
"tests/boundary/providers/RizinDebugSessionCoverage.part*.test.ts",
"tests/boundary/providers/RizinCommandTimeout.test.ts",
"tests/boundary/providers/RizinTimeoutEvidence.test.ts",
"tests/boundary/providers/RizinDebugSession*.test.ts",
"tests/boundary/providers/RizinInferiorTty.test.ts",
"tests/boundary/providers/RizinTestInferiorTty.ts",
"tests/boundary/process/ownedDebuggerSession.test.ts",
"tests/boundary/cli/gdbStartupCleanupCli.test.ts",
"tests/boundary/cli/reverseEngineeringCommands.test.ts",
"tests/boundary/mcp/cutter*.test.ts",
"tests/boundary/mcp/gdbCancellationMcp.test.ts",
"tests/boundary/mcp/debuggerCancellationMcp.test.ts",
"tests/boundary/mcp/rizinDebuggerCancellationMcp.test.ts",
"tests/boundary/mcp/reverseEngineeringPartialMcp.test.ts",
"tests/boundary/mcp/workflowSnapshotReplayMcp.test.ts",
"tests/boundary/mcp/reverseEngineeringEnvironmentMcp.test.ts",
"tests/support/reverseEngineering*.ts",
"tests/support/ownedRizinCommandEvidenceTests.ts",
"tests/support/reverseDebuggerMcpTestHelpers.ts",
"tests/processOwnershipRunner.ts",
"tests/composition/reverseEngineering*.test.ts",
"vitest.reverse-tools.config.ts",
"vitest.reverse-tools-semantics.config.ts",
"stryker.config.mjs",
"stryker.semantics.config.mjs",
"tsconfig.reverse-tools-mutation.json",
"scripts/prepare-reverse-tools-mutation.mjs",
"scripts/lib/reverse-tools-mutation-targets.mjs",
],
// Apple artifact CLI/MCP lane on macOS, including its focused regressions.
apple: [
"src/native/Apple*",
+31
View File
@@ -19,6 +19,11 @@ export const toolFamilyCatalog = (sources) => {
contracts: [
...sources.nativeContracts.NATIVE_TOOL_CONTRACTS,
...sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS,
...sources.cutterContracts.CUTTER_TOOL_CONTRACTS,
...sources.reverseEngineeringContracts
.REVERSE_ENGINEERING_TOOL_CONTRACTS,
...sources.gdbContracts.GDB_TOOL_CONTRACTS,
...sources.rizinDebugContracts.RIZIN_DEBUG_TOOL_CONTRACTS,
],
},
{
@@ -164,6 +169,32 @@ export const providerCatalog = (sources) => {
activeContracts,
} = electronContractSlices(sources);
return [
{
identity: sources.cutterProvider.CUTTER_PROVIDER_IDENTITY,
contracts: sources.cutterContracts.CUTTER_TOOL_CONTRACTS,
},
{
identity: sources.objdumpProvider.OBJDUMP_PROVIDER_IDENTITY,
contracts:
sources.reverseEngineeringContracts.REVERSE_ENGINEERING_TOOL_CONTRACTS.filter(
({ name }) => name === "inspect_with_objdump",
),
},
{
identity: sources.rizinProvider.RIZIN_PROVIDER_IDENTITY,
contracts:
sources.reverseEngineeringContracts.REVERSE_ENGINEERING_TOOL_CONTRACTS.filter(
({ name }) => name === "execute_rizin_command",
),
},
{
identity: sources.gdbProvider.GDB_PROVIDER_IDENTITY,
contracts: sources.gdbContracts.GDB_TOOL_CONTRACTS,
},
{
identity: sources.rizinDebugProvider.RIZIN_DEBUGGER_PROVIDER_IDENTITY,
contracts: sources.rizinDebugContracts.RIZIN_DEBUG_TOOL_CONTRACTS,
},
{
identity: sources.fridaProvider.FRIDA_PROVIDER_IDENTITY,
contracts: sources.fridaContracts.FRIDA_TOOL_CONTRACTS,
+10
View File
@@ -43,6 +43,16 @@ const SOURCE_PATHS = {
sessionContracts: "dist/contracts/sessionToolContracts.js",
binaryDiagnosticsContracts:
"dist/contracts/native/binaryDiagnosticsToolContracts.js",
cutterContracts: "dist/contracts/cutterToolContracts.js",
cutterProvider: "dist/cutter/CutterBridgeClient.js",
reverseEngineeringContracts:
"dist/contracts/reverseEngineeringToolContracts.js",
gdbContracts: "dist/contracts/gdbToolContracts.js",
gdbProvider: "dist/gdb/GdbSessionManager.js",
rizinDebugContracts: "dist/contracts/rizinDebugToolContracts.js",
objdumpProvider: "dist/objdump/ObjdumpCommand.js",
rizinProvider: "dist/rizin/RizinCommand.js",
rizinDebugProvider: "dist/rizin/RizinDebugSessionManager.js",
fridaContracts: "dist/contracts/fridaToolContracts.js",
fridaProvider: "dist/frida/FridaInstrumentationManager.js",
pwntoolsProvider: "dist/native/pwntools/PwntoolsRelease.js",
@@ -0,0 +1,182 @@
import { readFileSync } from "node:fs";
import ts from "typescript6";
const compact = (text) => text.replace(/\s+/gu, "");
const nameOf = (node, source) => node.name?.getText(source);
const methodOwner = (node, source) => {
for (let parent = node; parent !== undefined; parent = parent.parent) {
if (ts.isMethodDeclaration(parent) || ts.isConstructorDeclaration(parent))
return {
owner: nameOf(parent.parent, source),
body: parent.body,
method: ts.isConstructorDeclaration(parent)
? "constructor"
: nameOf(parent, source),
};
}
return undefined;
};
const withinOwner = (node, selector, source) => {
if (selector.owner === undefined) return true;
const owner = methodOwner(node, source);
return (
owner?.owner === selector.owner &&
(selector.method === undefined || owner.method === selector.method)
);
};
const contains = (node, predicate) =>
predicate(node) ||
ts.forEachChild(node, (child) => contains(child, predicate)) === true;
const inBlockWithBinding = (node, binding) => {
if (binding === undefined) return true;
let block = node.parent;
while (block !== undefined && !ts.isBlock(block)) block = block.parent;
return block?.statements.some(
(statement) =>
ts.isVariableStatement(statement) &&
statement.declarationList.declarations.some(
(declaration) =>
ts.isIdentifier(declaration.name) &&
declaration.name.text === binding,
),
);
};
const isCommandHandler = (node, command) => {
if (!ts.isArrowFunction(node)) return false;
const property = node.parent;
if (
!ts.isPropertyAssignment(property) ||
!ts.isIdentifier(property.name) ||
property.name.text !== "run"
)
return false;
const call = property.parent.parent;
if (
!ts.isCallExpression(call) ||
!ts.isPropertyAccessExpression(call.expression) ||
!ts.isIdentifier(call.expression.expression) ||
call.expression.expression.text !== "cli" ||
call.expression.name.text !== "command"
)
return false;
const selected = call.arguments[0];
return (
selected !== undefined &&
ts.isPropertyAccessExpression(selected) &&
ts.isIdentifier(selected.expression) &&
selected.expression.text === "CLI_COMMANDS" &&
selected.name.text === command
);
};
const matches = (node, selector, source) => {
if (!withinOwner(node, selector, source)) return false;
if (
selector.direct === true &&
node.parent !== methodOwner(node, source)?.body
)
return false;
switch (selector.kind) {
case "expression":
return (
ts.isBinaryExpression(node) &&
compact(node.getText(source)) === compact(selector.text)
);
case "declaration":
return (
ts.isVariableDeclaration(node) &&
nameOf(node, source) === selector.name &&
inBlockWithBinding(node, selector.withinBlockWithBinding)
);
case "method":
return (
ts.isMethodDeclaration(node) && nameOf(node, source) === selector.name
);
case "constructor":
return ts.isConstructorDeclaration(node);
case "guard":
return (
ts.isIfStatement(node) &&
compact(node.expression.getText(source)) === compact(selector.condition)
);
case "call":
return (
ts.isCallExpression(node) &&
compact(node.expression.getText(source)) === compact(selector.callee) &&
(selector.event === undefined ||
(node.arguments[0] !== undefined &&
ts.isStringLiteral(node.arguments[0]) &&
node.arguments[0].text === selector.event))
);
case "try":
return (
ts.isTryStatement(node) &&
contains(node.tryBlock, (child) =>
selector.assignment !== undefined
? ts.isBinaryExpression(child) &&
child.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
ts.isIdentifier(child.left) &&
child.left.text === selector.assignment
: ts.isCallExpression(child) &&
compact(child.expression.getText(source)) ===
compact(selector.callee),
)
);
case "command-handler":
return isCommandHandler(node, selector.command);
default:
throw new Error(
`Unknown mutation selector kind: ${String(selector.kind)}`,
);
}
};
/** Parse a source once and resolve one owned region for every requested selector. */
export const mutationRanges = (path, selectors) => {
if (!Array.isArray(selectors) || selectors.length === 0)
throw new Error(`Mutation selectors must be nonempty: ${path}`);
for (const selector of selectors)
if (
["method", "constructor", "guard", "call", "try"].includes(
selector.kind,
) &&
typeof selector.owner !== "string"
)
throw new Error(
`Mutation selector requires a named class owner: ${path}`,
);
const source = ts.createSourceFile(
path,
readFileSync(path, "utf8"),
ts.ScriptTarget.Latest,
true,
);
if (source.parseDiagnostics.length !== 0)
throw new Error(
`Cannot select mutations from malformed TypeScript: ${path}`,
);
const found = selectors.map(() => []);
const visit = (node) => {
for (const [index, selector] of selectors.entries())
if (matches(node, selector, source)) found[index].push(node);
ts.forEachChild(node, visit);
};
visit(source);
return selectors.map((selector, index) => {
const candidates = found[index];
if (candidates.length !== 1)
throw new Error(
`Expected one mutation target in ${path}: ${JSON.stringify(selector)}; found ${candidates.length}`,
);
const node = candidates[0];
const first =
source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
const last = source.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
return `${path}:${first}-${last}`;
});
};
@@ -0,0 +1,19 @@
import { mkdir, rm, symlink } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const compiler = resolve(root, "node_modules/typescript6");
for (const packageName of ["core", "typescript-checker"]) {
const packagePath = resolve(
root,
"node_modules/@stryker-mutator",
packageName,
);
const nodeModulesPath = resolve(packagePath, "node_modules");
const typescriptPath = resolve(nodeModulesPath, "typescript");
await mkdir(nodeModulesPath, { recursive: true });
await rm(typescriptPath, { recursive: true, force: true });
await symlink(compiler, typescriptPath, "dir");
}
+2
View File
@@ -4,6 +4,7 @@ import { promisify } from "node:util";
const exec = promisify(execFile);
const packedHopperBridge = "package/bridge/hopper_bridge.py";
const packedCutterBridge = "package/bridge/cutter/rea_bridge.py";
const packedGhidraBridge = "package/bridge/ghidra/ReaGhidraBridge.java";
const packedGhidraActivator =
"package/bridge/ghidra/ReaGhidraBundleActivator.java";
@@ -12,6 +13,7 @@ const packedGhidraActivator =
export async function verifyPackedBridge({ workspace, tarball, packedFiles }) {
for (const path of [
packedHopperBridge,
packedCutterBridge,
packedGhidraBridge,
packedGhidraActivator,
]) {
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { execFile } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import {
access,
constants,
mkdtemp,
readFile,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { promisify } from "node:util";
import { createServer } from "../dist/server/createServer.js";
const execFileAsync = promisify(execFile);
const cli = resolve("scripts/rea.mjs");
const runCli = async (...arguments_) => {
const { stdout } = await execFileAsync(
process.execPath,
[cli, ...arguments_, "--format", "json"],
{
encoding: "utf8",
maxBuffer: 4 * 1024 * 1024,
env: process.env,
},
);
return JSON.parse(stdout);
};
const projectRoot = await mkdtemp(join(tmpdir(), "rea-cutter-smoke-"));
const projectPath = join(projectRoot, "smoke.rzdb");
const rizinQuote = (value) =>
`"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
const unavailableAnalysis = {
execute: async () => {
throw new Error(
"The Cutter verification does not call unrelated analysis tools.",
);
},
};
const expectedPlatform = {
Linux: "linux",
macOS: "darwin",
Windows: "win32",
}[process.env.EXPECTED_PLATFORM];
if (expectedPlatform !== undefined)
assert.equal(
process.platform,
expectedPlatform,
"Cutter runner did not match its declared platform.",
);
try {
const discovery = await runCli("list-cutter-sessions");
assert.equal(
discovery.discovery_status,
"sessions_found",
"Open upstream Cutter with the REA plugin before running this smoke test.",
);
assert.ok(discovery.sessions.length > 0);
const session = discovery.sessions[0];
assert.ok(session);
assert.ok(
session.cutter_version,
"The upstream Cutter API did not report its version.",
);
if (process.env.REA_CUTTER_EXPECTED_VERSION !== undefined)
assert.ok(
session.cutter_version.includes(process.env.REA_CUTTER_EXPECTED_VERSION),
"The live Cutter version did not match the configured version.",
);
assert.ok(
session.current_file,
"Open a disposable local binary in Cutter before running this smoke test.",
);
const artifactPath = session.current_file;
const artifactDigest = createHash("sha256")
.update(await readFile(artifactPath))
.digest("hex");
const inspection = await runCli(
"cutter-command",
session.session_id,
String(session.document_generation),
"ij",
"--json",
);
assert.equal(inspection.predicate_type, "rea.cutter.command-observation");
assert.ok(
Object.hasOwn(inspection, "raw_result"),
"Cutter ij result was not retained as Evidence.",
);
assert.equal(typeof inspection.raw_result.bin?.arch, "string");
const mcpServer = createServer(
{ kind: "fixed", analysis: unavailableAnalysis },
{
providerEnvironment: process.env,
},
);
const mcpClient = new Client({
name: "rea-cutter-upstream-smoke",
version: "1",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await mcpServer.connect(serverTransport);
await mcpClient.connect(clientTransport);
const mcpDiscovery = await mcpClient.callTool({
name: "list_cutter_sessions",
arguments: {},
});
assert.notEqual(mcpDiscovery.isError, true);
assert.equal(
mcpDiscovery.structuredContent.discovery_status,
"sessions_found",
);
const mcpInspection = await mcpClient.callTool({
name: "cutter_command",
arguments: {
session_id: session.session_id,
expected_generation: session.document_generation,
command: "ij",
json: true,
},
});
assert.notEqual(mcpInspection.isError, true);
assert.deepEqual(
mcpInspection.structuredContent.normalized_result,
inspection.normalized_result,
"Cutter CLI and MCP returned different analysis results.",
);
} finally {
await mcpClient.close();
await mcpServer.close();
}
const annotation = `rea-smoke-${randomUUID()}`;
await runCli(
"cutter-command",
session.session_id,
String(session.document_generation),
`CC ${annotation} @ entry0`,
);
await runCli(
"cutter-command",
session.session_id,
String(session.document_generation),
`Ps ${rizinQuote(projectPath)}`,
);
await access(projectPath, constants.R_OK | constants.W_OK);
await runCli(
"cutter-command",
session.session_id,
String(session.document_generation),
"CC- @ entry0",
);
const removed = await runCli(
"cutter-command",
session.session_id,
String(session.document_generation),
"CC. @ entry0",
);
assert.ok(
!String(removed.raw_result).includes(annotation),
"The in-memory annotation was not removed before project reopening.",
);
await runCli(
"cutter-command",
session.session_id,
String(session.document_generation),
`Po ${rizinQuote(projectPath)}`,
);
const recovered = await runCli(
"cutter-command",
session.session_id,
String(session.document_generation),
"CC. @ entry0",
);
assert.equal(recovered.predicate_type, "rea.cutter.command-observation");
assert.ok(
String(recovered.raw_result).includes(annotation),
"The saved project did not preserve the analysis comment after reopening.",
);
const finalArtifactDigest = createHash("sha256")
.update(await readFile(artifactPath))
.digest("hex");
assert.equal(
finalArtifactDigest,
artifactDigest,
"Cutter project edits changed the executable bytes.",
);
const report = {
ok: true,
platform: process.platform,
cutter_version: session.cutter_version,
document_generation: session.document_generation,
cli_mcp_evidence_parity: true,
annotation_saved_and_reopened: true,
executable_digest_unchanged: true,
};
await writeFile("cutter-smoke.json", `${JSON.stringify(report, null, 2)}\n`, {
mode: 0o600,
});
console.log(JSON.stringify(report));
} finally {
await rm(projectRoot, { recursive: true, force: true });
}
+49
View File
@@ -148,6 +148,10 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
"//localhost/share/target.bin",
])
assert.throws(() => native.call("open", [rejected]));
assert.throws(
() => native.call("cutter_bridge_verify_directory", [sourceDirectory]),
/owner|DACL/u,
);
report.controls.ordinaryDriveSeparatorsAndRequestedIdentity = true;
// An owned runtime's DELETE lease conflicts with a fresh parent admission.
// This produces a genuine sharing failure after successful authority caching,
@@ -282,6 +286,10 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
() => native.call("open", [join(junction, "target.bin")]),
/Reparse/u,
);
assert.throws(
() => native.call("cutter_bridge_verify_directory", [junction]),
/Reparse/u,
);
assert.throws(() => native.call("open", [source + ":stream"]));
const runtime = native.call("runtime_create", [
workspace.replaceAll("\\", "/"),
@@ -326,6 +334,47 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
"descriptor.json",
Buffer.from("fixture"),
]);
assert.equal(
native.call("cutter_bridge_verify_directory", [runtime.path]).privateDacl,
true,
);
assert.equal(
native
.call("cutter_bridge_read_descriptor", [
runtime.path,
"descriptor.json",
64 * 1024,
])
.toString("utf8"),
"fixture",
);
const descriptorJunction = join(runtime.path, "descriptor-link.json");
await symlink(sourceDirectory, descriptorJunction, "junction");
assert.throws(() =>
native.call("cutter_bridge_read_descriptor", [
runtime.path,
"descriptor-link.json",
64 * 1024,
]),
);
assert.throws(
() =>
native.call("cutter_bridge_read_descriptor", [
runtime.path,
"descriptor.json",
3,
]),
/byte limit/u,
);
assert.throws(
() =>
native.call("cutter_bridge_read_descriptor", [
runtime.path,
"..\\escape.json",
64 * 1024,
]),
/filename component/u,
);
assert.throws(() =>
native.call("runtime_write", [
runtime.handle,