mirror of
https://github.com/heaven-hm/re-agent.git
synced 2026-11-07 02:36:04 -10:00
feat: strengthen reconstruction and runtime workflows
This commit is contained in:
1 parent
70ac34d6b6
commit
85fc7c58d1
18 files changed
+1408
-28
No files matched your search
@@ -0,0 +1,56 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
||||
const recipes = [
|
||||
"scripts/calculator/build.sh",
|
||||
"benchmarks/native-windows-ports/snake/replica/build.sh",
|
||||
"benchmarks/native-windows-ports/pixeltetris/replica/build.sh",
|
||||
"benchmarks/native-windows-ports/tetris/replica/build.sh",
|
||||
"benchmarks/native-windows-ports/notepad/replica/build.sh",
|
||||
"benchmarks/native-windows-ports/clipjar/replica/build.sh",
|
||||
"benchmarks/native-windows-ports/gconv/replica/build.sh",
|
||||
"benchmarks/native-windows-ports/imgview/replica/build.sh",
|
||||
"benchmarks/plink/replica/build.sh",
|
||||
"benchmarks/native-batch-2/reconstructed/fd/build.sh",
|
||||
"benchmarks/native-batch-2/reconstructed/ripgrep/build.sh",
|
||||
"benchmarks/native-batch-2/reconstructed/bat/build.sh",
|
||||
];
|
||||
|
||||
if (process.platform !== "darwin") {
|
||||
throw new Error(
|
||||
"The checked-in desktop replicas currently target macOS; run this build on macOS.",
|
||||
);
|
||||
}
|
||||
|
||||
const failures = [];
|
||||
for (const recipe of recipes) {
|
||||
const path = resolve(root, recipe);
|
||||
if (!existsSync(path)) {
|
||||
failures.push(`${recipe}: build recipe is missing`);
|
||||
continue;
|
||||
}
|
||||
|
||||
process.stdout.write(`\n==> ${recipe}\n`);
|
||||
const result = spawnSync("bash", [path], {
|
||||
cwd: root,
|
||||
stdio: "inherit",
|
||||
});
|
||||
if (result.error !== undefined)
|
||||
failures.push(`${recipe}: ${result.error.message}`);
|
||||
else if (result.status !== 0)
|
||||
failures.push(`${recipe}: exited with ${result.status}`);
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
process.stderr.write(`\n${failures.length} benchmark build(s) failed:\n`);
|
||||
for (const failure of failures) process.stderr.write(`- ${failure}\n`);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
process.stdout.write(`\nBuilt ${recipes.length} macOS benchmark replicas.\n`);
|
||||
process.stdout.write(
|
||||
"Outputs are host-specific and remain under ignored build/ directories.\n",
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Local Swift executable and compiler cache are reproducible build output.
|
||||
build
|
||||
build/**
|
||||
@@ -0,0 +1,30 @@
|
||||
import AppKit
|
||||
|
||||
@MainActor
|
||||
final class CalculatorApplicationDelegate: NSObject, NSApplicationDelegate {
|
||||
private var calculatorWindow: NSWindow?
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
let engine = CalculatorEngine()
|
||||
let window = createCalculatorWindow(engine: engine)
|
||||
calculatorWindow = window
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@main
|
||||
@MainActor
|
||||
struct CalculatorApp {
|
||||
static func main() {
|
||||
let application = NSApplication.shared
|
||||
let applicationDelegate = CalculatorApplicationDelegate()
|
||||
application.delegate = applicationDelegate
|
||||
application.setActivationPolicy(.regular)
|
||||
application.run()
|
||||
}
|
||||
}
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
output_dir="$script_dir/build"
|
||||
mkdir -p "$output_dir"
|
||||
: "${SWIFTC:=swiftc}"
|
||||
"$SWIFTC" -O -module-cache-path "$output_dir/module-cache" \
|
||||
-o "$output_dir/calculatormac" \
|
||||
"$script_dir/CalculatorEngine.swift" \
|
||||
"$script_dir/CalculatorWindow.swift" \
|
||||
"$script_dir/CalculatorApp.swift" \
|
||||
-framework Cocoa
|
||||
file "$output_dir/calculatormac"
|
||||
@@ -0,0 +1,338 @@
|
||||
/** Bounded file-conversion benchmark using REA's shared owned-process boundary.
|
||||
* Work directories are retained evidence, not an OS sandbox. Commands must be
|
||||
* authorized and isolated by the caller's selected backend where required.
|
||||
*/
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { constants } from "node:fs";
|
||||
import { mkdir, mkdtemp, open, rename, rm, writeFile } from "node:fs/promises";
|
||||
import { basename, join, resolve } from "node:path";
|
||||
import { runOwnedCommand } from "../../dist/process/OwnedCommand.js";
|
||||
|
||||
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
const dimension = (equal) => ({ status: equal ? "PASS" : "FAIL" });
|
||||
const positive = (value, name) => {
|
||||
if (!Number.isSafeInteger(value) || value <= 0)
|
||||
throw new TypeError(`${name} must be a positive safe integer`);
|
||||
return value;
|
||||
};
|
||||
const vector = (value, name) => {
|
||||
if (
|
||||
!Array.isArray(value) ||
|
||||
value.length === 0 ||
|
||||
value.some((part) => typeof part !== "string" || part.includes("\0")) ||
|
||||
value[0].length === 0
|
||||
)
|
||||
throw new TypeError(`${name} must be a nonempty command vector`);
|
||||
return value;
|
||||
};
|
||||
const safeName = (name) => {
|
||||
if (
|
||||
typeof name !== "string" ||
|
||||
!name ||
|
||||
basename(name) !== name ||
|
||||
name === "." ||
|
||||
name === ".." ||
|
||||
/[\\/\0]/u.test(name)
|
||||
)
|
||||
throw new TypeError("outputName must be a single file name");
|
||||
return name;
|
||||
};
|
||||
const failureProjection = (cause) => ({
|
||||
name: cause?.name ?? "Error",
|
||||
reason: cause?.reason ?? cause?.code ?? "exception",
|
||||
message: cause?.message ?? String(cause),
|
||||
...(typeof cause?.cleanupFailure === "string"
|
||||
? { cleanupFailure: cause.cleanupFailure }
|
||||
: {}),
|
||||
...(cause?.cleanupOwner ? { unresolvedCleanupOwner: true } : {}),
|
||||
...(Array.isArray(cause?.resources)
|
||||
? {
|
||||
resources: cause.resources.filter((value) => typeof value === "string"),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
async function atomicReport(path, report) {
|
||||
const temporary = path + "." + randomUUID() + ".tmp";
|
||||
try {
|
||||
await writeFile(temporary, JSON.stringify(report, null, 2) + "\n", {
|
||||
flag: "wx",
|
||||
mode: 0o600,
|
||||
});
|
||||
await rename(temporary, path);
|
||||
} finally {
|
||||
await rm(temporary, { force: true });
|
||||
}
|
||||
}
|
||||
async function boundedFile(path, limit) {
|
||||
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
||||
try {
|
||||
const before = await handle.stat();
|
||||
if (!before.isFile() || before.size > limit)
|
||||
throw new Error(`Not a regular bounded file: ${path}`);
|
||||
const bytes = Buffer.alloc(before.size);
|
||||
let offset = 0;
|
||||
while (offset < bytes.length) {
|
||||
const read = await handle.read(
|
||||
bytes,
|
||||
offset,
|
||||
bytes.length - offset,
|
||||
offset,
|
||||
);
|
||||
if (!read.bytesRead) break;
|
||||
offset += read.bytesRead;
|
||||
}
|
||||
const after = await handle.stat();
|
||||
if (
|
||||
offset !== bytes.length ||
|
||||
before.size !== after.size ||
|
||||
before.mtimeMs !== after.mtimeMs ||
|
||||
before.ctimeMs !== after.ctimeMs
|
||||
)
|
||||
throw new Error(`File changed during observation: ${path}`);
|
||||
return bytes;
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** Command arguments support {input}, {output}, {workdir}; no shell is used.
|
||||
* Cases are serial and both sides receive independent copies of the same input.
|
||||
* Missing output, timeouts, cancelled runs and truncated diagnostics never PASS.
|
||||
* reportPath is retained on exceptions; onProgress receives a detached journal
|
||||
* after each atomic write. Reports are supervision evidence, not a canonical
|
||||
* ProcessCapture. Atomic rename prevents partial JSON, not power-loss durability.
|
||||
*/
|
||||
export async function runFileDifferentialBenchmark(options) {
|
||||
const original = vector(options.originalCommand, "originalCommand");
|
||||
const replica = vector(options.replicaCommand, "replicaCommand");
|
||||
const timeoutMs = positive(options.timeoutMs ?? 15_000, "timeoutMs");
|
||||
const maxFileBytes = positive(
|
||||
options.maxFileBytes ?? 16 * 1024 * 1024,
|
||||
"maxFileBytes",
|
||||
);
|
||||
const diagnosticBytes = positive(
|
||||
options.diagnosticBytes ?? 1024 * 1024,
|
||||
"diagnosticBytes",
|
||||
);
|
||||
if (
|
||||
options.onProgress !== undefined &&
|
||||
typeof options.onProgress !== "function"
|
||||
)
|
||||
throw new TypeError("onProgress must be a function");
|
||||
if (!Array.isArray(options.cases) || options.cases.length === 0)
|
||||
throw new TypeError("A nonempty declared case suite is required");
|
||||
const seen = new Set();
|
||||
for (const item of options.cases) {
|
||||
if (
|
||||
typeof item.id !== "string" ||
|
||||
!/^[a-zA-Z0-9_-]+$/u.test(item.id) ||
|
||||
seen.has(item.id)
|
||||
)
|
||||
throw new TypeError("Case IDs must be unique path-safe identifiers");
|
||||
seen.add(item.id);
|
||||
safeName(item.outputName ?? "output.bin");
|
||||
if (
|
||||
item.arguments !== undefined &&
|
||||
(!Array.isArray(item.arguments) ||
|
||||
item.arguments.some(
|
||||
(part) => typeof part !== "string" || part.includes("\0"),
|
||||
))
|
||||
)
|
||||
throw new TypeError("Case arguments must be strings without NUL");
|
||||
}
|
||||
options.signal?.throwIfAborted();
|
||||
const directory = resolve(options.outputDirectory);
|
||||
await mkdir(directory, { recursive: true });
|
||||
const runDirectory = await mkdtemp(join(directory, "file-diff-"));
|
||||
const report = {
|
||||
schema: "rea.file-differential-benchmark.v1",
|
||||
status: "UNVERIFIED",
|
||||
scope:
|
||||
"Exact output-file bytes, retained UTF-8 diagnostic text, and exit status on the declared cases only.",
|
||||
universalEquivalence: false,
|
||||
platform: { os: process.platform, architecture: process.arch },
|
||||
limits: { timeoutMs, maxFileBytes, diagnosticBytes },
|
||||
runDirectory,
|
||||
reportPath: join(runDirectory, "report.json"),
|
||||
phase: "running",
|
||||
cases: options.cases.map((item) => ({
|
||||
id: item.id,
|
||||
status: "UNVERIFIED",
|
||||
phase: "pending",
|
||||
})),
|
||||
};
|
||||
const persist = async (notify = true) => {
|
||||
await atomicReport(report.reportPath, report);
|
||||
if (notify && options.onProgress)
|
||||
await options.onProgress(structuredClone(report));
|
||||
};
|
||||
try {
|
||||
await persist();
|
||||
for (const item of options.cases) {
|
||||
options.signal?.throwIfAborted();
|
||||
const record = report.cases.find((candidate) => candidate.id === item.id);
|
||||
record.phase = "reading-input";
|
||||
await persist();
|
||||
const input = await boundedFile(resolve(item.inputPath), maxFileBytes);
|
||||
record.input = {
|
||||
path: resolve(item.inputPath),
|
||||
bytes: input.length,
|
||||
sha256: hash(input),
|
||||
};
|
||||
await persist();
|
||||
const runSide = async (side, command, environment) => {
|
||||
const cwd = join(runDirectory, `${item.id}-${side}`);
|
||||
await mkdir(cwd);
|
||||
const inputPath = join(
|
||||
cwd,
|
||||
"input" +
|
||||
(basename(item.inputPath).match(/\.[^.]+$/u)?.[0] ?? ".bin"),
|
||||
);
|
||||
const outputPath = join(cwd, item.outputName ?? "output.bin");
|
||||
if (inputPath === outputPath)
|
||||
throw new TypeError("Output must differ from staged input");
|
||||
await writeFile(inputPath, input, { flag: "wx", mode: 0o600 });
|
||||
const expand = (arg) =>
|
||||
arg
|
||||
.split("{input}")
|
||||
.join(inputPath)
|
||||
.split("{output}")
|
||||
.join(outputPath)
|
||||
.split("{workdir}")
|
||||
.join(cwd);
|
||||
const expanded = [...command, ...(item.arguments ?? [])].map(expand);
|
||||
const observation = {
|
||||
command: expanded,
|
||||
workDirectory: cwd,
|
||||
status: "UNVERIFIED",
|
||||
};
|
||||
record[side] = observation;
|
||||
record.phase = `${side}-running`;
|
||||
await persist();
|
||||
options.signal?.throwIfAborted();
|
||||
try {
|
||||
const captured = await runOwnedCommand(
|
||||
{
|
||||
command: expanded[0],
|
||||
arguments: expanded.slice(1),
|
||||
cwd,
|
||||
env: environment ?? process.env,
|
||||
runId: "rea-file-benchmark-" + randomUUID(),
|
||||
expectedCommand: null,
|
||||
},
|
||||
{ timeoutMs, diagnosticBytes, acceptNonZeroExit: true },
|
||||
{ signal: options.signal },
|
||||
);
|
||||
Object.assign(observation, {
|
||||
status: "OBSERVED",
|
||||
exitCode: captured.exitCode,
|
||||
signal: captured.signal,
|
||||
stdout: captured.stdout,
|
||||
stderr: captured.stderr,
|
||||
});
|
||||
} catch (cause) {
|
||||
// Cleanup failures are exceptional: preserve the owner's error/resources.
|
||||
if (cause.cleanupFailure || cause.cleanupOwner) {
|
||||
observation.failure = failureProjection(cause);
|
||||
if (cause.snapshot) observation.snapshot = cause.snapshot;
|
||||
throw cause;
|
||||
}
|
||||
Object.assign(observation, {
|
||||
status: "UNVERIFIED",
|
||||
reason: cause.reason ?? "launch",
|
||||
message: cause.message,
|
||||
snapshot: cause.snapshot ?? null,
|
||||
});
|
||||
}
|
||||
record.phase = `${side}-captured`;
|
||||
await persist();
|
||||
options.signal?.throwIfAborted();
|
||||
let output;
|
||||
try {
|
||||
output = await boundedFile(outputPath, maxFileBytes);
|
||||
observation.output = {
|
||||
path: outputPath,
|
||||
bytes: output.length,
|
||||
sha256: hash(output),
|
||||
};
|
||||
} catch (cause) {
|
||||
observation.output = {
|
||||
path: outputPath,
|
||||
status: "UNVERIFIED",
|
||||
reason: cause.code ?? cause.message,
|
||||
};
|
||||
}
|
||||
record.phase = `${side}-complete`;
|
||||
await persist();
|
||||
return { observation, output };
|
||||
};
|
||||
const left = await runSide(
|
||||
"original",
|
||||
original,
|
||||
options.originalEnvironment,
|
||||
);
|
||||
options.signal?.throwIfAborted();
|
||||
const right = await runSide(
|
||||
"replica",
|
||||
replica,
|
||||
options.replicaEnvironment,
|
||||
);
|
||||
const complete =
|
||||
left.observation.status === "OBSERVED" &&
|
||||
right.observation.status === "OBSERVED";
|
||||
const compare = (equal) =>
|
||||
complete ? dimension(equal) : { status: "UNVERIFIED" };
|
||||
const dimensions = {
|
||||
outputFileBytes:
|
||||
complete && left.output && right.output
|
||||
? dimension(left.output.equals(right.output))
|
||||
: { status: "UNVERIFIED" },
|
||||
stdoutDiagnosticText: compare(
|
||||
left.observation.stdout?.text === right.observation.stdout?.text,
|
||||
),
|
||||
stderrDiagnosticText: compare(
|
||||
left.observation.stderr?.text === right.observation.stderr?.text,
|
||||
),
|
||||
exitStatus: compare(
|
||||
left.observation.exitCode === right.observation.exitCode &&
|
||||
left.observation.signal === null &&
|
||||
right.observation.signal === null,
|
||||
),
|
||||
};
|
||||
const statuses = Object.values(dimensions).map((value) => value.status);
|
||||
Object.assign(record, {
|
||||
dimensions,
|
||||
phase: "complete",
|
||||
status: statuses.includes("FAIL")
|
||||
? "FAIL"
|
||||
: statuses.every((status) => status === "PASS")
|
||||
? "PASS"
|
||||
: "UNVERIFIED",
|
||||
});
|
||||
await persist();
|
||||
}
|
||||
options.signal?.throwIfAborted();
|
||||
report.status = report.cases.some((item) => item.status === "FAIL")
|
||||
? "FAIL"
|
||||
: report.cases.every((item) => item.status === "PASS")
|
||||
? "PASS"
|
||||
: "UNVERIFIED";
|
||||
report.phase = "complete";
|
||||
await persist();
|
||||
return report;
|
||||
} catch (cause) {
|
||||
report.status = "UNVERIFIED";
|
||||
report.phase = options.signal?.aborted ? "cancelled" : "interrupted";
|
||||
report.failure = failureProjection(cause);
|
||||
try {
|
||||
await persist(false);
|
||||
} catch (journalCause) {
|
||||
// A report write failure must not replace unresolved cleanup ownership.
|
||||
if (cause && typeof cause === "object" && Object.isExtensible(cause))
|
||||
cause.journalFailure = failureProjection(journalCause);
|
||||
}
|
||||
if (cause && typeof cause === "object" && Object.isExtensible(cause))
|
||||
cause.reportPath = report.reportPath;
|
||||
throw cause;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { runFileDifferentialBenchmark } from "./file-differential-benchmark.mjs";
|
||||
|
||||
const fixture = `
|
||||
const fs = require('node:fs');
|
||||
const [mode,input,output] = process.argv.slice(1);
|
||||
if (mode === 'hang') setInterval(() => {},1000);
|
||||
else if (mode === 'overflow') process.stdout.write('x'.repeat(10000));
|
||||
else if (mode !== 'missing') {
|
||||
const bytes=fs.readFileSync(input);
|
||||
if (mode === 'different') bytes[0] ^= 255;
|
||||
fs.writeFileSync(output,bytes);
|
||||
if (mode === 'stdout') process.stdout.write('different stdout');
|
||||
if (mode === 'stderr') process.stderr.write('different stderr');
|
||||
if (mode === 'exit') process.exitCode=7;
|
||||
}
|
||||
`;
|
||||
const command = (mode) => [
|
||||
process.execPath,
|
||||
"-e",
|
||||
fixture,
|
||||
mode,
|
||||
"{input}",
|
||||
"{output}",
|
||||
];
|
||||
async function scope(operation) {
|
||||
const directory = await mkdtemp(join(tmpdir(), "rea-file-diff-test-"));
|
||||
try {
|
||||
const inputPath = join(directory, "fixture.dat");
|
||||
await writeFile(inputPath, Buffer.from([0, 255, 13, 10, 128]));
|
||||
return await operation({
|
||||
outputDirectory: directory,
|
||||
originalCommand: command("copy"),
|
||||
replicaCommand: command("copy"),
|
||||
cases: [{ id: "binary", inputPath }],
|
||||
timeoutMs: 10000,
|
||||
});
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test("exact binary outputs and retained report from independent fresh directories", () =>
|
||||
scope(async (options) => {
|
||||
const report = await runFileDifferentialBenchmark(options);
|
||||
assert.equal(report.status, "PASS");
|
||||
assert.equal(report.universalEquivalence, false);
|
||||
const item = report.cases[0];
|
||||
assert.notEqual(item.original.workDirectory, item.replica.workDirectory);
|
||||
assert.equal(item.original.output.sha256, item.input.sha256);
|
||||
assert.equal(item.replica.output.sha256, item.input.sha256);
|
||||
assert.deepEqual(
|
||||
JSON.parse(await readFile(report.reportPath, "utf8")),
|
||||
report,
|
||||
);
|
||||
const repeat = await runFileDifferentialBenchmark(options);
|
||||
assert.notEqual(report.runDirectory, repeat.runDirectory);
|
||||
}));
|
||||
|
||||
for (const [mode, dimension] of [
|
||||
["different", "outputFileBytes"],
|
||||
["stdout", "stdoutDiagnosticText"],
|
||||
["stderr", "stderrDiagnosticText"],
|
||||
["exit", "exitStatus"],
|
||||
]) {
|
||||
test(`reports ${dimension} mismatch independently`, () =>
|
||||
scope(async (options) => {
|
||||
const report = await runFileDifferentialBenchmark({
|
||||
...options,
|
||||
replicaCommand: command(mode),
|
||||
});
|
||||
assert.equal(report.status, "FAIL");
|
||||
for (const [name, value] of Object.entries(report.cases[0].dimensions))
|
||||
assert.equal(value.status, name === dimension ? "FAIL" : "PASS");
|
||||
}));
|
||||
}
|
||||
|
||||
for (const mode of ["missing", "hang", "overflow"]) {
|
||||
test(`never passes ${mode}`, () =>
|
||||
scope(async (options) => {
|
||||
const report = await runFileDifferentialBenchmark({
|
||||
...options,
|
||||
originalCommand: command(mode),
|
||||
replicaCommand: command(mode),
|
||||
timeoutMs: mode === "hang" ? 100 : 10000,
|
||||
diagnosticBytes: 1000,
|
||||
});
|
||||
assert.equal(report.status, "UNVERIFIED");
|
||||
assert.equal(
|
||||
report.cases[0].dimensions.outputFileBytes.status,
|
||||
"UNVERIFIED",
|
||||
);
|
||||
if (mode !== "missing") {
|
||||
assert.equal(report.cases[0].original.status, "UNVERIFIED");
|
||||
assert.equal(
|
||||
report.cases[0].original.reason,
|
||||
mode === "hang" ? "timeout" : "output-limit",
|
||||
);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
test("rejects duplicate IDs and output path traversal before target execution", () =>
|
||||
scope(async (options) => {
|
||||
await assert.rejects(
|
||||
runFileDifferentialBenchmark({
|
||||
...options,
|
||||
cases: [options.cases[0], options.cases[0]],
|
||||
}),
|
||||
/unique/u,
|
||||
);
|
||||
await assert.rejects(
|
||||
runFileDifferentialBenchmark({
|
||||
...options,
|
||||
cases: [{ ...options.cases[0], outputName: "../escape" }],
|
||||
}),
|
||||
/single file/u,
|
||||
);
|
||||
}));
|
||||
|
||||
test("pre-cancelled suite cannot run or pass", () =>
|
||||
scope(async (options) => {
|
||||
const abort = new AbortController();
|
||||
abort.abort();
|
||||
await assert.rejects(
|
||||
runFileDifferentialBenchmark({ ...options, signal: abort.signal }),
|
||||
{ name: "AbortError" },
|
||||
);
|
||||
}));
|
||||
|
||||
test("retains completed first case when second input is missing", () =>
|
||||
scope(async (options) => {
|
||||
let failure;
|
||||
try {
|
||||
await runFileDifferentialBenchmark({
|
||||
...options,
|
||||
cases: [
|
||||
options.cases[0],
|
||||
{
|
||||
id: "absent",
|
||||
inputPath: join(options.outputDirectory, "missing.dat"),
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (cause) {
|
||||
failure = cause;
|
||||
}
|
||||
assert.equal(failure?.code, "ENOENT");
|
||||
const report = JSON.parse(await readFile(failure.reportPath, "utf8"));
|
||||
assert.equal(report.status, "UNVERIFIED");
|
||||
assert.equal(report.phase, "interrupted");
|
||||
assert.equal(report.cases[0].status, "PASS");
|
||||
assert.equal(
|
||||
report.cases[0].original.output.sha256,
|
||||
report.cases[0].input.sha256,
|
||||
);
|
||||
assert.equal(report.cases[1].phase, "reading-input");
|
||||
assert.equal(report.failure.reason, "ENOENT");
|
||||
}));
|
||||
|
||||
test("cancellation between cases preserves earlier real observations", () =>
|
||||
scope(async (options) => {
|
||||
const abort = new AbortController();
|
||||
let failure;
|
||||
try {
|
||||
await runFileDifferentialBenchmark({
|
||||
...options,
|
||||
signal: abort.signal,
|
||||
cases: [options.cases[0], { ...options.cases[0], id: "second" }],
|
||||
onProgress(report) {
|
||||
if (report.cases[0].phase === "complete") abort.abort();
|
||||
},
|
||||
});
|
||||
} catch (cause) {
|
||||
failure = cause;
|
||||
}
|
||||
assert.equal(failure?.name, "AbortError");
|
||||
const report = JSON.parse(await readFile(failure.reportPath, "utf8"));
|
||||
assert.equal(report.status, "UNVERIFIED");
|
||||
assert.equal(report.phase, "cancelled");
|
||||
assert.equal(report.cases[0].status, "PASS");
|
||||
assert.equal(report.cases[1].phase, "pending");
|
||||
assert.equal(report.cases[1].original, undefined);
|
||||
}));
|
||||
|
||||
test("failure projection preserves original owner error and prior evidence (simulated fault)", () =>
|
||||
scope(async (options) => {
|
||||
// This tests exceptional journal propagation, not real cleanup failure.
|
||||
const fault = new Error("simulated unresolved cleanup");
|
||||
fault.cleanupOwner = { simulated: true };
|
||||
fault.cleanupFailure = "simulated failure";
|
||||
fault.resources = ["simulated-owned-resource"];
|
||||
let failure;
|
||||
try {
|
||||
await runFileDifferentialBenchmark({
|
||||
...options,
|
||||
cases: [options.cases[0], { ...options.cases[0], id: "second" }],
|
||||
onProgress(report) {
|
||||
if (report.cases[1].phase === "original-captured") throw fault;
|
||||
},
|
||||
});
|
||||
} catch (cause) {
|
||||
failure = cause;
|
||||
}
|
||||
assert.equal(failure, fault);
|
||||
assert.equal(failure.cleanupOwner.simulated, true);
|
||||
const report = JSON.parse(await readFile(failure.reportPath, "utf8"));
|
||||
assert.equal(report.status, "UNVERIFIED");
|
||||
assert.equal(report.cases[0].status, "PASS");
|
||||
assert.equal(report.cases[1].original.status, "OBSERVED");
|
||||
assert.equal(report.failure.unresolvedCleanupOwner, true);
|
||||
assert.equal(report.failure.cleanupFailure, "simulated failure");
|
||||
assert.deepEqual(report.failure.resources, ["simulated-owned-resource"]);
|
||||
}));
|
||||
@@ -102,11 +102,20 @@ export async function verifyTicTacToe(
|
||||
captured.push({ e, c, raw });
|
||||
}
|
||||
const [a, b] = captured;
|
||||
const boards = a.raw
|
||||
.toString()
|
||||
.split(/\r?\n/)
|
||||
.filter((l) => /^\d+:(-1|0|1|2):/.test(l));
|
||||
assert.equal(boards.length, 19683);
|
||||
const boardRecords = (raw) =>
|
||||
raw
|
||||
.toString()
|
||||
.split(/\r+\n/)
|
||||
.filter((line) => /^\d+:(-1|0|1|2):/.test(line));
|
||||
const originalBoards = boardRecords(a.raw);
|
||||
const replicaBoards = boardRecords(b.raw);
|
||||
assert.equal(originalBoards.length, 19683);
|
||||
assert.equal(replicaBoards.length, 19683);
|
||||
const boardRecordMismatchCount = originalBoards.reduce(
|
||||
(count, record, index) => count + Number(record !== replicaBoards[index]),
|
||||
0,
|
||||
);
|
||||
const boardRecordsEqual = boardRecordMismatchCount === 0;
|
||||
const frames = [];
|
||||
for (const name of (await readdir(join(output, "original-frames")))
|
||||
.filter((n) => n.endsWith(".png"))
|
||||
@@ -123,7 +132,7 @@ export async function verifyTicTacToe(
|
||||
assert.equal(h(await readFile(original)), hashBefore);
|
||||
const report = {
|
||||
status:
|
||||
a.raw.equals(b.raw) &&
|
||||
boardRecordsEqual &&
|
||||
frames.length === 6 &&
|
||||
frames.every((f) => f.pngBytesEqual)
|
||||
? "suite-passed"
|
||||
@@ -132,7 +141,9 @@ export async function verifyTicTacToe(
|
||||
originalSha256: hashBefore,
|
||||
replicaSha256: h(await readFile(replica)),
|
||||
probeSha256: h(await readFile(probe)),
|
||||
boardStates: boards.length,
|
||||
boardStates: originalBoards.length,
|
||||
boardRecordsEqual,
|
||||
boardRecordMismatchCount,
|
||||
terminalBytesEqual: a.raw.equals(b.raw),
|
||||
originalTerminalSha256: h(a.raw),
|
||||
replicaTerminalSha256: h(b.raw),
|
||||
@@ -144,14 +155,15 @@ export async function verifyTicTacToe(
|
||||
limitations: [
|
||||
"Exhaustive board configurations cover only the CheckGameStatus method and observed indicator properties.",
|
||||
"Six images are actual Form.DrawToBitmap renders following real button handlers; not Windows OS screenshots.",
|
||||
"Raw PTY terminal bytes are retained separately from exact board-record comparisons; an extra carriage return in the terminal capture can make raw streams differ while all 19,683 board records and rendered frames match.",
|
||||
"Original and replica are executed on the same macOS ARM Mono/X11 runtime.",
|
||||
"AI, RNG, all menus, timing, sound and whole-game equivalence remain unverified.",
|
||||
"Probe executes original and reconstructed code; no game logic is implemented in the probe.",
|
||||
],
|
||||
};
|
||||
await save("report.json", report);
|
||||
return report;
|
||||
assert.equal(report.status, "suite-passed");
|
||||
return report;
|
||||
}
|
||||
if (
|
||||
process.argv[1] &&
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { systemDoctorHost } from "./Doctor.js";
|
||||
|
||||
it("threads the selected host and environment into system diagnostics", async () => {
|
||||
const environment = {
|
||||
HOME: "/selected/home",
|
||||
HOPPER_LAUNCHER_PATH: "/selected/bin/hopper",
|
||||
PATH: "/selected/bin",
|
||||
};
|
||||
const calls: Array<{
|
||||
command: string;
|
||||
arguments: readonly string[];
|
||||
environment: NodeJS.ProcessEnv | undefined;
|
||||
}> = [];
|
||||
const host = systemDoctorHost({
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
environment,
|
||||
execFileOutput: async (command, arguments_, options) => {
|
||||
calls.push({ command, arguments: arguments_, environment: options?.env });
|
||||
if (command === "sw_vers") return { stdout: "15.0\n", stderr: "" };
|
||||
if (command === "ldd") return { stdout: "linux-vdso.so.1", stderr: "" };
|
||||
return { stdout: "/selected/bin/rea\n", stderr: "" };
|
||||
},
|
||||
});
|
||||
|
||||
expect(host.homeDirectory).toBe("/selected/home");
|
||||
expect(host.platform).toBe("linux");
|
||||
expect(host.architecture).toBe("x64");
|
||||
await host.installationPaths?.();
|
||||
await host.macosVersion();
|
||||
await host.executable(process.execPath);
|
||||
|
||||
expect(calls.map(({ command }) => command)).toEqual([
|
||||
"which",
|
||||
"sw_vers",
|
||||
"ldd",
|
||||
]);
|
||||
expect(
|
||||
calls.every(({ environment: observed }) => observed === environment),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not report a directory as an available executable", async () => {
|
||||
const host = systemDoctorHost({
|
||||
platform: "darwin",
|
||||
architecture: "arm64",
|
||||
environment: {},
|
||||
execFileOutput: () => Promise.reject(new Error("unexpected command")),
|
||||
});
|
||||
|
||||
await expect(host.executable(dirname(process.execPath))).resolves.toBe(false);
|
||||
await expect(host.executable(process.execPath)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ default: { appdir: "/Applications" } }, "/Applications"],
|
||||
[
|
||||
{
|
||||
default: { appdir: "/Applications" },
|
||||
env: { appdir: "/Env Apps" },
|
||||
explicit: { appdir: "~/Tools" },
|
||||
},
|
||||
"HOME/Tools",
|
||||
],
|
||||
[
|
||||
{ default: { appdir: "/Applications" }, env: { appdir: "/Env Apps" } },
|
||||
"/Env Apps",
|
||||
],
|
||||
])(
|
||||
"locates the Hopper cask from its Caskroom receipt without a cask command (%j)",
|
||||
async (config, expectedAppdir) => {
|
||||
const root = await createTestTempDirectory("rea-doctor-caskroom-");
|
||||
const caskroom = join(root, "Caskroom");
|
||||
const metadata = join(caskroom, "hopper-disassembler", ".metadata");
|
||||
await mkdir(metadata, { recursive: true });
|
||||
await writeFile(join(metadata, "config.json"), JSON.stringify(config));
|
||||
const calls: (readonly string[])[] = [];
|
||||
const host = systemDoctorHost({
|
||||
platform: "darwin",
|
||||
environment: { HOME: join(root, "home") },
|
||||
execFileOutput: async (_command, arguments_) => {
|
||||
calls.push(arguments_);
|
||||
return { stdout: `${caskroom}\n`, stderr: "" };
|
||||
},
|
||||
});
|
||||
|
||||
expect(await host.brewHopperPath()).toBe(
|
||||
join(
|
||||
expectedAppdir.replace("HOME", join(root, "home")),
|
||||
"Hopper Disassembler.app/Contents/MacOS/hopper",
|
||||
),
|
||||
);
|
||||
// A cask argument makes Homebrew fetch and cache its API data.
|
||||
expect(calls).toEqual([["--caskroom"]]);
|
||||
},
|
||||
);
|
||||
|
||||
it("reports no Homebrew Hopper when the cask has no receipt", async () => {
|
||||
const caskroom = await createTestTempDirectory("rea-doctor-caskroom-");
|
||||
const calls: (readonly string[])[] = [];
|
||||
const host = systemDoctorHost({
|
||||
platform: "darwin",
|
||||
environment: { HOME: caskroom },
|
||||
execFileOutput: async (_command, arguments_) => {
|
||||
calls.push(arguments_);
|
||||
return { stdout: `${caskroom}\n`, stderr: "" };
|
||||
},
|
||||
});
|
||||
|
||||
expect(await host.brewHopperPath()).toBeUndefined();
|
||||
expect(
|
||||
calls.every((arguments_) => arguments_.join(" ") === "--caskroom"),
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -0,0 +1,174 @@
|
||||
import type { FileHandle } from "node:fs/promises";
|
||||
import type {
|
||||
MipsAbiFlags,
|
||||
MipsElfMetadata,
|
||||
} from "../domain/binaryTargetTypes.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
|
||||
/** Observed ABI declarations and coverage of their source tables. */
|
||||
export interface MipsAbiFlagsInspection {
|
||||
readonly abiFlags: MipsAbiFlags | null;
|
||||
readonly limitations: readonly string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read ELF32 MIPS ABI declarations from the caller's already-open file.
|
||||
* Inspect both typed tables (not section names), including records beyond the
|
||||
* initial header probe. A missing record is unknown, not a default FP ABI.
|
||||
* The caller supplies its cancellation check so its tagged error is preserved.
|
||||
*/
|
||||
export const readMipsElfAbiFlags = async (
|
||||
handle: FileHandle,
|
||||
metadata: MipsElfMetadata,
|
||||
checkCancelled: () => void,
|
||||
): Promise<Result<MipsAbiFlagsInspection, string>> => {
|
||||
checkCancelled();
|
||||
if (metadata.elfClass !== 32)
|
||||
return err("MIPS ABI record inspection currently supports ELF32 only");
|
||||
const fileSize = (await handle.stat()).size;
|
||||
checkCancelled();
|
||||
const read = async (
|
||||
offset: number,
|
||||
length: number,
|
||||
label: string,
|
||||
): Promise<Result<Buffer, string>> => {
|
||||
checkCancelled();
|
||||
if (
|
||||
!Number.isSafeInteger(fileSize) ||
|
||||
offset < 0 ||
|
||||
length < 0 ||
|
||||
offset > fileSize - length
|
||||
)
|
||||
return err(`MIPS ${label} lies outside the file`);
|
||||
const bytes = Buffer.alloc(length);
|
||||
let total = 0;
|
||||
while (total < length) {
|
||||
checkCancelled();
|
||||
const observed = await handle.read(
|
||||
bytes,
|
||||
total,
|
||||
length - total,
|
||||
offset + total,
|
||||
);
|
||||
checkCancelled();
|
||||
if (observed.bytesRead === 0) return err(`truncated MIPS ${label}`);
|
||||
total += observed.bytesRead;
|
||||
}
|
||||
return ok(bytes);
|
||||
};
|
||||
const header = await read(0, 52, "ELF32 header");
|
||||
if (!header.ok) return header;
|
||||
const bytes = header.value;
|
||||
const little = metadata.byteOrder === "little";
|
||||
const u16 = (data: Buffer, offset: number): number =>
|
||||
little ? data.readUInt16LE(offset) : data.readUInt16BE(offset);
|
||||
const u32 = (data: Buffer, offset: number): number =>
|
||||
little ? data.readUInt32LE(offset) : data.readUInt32BE(offset);
|
||||
if (
|
||||
bytes.toString("hex", 0, 4) !== "7f454c46" ||
|
||||
bytes[4] !== 1 ||
|
||||
bytes[5] !== (little ? 1 : 2) ||
|
||||
u16(bytes, 18) !== 8 ||
|
||||
u16(bytes, 16) !== metadata.type ||
|
||||
u32(bytes, 36) !== metadata.flags
|
||||
)
|
||||
return err("MIPS ELF header no longer matches the resolved identity");
|
||||
if (bytes[6] !== 1 || u32(bytes, 20) !== 1 || u16(bytes, 40) !== 52)
|
||||
return err("invalid MIPS ELF32 version or header size");
|
||||
|
||||
const tables = [
|
||||
{
|
||||
label: "program header table",
|
||||
offset: u32(bytes, 28),
|
||||
count: u16(bytes, 44),
|
||||
stride: u16(bytes, 42),
|
||||
entrySize: 32,
|
||||
typeOffset: 0,
|
||||
dataOffset: 4,
|
||||
sizeOffset: 16,
|
||||
recordType: 0x70000003, // PT_MIPS_ABIFLAGS
|
||||
optional: false,
|
||||
},
|
||||
{
|
||||
label: "section header table",
|
||||
offset: u32(bytes, 32),
|
||||
count: u16(bytes, 48),
|
||||
stride: u16(bytes, 46),
|
||||
entrySize: 40,
|
||||
typeOffset: 4,
|
||||
dataOffset: 16,
|
||||
sizeOffset: 20,
|
||||
recordType: 0x7000002a, // SHT_MIPS_ABIFLAGS
|
||||
optional: true,
|
||||
},
|
||||
];
|
||||
// Extended numbering needs a separate section-zero reader. Refuse it rather
|
||||
// than silently treating an unexamined table as having no ABI declarations.
|
||||
if (u16(bytes, 44) === 0xffff || u16(bytes, 48) >= 0xff00)
|
||||
return err("extended or reserved MIPS ELF table numbering is unsupported");
|
||||
let record: Buffer | undefined;
|
||||
const limitations: string[] = [];
|
||||
for (const table of tables) {
|
||||
if (table.count === 0) {
|
||||
if (table.offset !== 0)
|
||||
return err(`unsupported extended or inconsistent MIPS ${table.label}`);
|
||||
continue;
|
||||
}
|
||||
if (table.offset < 52 || table.stride !== table.entrySize)
|
||||
return err(`invalid MIPS ${table.label} offset or entry size`);
|
||||
const count = table.optional
|
||||
? Math.min(
|
||||
table.count,
|
||||
Math.max(0, Math.floor((fileSize - table.offset) / table.entrySize)),
|
||||
)
|
||||
: table.count;
|
||||
if (count < table.count)
|
||||
limitations.push(
|
||||
`MIPS ${table.label} at file offset ${table.offset} extends beyond the captured file: examined ${count} of ${table.count} complete headers. ABI declarations in unavailable headers remain unknown.`,
|
||||
);
|
||||
if (count === 0) continue;
|
||||
// Counts are bounded by the ELF16 fields and strides by their exact ELF32
|
||||
// structure sizes. Never allocate according to an untrusted section size.
|
||||
const contents = await read(
|
||||
table.offset,
|
||||
count * table.entrySize,
|
||||
table.label,
|
||||
);
|
||||
if (!contents.ok) return contents;
|
||||
let found = false;
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
checkCancelled();
|
||||
const position = i * table.entrySize;
|
||||
if (u32(contents.value, position + table.typeOffset) !== table.recordType)
|
||||
continue;
|
||||
if (found) return err(`duplicate MIPS ABI records in ${table.label}`);
|
||||
found = true;
|
||||
const offset = u32(contents.value, position + table.dataOffset);
|
||||
const size = u32(contents.value, position + table.sizeOffset);
|
||||
if (size !== 24)
|
||||
return err("unsupported MIPS ABI record size; expected 24 bytes");
|
||||
const candidate = await read(offset, 24, "ABI record");
|
||||
if (!candidate.ok) return candidate;
|
||||
if (record !== undefined && !record.equals(candidate.value))
|
||||
return err("conflicting MIPS program/section ABI declarations");
|
||||
record = candidate.value;
|
||||
}
|
||||
}
|
||||
if (record === undefined) return ok({ abiFlags: null, limitations });
|
||||
// Preserve declarations verbatim, including unknown versions/values. Provider
|
||||
// admission, not parsing, decides which interpretations have been verified.
|
||||
const abiFlags: MipsAbiFlags = {
|
||||
version: u16(record, 0),
|
||||
isaLevel: record.readUInt8(2),
|
||||
isaRevision: record.readUInt8(3),
|
||||
gprSize: record.readUInt8(4),
|
||||
cpr1Size: record.readUInt8(5),
|
||||
cpr2Size: record.readUInt8(6),
|
||||
fpAbi: record.readUInt8(7),
|
||||
isaExtension: u32(record, 8),
|
||||
ases: u32(record, 12),
|
||||
flags1: u32(record, 16),
|
||||
flags2: u32(record, 20),
|
||||
};
|
||||
return ok({ abiFlags, limitations });
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { join, resolve } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import {
|
||||
runUpdate,
|
||||
@@ -25,7 +26,7 @@ class FakeUpdateHost implements UpdateHost {
|
||||
installationResult: UpdateInstallation | undefined = {
|
||||
manager: "npm",
|
||||
prefix: "/fixture",
|
||||
packageRoot: "/fixture/lib/node_modules/rea-agents",
|
||||
packageRoot: `/fixture/lib/node_modules/${PRODUCT_IDENTITY.packageName}`,
|
||||
};
|
||||
installResult: Result<void, string> = ok(undefined);
|
||||
verified: Result<string, string> = ok("3.3.0");
|
||||
@@ -121,7 +122,7 @@ describe("REA update", () => {
|
||||
"--ignore-scripts",
|
||||
"--prefix",
|
||||
"/fixture",
|
||||
"rea-agents@3.3.0",
|
||||
`${PRODUCT_IDENTITY.packageName}@3.3.0`,
|
||||
],
|
||||
});
|
||||
expect(host.installedVersions).toEqual(["3.3.0"]);
|
||||
@@ -134,7 +135,7 @@ describe("REA update", () => {
|
||||
host.installationResult = {
|
||||
manager: "bun",
|
||||
globalDirectory: "/home/user/.bun/install/global",
|
||||
packageRoot: "/home/user/.bun/install/global/node_modules/rea-agents",
|
||||
packageRoot: `/home/user/.bun/install/global/node_modules/${PRODUCT_IDENTITY.packageName}`,
|
||||
};
|
||||
expect(await runUpdate("3.2.1", host, "structured")).toMatchObject({
|
||||
status: "updated",
|
||||
@@ -145,7 +146,7 @@ describe("REA update", () => {
|
||||
"add",
|
||||
"--global",
|
||||
"--ignore-scripts",
|
||||
"rea-agents@3.3.0",
|
||||
`${PRODUCT_IDENTITY.packageName}@3.3.0`,
|
||||
],
|
||||
});
|
||||
expect(host.verifiedInstallation).toEqual(host.installationResult);
|
||||
@@ -320,17 +321,25 @@ describe("update failures and version boundaries", () => {
|
||||
"remediation",
|
||||
expect.not.stringContaining("npm install --global"),
|
||||
);
|
||||
expect(result).toHaveProperty(
|
||||
"remediation",
|
||||
expect.stringContaining(`npx ${PRODUCT_IDENTITY.packageSpecifier}`),
|
||||
);
|
||||
expect(host.installedVersions).toEqual([]);
|
||||
});
|
||||
|
||||
it("reports actual npm failures and skips verification", async () => {
|
||||
const host = new FakeUpdateHost();
|
||||
host.installResult = err("EACCES /fixture/lib/node_modules/rea-agents");
|
||||
host.installResult = err(
|
||||
`EACCES /fixture/lib/node_modules/${PRODUCT_IDENTITY.packageName}`,
|
||||
);
|
||||
const result = await runUpdate("3.2.1", host);
|
||||
expect(result).toMatchObject({ status: "failed", reason: "install" });
|
||||
expect(result).toHaveProperty(
|
||||
"remediation",
|
||||
expect.stringContaining("EACCES /fixture/lib/node_modules/rea-agents"),
|
||||
expect.stringContaining(
|
||||
`EACCES /fixture/lib/node_modules/${PRODUCT_IDENTITY.packageName}`,
|
||||
),
|
||||
);
|
||||
expect(host.verifiedInstallation).toBeUndefined();
|
||||
});
|
||||
@@ -344,14 +353,14 @@ describe("npm installation ownership", () => {
|
||||
});
|
||||
|
||||
it("identifies the global prefix and package root", async () => {
|
||||
const packageRoot = "/usr/local/lib/node_modules/rea-agents";
|
||||
const packageRoot = `/usr/local/lib/node_modules/${PRODUCT_IDENTITY.packageName}`;
|
||||
await expect(
|
||||
detectNpmInstallation(packageRoot, host("/usr/local/lib/node_modules")),
|
||||
).resolves.toEqual({ manager: "npm", prefix: "/usr/local", packageRoot });
|
||||
});
|
||||
|
||||
it("retains the curl installer's custom Unix prefix", async () => {
|
||||
const packageRoot = "/home/user/.local/lib/node_modules/rea-agents";
|
||||
const packageRoot = `/home/user/.local/lib/node_modules/${PRODUCT_IDENTITY.packageName}`;
|
||||
await expect(
|
||||
detectNpmInstallation(packageRoot, host("/usr/local/lib/node_modules")),
|
||||
).resolves.toEqual({
|
||||
@@ -363,8 +372,8 @@ describe("npm installation ownership", () => {
|
||||
|
||||
it.each([
|
||||
"/work/rea",
|
||||
"/work/node_modules/rea-agents",
|
||||
"/cache/_npx/abc/node_modules/rea-agents",
|
||||
`/work/node_modules/${PRODUCT_IDENTITY.packageName}`,
|
||||
`/cache/_npx/abc/node_modules/${PRODUCT_IDENTITY.packageName}`,
|
||||
])("rejects unrelated invocation %s", async (packageRoot) => {
|
||||
await expect(
|
||||
detectNpmInstallation(packageRoot, host("/usr/local/lib/node_modules")),
|
||||
@@ -378,7 +387,7 @@ describe("npm installation ownership", () => {
|
||||
};
|
||||
await expect(
|
||||
detectNpmInstallation(
|
||||
"/home/.local/lib/node_modules/rea-agents",
|
||||
`/home/.local/lib/node_modules/${PRODUCT_IDENTITY.packageName}`,
|
||||
failing,
|
||||
),
|
||||
).resolves.toBeUndefined();
|
||||
@@ -387,7 +396,7 @@ describe("npm installation ownership", () => {
|
||||
|
||||
describe("Bun installation ownership", () => {
|
||||
const globalDirectory = "/home/user/.bun/install/global";
|
||||
const packageRoot = `${globalDirectory}/node_modules/rea-agents`;
|
||||
const packageRoot = `${globalDirectory}/node_modules/${PRODUCT_IDENTITY.packageName}`;
|
||||
// Fixture paths are POSIX; join() may produce Windows separators on Windows hosts.
|
||||
const host = (
|
||||
manifests: readonly string[] = [`${globalDirectory}/package.json`],
|
||||
@@ -410,9 +419,9 @@ describe("Bun installation ownership", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
"/home/user/project/node_modules/rea-agents",
|
||||
"/usr/local/lib/node_modules/rea-agents",
|
||||
"/home/user/.bun/install/cache/[email protected]@@@1",
|
||||
`/home/user/project/node_modules/${PRODUCT_IDENTITY.packageName}`,
|
||||
`/usr/local/lib/node_modules/${PRODUCT_IDENTITY.packageName}`,
|
||||
`/home/user/.bun/install/cache/${PRODUCT_IDENTITY.packageName}@${PRODUCT_IDENTITY.packageVersion}@@@1`,
|
||||
])("rejects package root %s outside the global project", async (root) => {
|
||||
await expect(
|
||||
detectBunInstallation(root, globalDirectory, host()),
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface NpmInstallation {
|
||||
/** Bun global installation that owns the running package and its entry point. */
|
||||
export interface BunInstallation {
|
||||
readonly manager: "bun";
|
||||
/** Bun's global project directory, which owns `node_modules/rea-agents`. */
|
||||
/** Bun's global project directory, which owns the package's `node_modules` entry. */
|
||||
readonly globalDirectory: string;
|
||||
readonly packageRoot: string;
|
||||
}
|
||||
@@ -202,8 +202,7 @@ const runUpdateWorkflow = async (
|
||||
currentVersion,
|
||||
latestVersion: targetVersion,
|
||||
reason: "unknown-install-method",
|
||||
remediation:
|
||||
"Update this installation with the package manager or source checkout that owns it. For a one-off latest invocation, use npx rea-agents@latest.",
|
||||
remediation: `Update this installation with the package manager or source checkout that owns it. For a one-off latest invocation, use npx ${PRODUCT_IDENTITY.packageSpecifier}.`,
|
||||
};
|
||||
const installed = await host.installVersion(
|
||||
installation,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
const installation = {
|
||||
manager: "npm",
|
||||
prefix: "/opt/rea",
|
||||
packageRoot: "/opt/rea/lib/node_modules/rea-agents",
|
||||
packageRoot: `/opt/rea/lib/node_modules/${PRODUCT_IDENTITY.packageName}`,
|
||||
} as const;
|
||||
|
||||
describe("npm latest release metadata", () => {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
captureProcessScenarioFile,
|
||||
projectProcessCliError,
|
||||
} from "./ProcessCli.js";
|
||||
import { JSON_BYTE_ORDER_MARK_MESSAGE } from "../Utf8JsonInput.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
roots.splice(0).map((root) => rm(root, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
const fixture = async (): Promise<string> => {
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-process-cli-diagnostics-"));
|
||||
roots.push(root);
|
||||
return root;
|
||||
};
|
||||
|
||||
describe("process CLI input diagnostics", () => {
|
||||
it("preserves local diagnostics for unexpected process failures", () => {
|
||||
const projected = projectProcessCliError(
|
||||
new Error("SECRET internal failure /private/path"),
|
||||
);
|
||||
expect(projected).toEqual({
|
||||
error: "Process command failed",
|
||||
category: "execution_failure",
|
||||
message:
|
||||
"Process command could not complete: SECRET internal failure /private/path",
|
||||
});
|
||||
expect(JSON.stringify(projected)).not.toContain("stack");
|
||||
});
|
||||
|
||||
it("projects missing, malformed JSON, and invalid scenario files", async () => {
|
||||
const root = await fixture();
|
||||
const malformed = join(root, "malformed.json");
|
||||
const invalid = join(root, "invalid.json");
|
||||
const invalidUtf8 = join(root, "invalid-utf8.json");
|
||||
await writeFile(malformed, "not-json");
|
||||
await writeFile(invalid, "{}");
|
||||
await writeFile(
|
||||
invalidUtf8,
|
||||
Buffer.concat([
|
||||
Buffer.from('{"executable":"'),
|
||||
Buffer.from([0xff]),
|
||||
Buffer.from('"}'),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(
|
||||
await captureProcessScenarioFile(join(root, "missing.json")),
|
||||
).toEqual({
|
||||
error: "Process command failed",
|
||||
category: "invalid_input",
|
||||
message: expect.stringContaining(
|
||||
`Process input file could not be read: ${join(root, "missing.json")} (ENOENT`,
|
||||
),
|
||||
});
|
||||
expect(await captureProcessScenarioFile(malformed)).toEqual({
|
||||
error: "Process command failed",
|
||||
category: "invalid_input",
|
||||
message: expect.stringContaining(
|
||||
`Process input file is not valid UTF-8 JSON: ${malformed}`,
|
||||
),
|
||||
});
|
||||
expect(await captureProcessScenarioFile(invalidUtf8)).toEqual({
|
||||
error: "Process command failed",
|
||||
category: "invalid_input",
|
||||
message: expect.stringContaining(
|
||||
`Process input file is not valid UTF-8 JSON: ${invalidUtf8}`,
|
||||
),
|
||||
});
|
||||
expect(await captureProcessScenarioFile(invalid)).toEqual({
|
||||
error: "Process command failed",
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
message: "Analysis input is invalid. Check the arguments and try again.",
|
||||
retryable: true,
|
||||
remediation: { action: "Correct the listed arguments and retry." },
|
||||
details: expect.objectContaining({
|
||||
operation: "capture_process_scenario",
|
||||
issues: expect.arrayContaining([
|
||||
expect.objectContaining({ path: ["executable"] }),
|
||||
]),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("names a leading byte-order mark instead of quoting it as a token", async () => {
|
||||
const path = join(await fixture(), "scenario.json");
|
||||
await writeFile(path, '\uFEFF{"executable":"/bin/echo"}');
|
||||
expect(await captureProcessScenarioFile(path)).toEqual({
|
||||
error: "Process command failed",
|
||||
category: "invalid_input",
|
||||
message: `Process input file is not valid UTF-8 JSON: ${path} (${JSON_BYTE_ORDER_MARK_MESSAGE}). Repair the file, then try again.`,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
lstat,
|
||||
mkdtemp,
|
||||
open,
|
||||
rm,
|
||||
symlink,
|
||||
unlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { expect, it } from "vitest";
|
||||
import { readStableArtifact } from "./readStableArtifact.js";
|
||||
|
||||
const checkChange = async (
|
||||
stage: "before-open" | "after-read" | "symlink-replacement",
|
||||
): Promise<void> => {
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-stable-read-"));
|
||||
const path = join(root, "selected");
|
||||
await writeFile(path, "original evidence");
|
||||
let inspected = 0;
|
||||
try {
|
||||
const reading = readStableArtifact(path, 1024, undefined, {
|
||||
async lstat(selected) {
|
||||
inspected++;
|
||||
if (stage === "after-read" && inspected === 2) await unlink(selected);
|
||||
const stat = await lstat(selected, { bigint: true });
|
||||
if (inspected === 1 && stage !== "after-read") {
|
||||
await unlink(selected);
|
||||
if (stage === "symlink-replacement")
|
||||
await symlink("missing", selected);
|
||||
}
|
||||
return stat;
|
||||
},
|
||||
open,
|
||||
});
|
||||
await expect(reading).rejects.toMatchObject({ reason: "integrity" });
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
};
|
||||
|
||||
it.each(["before-open", "after-read"] as const)(
|
||||
"retains an actual post-validation %s change as integrity failure",
|
||||
checkChange,
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"retains a post-validation symlink replacement as an integrity failure",
|
||||
() => checkChange("symlink-replacement"),
|
||||
);
|
||||
|
||||
it("keeps initially missing inputs distinguishable from later changes", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-stable-missing-"));
|
||||
try {
|
||||
await expect(
|
||||
readStableArtifact(join(root, "missing"), 1024),
|
||||
).rejects.toMatchObject({ code: "ENOENT" });
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import { visit } from "jsonc-parser";
|
||||
import { LosslessNumber } from "lossless-json";
|
||||
import { WEB_NETWORK_CAPTURE_LIMITS } from "../../domain/webNetworkCapture.js";
|
||||
import { CaptureFormatError } from "./CaptureFormatError.js";
|
||||
|
||||
type Frame =
|
||||
| {
|
||||
kind: "object";
|
||||
pointer: string;
|
||||
members: Map<string, unknown>;
|
||||
pendingKey?: string;
|
||||
}
|
||||
| { kind: "array"; pointer: string; items: unknown[] };
|
||||
|
||||
/** Materialize upstream JSON events without duplicate folding, prototype setters or numeric rounding. */
|
||||
export const parseHarJson = (text: string): unknown => {
|
||||
const frames: Frame[] = [];
|
||||
let result: unknown;
|
||||
const valuePointer = (): string => {
|
||||
const parent = frames.at(-1);
|
||||
if (parent === undefined) return "";
|
||||
if (parent.kind === "array")
|
||||
return `${parent.pointer}/${parent.items.length}`;
|
||||
return parent.pendingKey === undefined
|
||||
? parent.pointer
|
||||
: `${parent.pointer}/${parent.pendingKey.replaceAll("~", "~0").replaceAll("/", "~1")}`;
|
||||
};
|
||||
const checkDepth = (): void => {
|
||||
if (frames.length > WEB_NETWORK_CAPTURE_LIMITS.depth)
|
||||
throw new CaptureFormatError(
|
||||
"input-limit",
|
||||
"HAR exceeds the 64-level complete-evidence nesting budget.",
|
||||
valuePointer(),
|
||||
);
|
||||
};
|
||||
const append = (value: unknown): void => {
|
||||
const parent = frames.at(-1);
|
||||
if (parent === undefined) result = value;
|
||||
else if (parent.kind === "array") parent.items.push(value);
|
||||
else {
|
||||
if (parent.pendingKey === undefined)
|
||||
throw new TypeError(
|
||||
"JSON visitor omitted an object property identity.",
|
||||
);
|
||||
parent.members.set(parent.pendingKey, value);
|
||||
delete parent.pendingKey;
|
||||
}
|
||||
};
|
||||
visit(
|
||||
text,
|
||||
{
|
||||
onObjectBegin: () => {
|
||||
checkDepth();
|
||||
frames.push({
|
||||
kind: "object",
|
||||
pointer: valuePointer(),
|
||||
members: new Map(),
|
||||
});
|
||||
},
|
||||
onObjectProperty: (key) => {
|
||||
// Prototype-named members are kept as ordinary own properties: the
|
||||
// members map keys them without prototype setters, and
|
||||
// Object.fromEntries materializes them as own data properties.
|
||||
const frame = frames.at(-1);
|
||||
if (frame?.kind !== "object")
|
||||
throw new TypeError(
|
||||
"JSON visitor emitted a property outside an object.",
|
||||
);
|
||||
if (frame.members.has(key))
|
||||
throw new CaptureFormatError(
|
||||
"format",
|
||||
"HAR JSON contains duplicate object members.",
|
||||
);
|
||||
frame.pendingKey = key;
|
||||
},
|
||||
onObjectEnd: () => {
|
||||
const frame = frames.pop();
|
||||
if (frame?.kind !== "object")
|
||||
throw new TypeError("JSON visitor ended an unexpected object.");
|
||||
append(Object.fromEntries(frame.members));
|
||||
},
|
||||
onArrayBegin: () => {
|
||||
checkDepth();
|
||||
frames.push({ kind: "array", pointer: valuePointer(), items: [] });
|
||||
},
|
||||
onArrayEnd: () => {
|
||||
const frame = frames.pop();
|
||||
if (frame?.kind !== "array")
|
||||
throw new TypeError("JSON visitor ended an unexpected array.");
|
||||
append(frame.items);
|
||||
},
|
||||
onLiteralValue: (value: unknown, offset, length) => {
|
||||
checkDepth();
|
||||
if (typeof value === "number")
|
||||
append(new LosslessNumber(text.slice(offset, offset + length)));
|
||||
else if (
|
||||
value === null ||
|
||||
typeof value === "boolean" ||
|
||||
typeof value === "string"
|
||||
)
|
||||
append(value);
|
||||
else
|
||||
throw new TypeError("JSON visitor emitted an unsupported literal.");
|
||||
},
|
||||
onError: () => {
|
||||
throw new CaptureFormatError("format", "HAR is malformed JSON.");
|
||||
},
|
||||
},
|
||||
{
|
||||
disallowComments: true,
|
||||
allowTrailingComma: false,
|
||||
allowEmptyContent: false,
|
||||
},
|
||||
);
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import { fc, it } from "@fast-check/vitest";
|
||||
import { describe, expect } from "vitest";
|
||||
|
||||
import {
|
||||
createDeclaringTypeLookup,
|
||||
declaringType,
|
||||
type TypeRange,
|
||||
} from "./ManagedMemberInspectorCore.js";
|
||||
|
||||
const range = (
|
||||
name: string,
|
||||
fieldStart: number,
|
||||
fieldEnd: number,
|
||||
methodStart = fieldStart,
|
||||
methodEnd = fieldEnd,
|
||||
): TypeRange => ({
|
||||
token: name,
|
||||
fullName: name,
|
||||
fieldStart,
|
||||
fieldEnd,
|
||||
methodStart,
|
||||
methodEnd,
|
||||
});
|
||||
|
||||
describe("managed declaring type ownership", () => {
|
||||
it("resolves field and method boundaries independently, including empty types and gaps", () => {
|
||||
const ranges = [
|
||||
range("first", 1, 3, 1, 1),
|
||||
range("empty", 3, 3, 1, 1),
|
||||
range("last", 5, 7, 1, 4),
|
||||
];
|
||||
const field = createDeclaringTypeLookup(ranges, "field");
|
||||
const method = createDeclaringTypeLookup(ranges, "method");
|
||||
expect([0, 1, 2, 3, 4, 5, 6, 7].map(field)).toEqual([
|
||||
null,
|
||||
{ token: "first", fullName: "first" },
|
||||
{ token: "first", fullName: "first" },
|
||||
null,
|
||||
null,
|
||||
{ token: "last", fullName: "last" },
|
||||
{ token: "last", fullName: "last" },
|
||||
null,
|
||||
]);
|
||||
expect([0, 1, 2, 3, 4].map(method)).toEqual([
|
||||
null,
|
||||
{ token: "last", fullName: "last" },
|
||||
{ token: "last", fullName: "last" },
|
||||
{ token: "last", fullName: "last" },
|
||||
null,
|
||||
]);
|
||||
});
|
||||
|
||||
it("retains the first observed owner for overlapping, reversed, or out-of-order ranges", () => {
|
||||
const ranges = [
|
||||
range("first", 5, 9),
|
||||
range("overlap", 1, 7),
|
||||
range("reversed", 10, 2),
|
||||
];
|
||||
const lookup = createDeclaringTypeLookup(ranges, "method");
|
||||
expect(lookup(6)).toEqual({ token: "first", fullName: "first" });
|
||||
expect(lookup(2)).toEqual({ token: "overlap", fullName: "overlap" });
|
||||
expect(lookup(10)).toBeNull();
|
||||
expect(createDeclaringTypeLookup([], "method")(1)).toBeNull();
|
||||
});
|
||||
|
||||
it.prop([
|
||||
fc.array(
|
||||
fc.record({
|
||||
fieldStart: fc.integer({ min: 0, max: 30 }),
|
||||
fieldEnd: fc.integer({ min: 0, max: 30 }),
|
||||
methodStart: fc.integer({ min: 0, max: 30 }),
|
||||
methodEnd: fc.integer({ min: 0, max: 30 }),
|
||||
}),
|
||||
{ maxLength: 20 },
|
||||
),
|
||||
])(
|
||||
"agrees with observed first-match ownership for arbitrary metadata ranges",
|
||||
(raw) => {
|
||||
const ranges = raw.map((value, index) => ({
|
||||
...value,
|
||||
token: String(index),
|
||||
fullName: `Type${String(index)}`,
|
||||
}));
|
||||
for (const table of ["field", "method"] as const) {
|
||||
const lookup = createDeclaringTypeLookup(ranges, table);
|
||||
for (const row of [
|
||||
NaN,
|
||||
-Infinity,
|
||||
Infinity,
|
||||
...Array.from({ length: 32 }, (_, i) => i),
|
||||
])
|
||||
expect(lookup(row)).toEqual(declaringType(ranges, table, row));
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -49,6 +49,35 @@ export interface TypeRange {
|
||||
readonly methodEnd: number;
|
||||
}
|
||||
|
||||
export type ManagedMemberTable = "field" | "method";
|
||||
|
||||
/** Return the first TypeDef whose observed metadata range owns this row. */
|
||||
export const declaringType = (
|
||||
ranges: readonly TypeRange[],
|
||||
table: ManagedMemberTable,
|
||||
row: number,
|
||||
): Pick<TypeRange, "token" | "fullName"> | null => {
|
||||
if (!Number.isSafeInteger(row) || row < 1) return null;
|
||||
const startKey = table === "field" ? "fieldStart" : "methodStart";
|
||||
const endKey = table === "field" ? "fieldEnd" : "methodEnd";
|
||||
for (const range of ranges) {
|
||||
const start = range[startKey];
|
||||
const end = range[endKey];
|
||||
if (start <= row && row < end)
|
||||
return { token: range.token, fullName: range.fullName };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/** Build an immutable row lookup while preserving the input's first-match ownership. */
|
||||
export const createDeclaringTypeLookup =
|
||||
(
|
||||
ranges: readonly TypeRange[],
|
||||
table: ManagedMemberTable,
|
||||
): ((row: number) => Pick<TypeRange, "token" | "fullName"> | null) =>
|
||||
(row) =>
|
||||
declaringType(ranges, table, row);
|
||||
|
||||
export interface MethodCore {
|
||||
readonly token: string;
|
||||
readonly name: string;
|
||||
|
||||
+5
-1
@@ -212,7 +212,11 @@
|
||||
"scripts/generate-completion-ledger.mjs",
|
||||
"scripts/lib/generated-file.mjs",
|
||||
"scripts/verify/managed/**",
|
||||
"scripts/fixtures/managed/**"
|
||||
"scripts/fixtures/managed/**",
|
||||
{
|
||||
"mode": "dependencyOutputs",
|
||||
"globs": ["skills/**"]
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
"docs/verification/managed-conformance-manifest.json",
|
||||
|
||||
Reference in new issue
Block a user