mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
test: strengthen integration oracles and verification (#8295)
* test: strengthen integration oracles and verification * test: bound lifecycle gates and strengthen workflow checks * test: verify complete paged and extracted results * test: preserve bytecode and cancellation evidence * test: strengthen retained evidence and fixture cleanup * test: require cross-projection bridge identities * test: retain fixture owners through cleanup failures * test: verify graph examples and project selectors * test: tighten provider and lifecycle regressions * test: consolidate hex coverage and bound cutter cleanup * test: bound subprocess output waits and strengthen graph checks * test: check mcp recovery and preserve cleanup owners * test: verify structured mcp argument failures * test: preserve artifact cleanup and gdb limit evidence * test: preserve evidence identities and bound provider gates * test: retain browser and comparison cleanup owners * test: bound startup and strengthen fixture oracles * test: bound lifecycle gates and verify recovered evidence * test: verify public outcomes and lifecycle ownership * test: assert precise evidence and release lifecycle gates * test: verify selected facts and bound cleanup waits * test: check retained evidence and decoded records * test: verify final reports and retain cleanup ownership * test: bound reviewed startup gates and retain cleanup * test: verify PE errors and preserve MCP cleanup * test: preserve MCP fixture cleanup ownership * test: release provider gates before late startup
This commit is contained in:
1 parent
2d5dec26c3
commit
60149282fc
342 files changed
+8844
-6279
No files matched your search
@@ -53,3 +53,8 @@ jobs:
|
||||
env:
|
||||
REA_BROWSER_EXECUTABLE: /usr/bin/google-chrome
|
||||
REA_BROWSER_NO_SANDBOX: "true"
|
||||
# The preceding verifier builds the runtime used by this request journey.
|
||||
- run: dbus-run-session -- node scripts/verify-browser-request.mjs
|
||||
env:
|
||||
REA_BROWSER_EXECUTABLE: /usr/bin/google-chrome
|
||||
REA_BROWSER_NO_SANDBOX: "true"
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
mkdir -p .cache
|
||||
rm -f "$report"
|
||||
node scripts/run-exact-tests.mjs run --reporter=default --reporter=json --outputFile.json="$report" \
|
||||
tests/boundary/captures/fieldDiscovery.test.ts src/process/LinuxResourceLimits.test.ts tests/boundary/captures/captureProvider.test.ts tests/boundary/captures/captureDescriptorCleanup.test.ts tests/boundary/pcap/pcapProvider.test.ts tests/boundary/pcap/pcapProviderCrossPlatform.test.ts tests/boundary/captures/resourceLimits.test.ts tests/boundary/captures/packetOutput.test.ts tests/boundary/mcp/packetCaptureMcp.test.ts tests/boundary/cli/captureCleanup.test.ts
|
||||
tests/boundary/captures/fieldDiscovery.test.ts src/process/LinuxResourceLimits.test.ts tests/boundary/captures/captureProvider.test.ts tests/boundary/captures/captureDescriptorCleanup.test.ts tests/boundary/pcap/pcapProvider.test.ts tests/boundary/pcap/pcapProviderCrossPlatform.test.ts tests/boundary/captures/resourceLimits.test.ts tests/boundary/captures/packetOutput.test.ts tests/boundary/mcp/packetCaptureMcp.test.ts tests/boundary/cli/captureCleanup.test.ts tests/acceptance/pcapVerifier.test.ts
|
||||
node scripts/ci/require-provider-tests.mjs packet "$report"
|
||||
- run: node scripts/verify-packet-capture.mjs
|
||||
env:
|
||||
|
||||
+1
-1
@@ -291,7 +291,7 @@
|
||||
"verify:native-ui:permissions": "npm run build:cached && node scripts/verify-native-ui.mjs --allow-permission-denial",
|
||||
"test:focused": "node scripts/run-development-tests.mjs focused",
|
||||
"test:sqlite": "npm run test:prepare && npm run test:sqlite:run",
|
||||
"test:sqlite:run": "vitest run src/application/sqlite/ tests/boundary/sqlite/ tests/acceptance/sqliteDatabaseCli.test.ts tests/boundary/mcp/serverLifecycleMcp.test.ts",
|
||||
"test:sqlite:run": "node scripts/run-exact-tests.mjs run tests/acceptance/sqliteDatabaseCli.test.ts tests/boundary/mcp/serverLifecycleMcp.test.ts && vitest run src/application/sqlite/ tests/boundary/sqlite/",
|
||||
"build:windows-native": "node scripts/build-windows-native.mjs",
|
||||
"verify:windows-native:artifact": "node scripts/verify-windows-native-artifact.mjs",
|
||||
"prepublishOnly": "npm run verify:windows-native:artifact",
|
||||
|
||||
@@ -19,6 +19,9 @@ const descriptors = [
|
||||
);
|
||||
const requirements = {
|
||||
packet: {
|
||||
"tests/acceptance/pcapVerifier.test.ts": [
|
||||
"verifies complete and unresolved HTTP coverage with real Wireshark tools",
|
||||
],
|
||||
"tests/boundary/captures/resourceLimits.test.ts": [
|
||||
...resources,
|
||||
"reports a real kernel file-limit termination with its effective ceiling",
|
||||
|
||||
@@ -28,6 +28,8 @@ const PATH_SCOPES = {
|
||||
"tests/boundary/cli/captureCleanup.test.ts",
|
||||
"tests/boundary/mcp/packetCaptureMcp.test.ts",
|
||||
"tests/boundary/pcap/**",
|
||||
"tests/acceptance/pcapVerifier.test.ts",
|
||||
"tests/fixtures/pcap/offlineHttpCapture.ts",
|
||||
"src/captures/**",
|
||||
"src/pcap/**",
|
||||
"src/domain/pcap/**",
|
||||
@@ -43,6 +45,7 @@ const PATH_SCOPES = {
|
||||
"src/server/registerCaptureTools.ts",
|
||||
...CAPTURE_SHARED_INPUTS,
|
||||
"scripts/verify-packet-capture.mjs",
|
||||
"scripts/verify-pcap.mjs",
|
||||
".github/workflows/real-packet-capture.yml",
|
||||
],
|
||||
signal_recording: [
|
||||
@@ -351,20 +354,30 @@ const PATH_SCOPES = {
|
||||
".github/workflows/real-browser.yml",
|
||||
"scripts/verify-real-browser.mjs",
|
||||
"scripts/verify-browser-network.mjs",
|
||||
"scripts/verify-browser-request.mjs",
|
||||
"scripts/verify-browser-scripts.mjs",
|
||||
"scripts/verify-browser-modules.mjs",
|
||||
"scripts/verify-browser-dom-destinations.mjs",
|
||||
"scripts/lib/browser-*.mjs",
|
||||
"scripts/fixtures/browser-network-site.mjs",
|
||||
"scripts/fixtures/browser-request-site.mjs",
|
||||
"scripts/fixtures/browser-script-site.mjs",
|
||||
"scripts/fixtures/browser-module-site.mjs",
|
||||
"scripts/fixtures/browser-verifier-site.mjs",
|
||||
"scripts/fixtures/page-cdp-proxy.mjs",
|
||||
"src/browser/**",
|
||||
"src/domain/browser*.ts",
|
||||
"src/domain/webCaptureDiffSchemas.ts",
|
||||
"src/domain/webScreenshot.ts",
|
||||
"src/domain/webScript*.ts",
|
||||
"src/domain/webModule*.ts",
|
||||
"src/domain/analysisView/environment*.ts",
|
||||
"src/domain/analysisView/{EnvironmentManifest*,EnvironmentState*}.ts",
|
||||
"src/domain/analysisView/{scenarioCaptureRecords,recordedResponseBody,recordedExchangeBody,recordedMediaType,webNetworkRouteTemplates,WebNetworkProjectionAccumulator}.ts",
|
||||
"src/application/BrowserScenario*.ts",
|
||||
"src/application/environment/**",
|
||||
"src/application/WebModule*.ts",
|
||||
"src/composition/browserScenario.ts",
|
||||
"src/composition/webModules.ts",
|
||||
"src/artifacts/readStableArtifact.ts",
|
||||
"src/artifacts/ArtifactPaths.ts",
|
||||
@@ -374,6 +387,8 @@ const PATH_SCOPES = {
|
||||
"src/server/registerWebModuleTool.ts",
|
||||
"src/server/createServer.ts",
|
||||
"src/cli/webModuleCommands.ts",
|
||||
"src/cliBrowserScenarioCommands.ts",
|
||||
"src/cli/environmentCommands.ts",
|
||||
"src/cli.ts",
|
||||
"src/cliCommandNames.ts",
|
||||
"src/contracts/toolEffects.ts",
|
||||
@@ -564,6 +579,8 @@ const PATH_SCOPES = {
|
||||
".github/workflows/real-web-source-map.yml",
|
||||
"scripts/verify-browser-source-maps.mjs",
|
||||
"scripts/lib/browser-source-map-e2e.mjs",
|
||||
"scripts/lib/browser-nested-source-map-e2e.mjs",
|
||||
"scripts/lib/browser-runtime-fixture-lifecycle.mjs",
|
||||
"scripts/fixtures/browser-source-map-site.mjs",
|
||||
"src/javascript/sourceMaps/**",
|
||||
"src/browser/assets/**",
|
||||
|
||||
@@ -94,6 +94,12 @@ export async function startBrowserRequestSite() {
|
||||
writes.push({
|
||||
method: request.method,
|
||||
path: url.pathname,
|
||||
query: url.search,
|
||||
body,
|
||||
contentType: request.headers["content-type"],
|
||||
cookie:
|
||||
request.headers.cookie?.includes(`csrftoken=${requestFixtureCsrf}`) ??
|
||||
false,
|
||||
csrf: request.headers["x-csrftoken"] === requestFixtureCsrf,
|
||||
bearer:
|
||||
request.headers.authorization === `Bearer ${requestFixtureToken}`,
|
||||
|
||||
@@ -67,10 +67,13 @@ const writeSession = (executable, origin) =>
|
||||
read("list-1", "/api/items"),
|
||||
read("list-2", "/api/items"),
|
||||
read("item-1", "/api/items/1"),
|
||||
write("create-1", "POST", "/api/items", {
|
||||
json: { title: "Scratch item 1" },
|
||||
status: 201,
|
||||
}),
|
||||
{
|
||||
...write("create-1", "POST", "/api/items", {
|
||||
json: { title: "Scratch item 1" },
|
||||
status: 201,
|
||||
}),
|
||||
query: [{ name: "org", value: { source: "literal", value: "1" } }],
|
||||
},
|
||||
read("created-1", "/api/items/3"),
|
||||
read("list-created-1", "/api/items"),
|
||||
write("create-2", "POST", "/api/items", {
|
||||
@@ -337,6 +340,16 @@ export async function verifyBrowserRequestSteps(
|
||||
assertRequestsAttributed(scenario, capture);
|
||||
assert.equal(site.writes.length, 6);
|
||||
assert.ok(site.writes.every(({ csrf, bearer }) => csrf && bearer));
|
||||
assert.deepEqual(site.writes[0], {
|
||||
method: "POST",
|
||||
path: "/api/items",
|
||||
query: "?org=1",
|
||||
body: { title: "Scratch item 1" },
|
||||
contentType: "application/json",
|
||||
cookie: true,
|
||||
csrf: true,
|
||||
bearer: true,
|
||||
});
|
||||
assertPageValuesRedacted(evidence, capture);
|
||||
|
||||
const capturePath = join(root.path, "items.capture.json");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { stat } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import {
|
||||
developmentTestPlan,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
let child;
|
||||
let forceTimer;
|
||||
let shutdownSignal;
|
||||
let childOwnsTree = false;
|
||||
const signalExitCodes = { SIGINT: 130, SIGTERM: 143 };
|
||||
for (const signal of Object.keys(signalExitCodes))
|
||||
process.on(signal, () => {
|
||||
@@ -17,8 +19,10 @@ for (const signal of Object.keys(signalExitCodes))
|
||||
shutdownSignal = signal;
|
||||
if (child !== undefined && child.exitCode === null) {
|
||||
child.kill(signal);
|
||||
forceTimer = setTimeout(() => child?.kill("SIGKILL"), 1_000);
|
||||
forceTimer.unref?.();
|
||||
if (!childOwnsTree) {
|
||||
forceTimer = setTimeout(() => child?.kill("SIGKILL"), 1_000);
|
||||
forceTimer.unref?.();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -55,28 +59,38 @@ try {
|
||||
throw new Error(
|
||||
"Test prerequisites require the locked dependency installation",
|
||||
);
|
||||
const prepared = await run(process.execPath, [
|
||||
"scripts/run-exclusive.mjs",
|
||||
"artifacts",
|
||||
const prepared = await run(
|
||||
process.execPath,
|
||||
resolve("scripts/run-turbo.mjs"),
|
||||
"run",
|
||||
...new Set([
|
||||
...(plan.needsBuild ? ["artifacts:skills"] : []),
|
||||
...plan.artifactTasks,
|
||||
]),
|
||||
]);
|
||||
[
|
||||
"scripts/run-exclusive.mjs",
|
||||
"artifacts",
|
||||
process.execPath,
|
||||
resolve("scripts/run-turbo.mjs"),
|
||||
"run",
|
||||
...new Set([
|
||||
...(plan.needsBuild ? ["artifacts:skills"] : []),
|
||||
...plan.artifactTasks,
|
||||
]),
|
||||
],
|
||||
false,
|
||||
"existing",
|
||||
);
|
||||
if (prepared.code !== 0) process.exitCode = prepared.code;
|
||||
}
|
||||
if (!process.exitCode && !shutdownSignal) {
|
||||
const tested = await run(process.execPath, [
|
||||
resolve(
|
||||
request.paths.length > 0
|
||||
? "scripts/run-exact-tests.mjs"
|
||||
: "node_modules/vitest/vitest.mjs",
|
||||
),
|
||||
...plan.vitestArguments,
|
||||
]);
|
||||
const tested = await run(
|
||||
process.execPath,
|
||||
[
|
||||
resolve(
|
||||
request.paths.length > 0
|
||||
? "scripts/run-exact-tests.mjs"
|
||||
: "node_modules/vitest/vitest.mjs",
|
||||
),
|
||||
...plan.vitestArguments,
|
||||
],
|
||||
false,
|
||||
"test",
|
||||
);
|
||||
process.exitCode = tested.code;
|
||||
}
|
||||
}
|
||||
@@ -86,15 +100,27 @@ try {
|
||||
}
|
||||
if (shutdownSignal) process.exitCode = signalExitCodes[shutdownSignal];
|
||||
|
||||
function run(executable, arguments_, capture = false) {
|
||||
function run(executable, arguments_, capture = false, supervision) {
|
||||
if (shutdownSignal)
|
||||
return Promise.resolve({
|
||||
code: signalExitCodes[shutdownSignal],
|
||||
output: "",
|
||||
});
|
||||
child = spawn(executable, arguments_, {
|
||||
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
||||
});
|
||||
// Source-only tests do not require the built Windows Job Object artifact.
|
||||
const wrap = supervision === "test" && process.platform !== "win32";
|
||||
childOwnsTree = supervision === "existing" || wrap;
|
||||
child = spawn(
|
||||
wrap ? process.execPath : executable,
|
||||
wrap
|
||||
? [
|
||||
fileURLToPath(new URL("./run-exclusive.mjs", import.meta.url)),
|
||||
"--unlocked",
|
||||
executable,
|
||||
...arguments_,
|
||||
]
|
||||
: arguments_,
|
||||
{ stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit" },
|
||||
);
|
||||
let output = "";
|
||||
if (capture) {
|
||||
child.stdout.on("data", (data) => (output += data.toString()));
|
||||
|
||||
@@ -33,7 +33,7 @@ if (!process.connected) {
|
||||
process.exitCode = signalExitCodes.SIGTERM;
|
||||
} else if (
|
||||
typeof lockName !== "string" ||
|
||||
!/^[a-z][a-z0-9-]*$/u.test(lockName) ||
|
||||
!(lockName === "--unlocked" || /^[a-z][a-z0-9-]*$/u.test(lockName)) ||
|
||||
command.length === 0
|
||||
) {
|
||||
console.error("Usage: run-exclusive.mjs <lock-name> <command> [args...]");
|
||||
@@ -45,19 +45,21 @@ if (!process.connected) {
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await mkdir(lockRoot, { recursive: true });
|
||||
// SQLite owns acquisition and OS-backed lock release, including process
|
||||
// death. Keep the database pathname stable; deleting it would split owners.
|
||||
database = new DatabaseSync(join(lockRoot, `${lockName}.sqlite`));
|
||||
try {
|
||||
database.exec("BEGIN EXCLUSIVE");
|
||||
} catch (cause) {
|
||||
if (isBusyError(cause))
|
||||
throw new Error(
|
||||
`Another ${lockName} command is already running. Wait for it to finish.`,
|
||||
{ cause },
|
||||
);
|
||||
throw cause;
|
||||
if (lockName !== "--unlocked") {
|
||||
await mkdir(lockRoot, { recursive: true });
|
||||
// SQLite owns acquisition and OS-backed lock release, including process
|
||||
// death. Keep the database pathname stable; deleting it would split owners.
|
||||
database = new DatabaseSync(join(lockRoot, `${lockName}.sqlite`));
|
||||
try {
|
||||
database.exec("BEGIN EXCLUSIVE");
|
||||
} catch (cause) {
|
||||
if (isBusyError(cause))
|
||||
throw new Error(
|
||||
`Another ${lockName} command is already running. Wait for it to finish.`,
|
||||
{ cause },
|
||||
);
|
||||
throw cause;
|
||||
}
|
||||
}
|
||||
if (shutdownSignal === undefined) {
|
||||
const result = await runCommand();
|
||||
|
||||
@@ -79,7 +79,9 @@ await withVerifierCleanup(async () => {
|
||||
(entry) =>
|
||||
entry.response_address_count > entry.response_addresses.length,
|
||||
);
|
||||
assert.equal(result.coverage, partial ? "partial" : "complete");
|
||||
// Count discrepancies prove partial coverage, but matching counts do not
|
||||
// rule out unresolved HTTP targets or unavailable DNS identity extraction.
|
||||
if (partial) assert.equal(result.coverage, "partial");
|
||||
assert.deepEqual(
|
||||
await hashVerifierFile(path, resources),
|
||||
identity,
|
||||
|
||||
@@ -11,9 +11,6 @@ import {
|
||||
APK_SIGNATURE_V3_ID,
|
||||
LEAF_CERTIFICATE,
|
||||
LEAF_SHA256,
|
||||
LEAF_SUBJECT,
|
||||
OID_ECDSA_SHA256,
|
||||
OID_SHA256,
|
||||
OPENSSL_BLOCK_ISSUER_SERIAL,
|
||||
ROOT_CERTIFICATE,
|
||||
ROOT_SHA256,
|
||||
@@ -149,18 +146,6 @@ const reportOf = async (path: string) => {
|
||||
};
|
||||
|
||||
describe("ApkSignatureProvider without the Android SDK", () => {
|
||||
it("retains the declared digest's signature algorithm ID and bytes", async () => {
|
||||
const report = await reportOf(
|
||||
await write(
|
||||
"algorithm-id.apk",
|
||||
syntheticSigned([{ id: V2, value: bogusV2Value() }]),
|
||||
),
|
||||
);
|
||||
expect(report.schemes.v2?.signers?.[0]?.digests[0]).toMatchObject({
|
||||
signature_algorithm_id: 0x0103,
|
||||
declared_digest_hex: "ab".repeat(32),
|
||||
});
|
||||
});
|
||||
it("reports an unsigned ZIP as having no scheme", async () => {
|
||||
const r = await reportOf(
|
||||
await write(
|
||||
@@ -174,53 +159,6 @@ describe("ApkSignatureProvider without the Android SDK", () => {
|
||||
expect(r.posture.v1_only).toBe(false);
|
||||
});
|
||||
|
||||
it("detects v1-only signing and names the signer certificate its block declares", async () => {
|
||||
for (const stored of [false, true]) {
|
||||
const r = await reportOf(
|
||||
await write(
|
||||
`v1-${String(stored)}.apk`,
|
||||
await zipWith([
|
||||
{ name: "classes.dex", data: "x" },
|
||||
{ name: "META-INF/MANIFEST.MF", data: "Manifest-Version: 1.0" },
|
||||
{ name: "META-INF/CERT.SF", data: "Signature-Version: 1.0" },
|
||||
{
|
||||
name: "META-INF/CERT.EC",
|
||||
data: OPENSSL_BLOCK_ISSUER_SERIAL,
|
||||
stored,
|
||||
},
|
||||
]),
|
||||
),
|
||||
);
|
||||
expect(r.schemes.v1.present).toBe(true);
|
||||
expect(r.posture.v1_only).toBe(true);
|
||||
expect(r.posture.strongest_scheme).toBe("v1");
|
||||
expect(r.posture.v1_signers_match_block_signers).toBeNull();
|
||||
const [signer] = r.schemes.v1.signers;
|
||||
expect(signer).toMatchObject({
|
||||
signature_file: "META-INF/CERT.SF",
|
||||
block_file: "META-INF/CERT.EC",
|
||||
certificate: {
|
||||
sha256_fingerprint: LEAF_SHA256,
|
||||
subject: LEAF_SUBJECT,
|
||||
public_key_algorithm: "ec",
|
||||
},
|
||||
certificates: [
|
||||
{ sha256_fingerprint: LEAF_SHA256, subject: LEAF_SUBJECT },
|
||||
{ sha256_fingerprint: ROOT_SHA256 },
|
||||
],
|
||||
signer_infos: [
|
||||
{
|
||||
identified_by: "issuer-and-serial",
|
||||
certificate_sha256: LEAF_SHA256,
|
||||
digest_algorithm_oid: OID_SHA256,
|
||||
signature_algorithm_oid: OID_ECDSA_SHA256,
|
||||
},
|
||||
],
|
||||
decode_error: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("decodes a v2 signer, keeps an unparseable certificate null, and flags a digest mismatch", async () => {
|
||||
const r = await reportOf(
|
||||
await write(
|
||||
|
||||
@@ -60,7 +60,7 @@ describe("v1 signer output projection", () => {
|
||||
expect(new X509Certificate(certificate).subject).toContain('\\"');
|
||||
const signer = signerOf(certificate);
|
||||
const facts = v1SignerFacts(signer);
|
||||
expect(facts.certificate?.subject).not.toBeNull();
|
||||
expect(facts.certificate?.subject).toEqual(expect.any(String));
|
||||
expect(projectedV1SignerBytes(signer)).toBeGreaterThanOrEqual(
|
||||
Buffer.byteLength(JSON.stringify(facts)),
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import { withVerifierCleanup } from "../../scripts/lib/verifier-cleanup.mjs";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { createDeferred } from "../../tests/fixtures/binarySession.js";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import { DarwinProcessOwnershipInspectionError } from "../process/DarwinProcessRunTokenReader.js";
|
||||
@@ -319,6 +320,7 @@ it("retries failed command peers without repeating successful helper cleanup", a
|
||||
it("joins preparation during close and retains a late helper handoff", async () => {
|
||||
const client = provider();
|
||||
const ready = createDeferred<void>();
|
||||
const entered = createDeferred<void>();
|
||||
const cleanup = new ProviderCleanupError(
|
||||
"process-ownership",
|
||||
["late-helper"],
|
||||
@@ -328,6 +330,7 @@ it("joins preparation during close and retains a late helper handoff", async ()
|
||||
cleanup.resolveCleanup();
|
||||
});
|
||||
vi.mocked(prepareOwnedCommand).mockImplementationOnce(async () => {
|
||||
entered.resolve();
|
||||
await ready.promise;
|
||||
throw new DarwinProcessOwnershipInspectionError(
|
||||
"physical preparation failure",
|
||||
@@ -335,12 +338,13 @@ it("joins preparation during close and retains a late helper handoff", async ()
|
||||
);
|
||||
});
|
||||
const pending = inspect(client);
|
||||
await vi.waitFor(() => expect(prepareOwnedCommand).toHaveBeenCalledOnce());
|
||||
let closed = false;
|
||||
const closing = client.close().then(() => {
|
||||
closed = true;
|
||||
});
|
||||
try {
|
||||
let closing: Promise<void> | undefined;
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(entered.promise, pending, "command preparation");
|
||||
closing = client.close().then(() => {
|
||||
closed = true;
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(closed).toBe(false);
|
||||
ready.resolve();
|
||||
@@ -351,11 +355,16 @@ it("joins preparation during close and retains a late helper handoff", async ()
|
||||
expect(close).toHaveBeenCalledOnce();
|
||||
expect(result.error.cleanupIncomplete).toBe(false);
|
||||
expect(runOwnedCommand).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
ready.resolve();
|
||||
await pending;
|
||||
await closing;
|
||||
}
|
||||
}, [
|
||||
() => ready.resolve(),
|
||||
async () => {
|
||||
await pending;
|
||||
},
|
||||
async () => {
|
||||
await closing;
|
||||
},
|
||||
() => client.close(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves an opaque cancellation reason before acquiring a command", async () => {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { withVerifierCleanup } from "../../scripts/lib/verifier-cleanup.mjs";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import { createDoctorHostFixture } from "./Doctor.fixture.js";
|
||||
@@ -47,18 +49,24 @@ it("joins a late provider inspection and retries only its unresolved cleanup own
|
||||
},
|
||||
});
|
||||
const inspection = host.providerInspections!();
|
||||
const signal = await entered;
|
||||
const closing = host.close!().then(
|
||||
() => {
|
||||
closed = true;
|
||||
return undefined;
|
||||
},
|
||||
(cause: unknown) => {
|
||||
closed = true;
|
||||
return cause;
|
||||
},
|
||||
);
|
||||
try {
|
||||
let closing: Promise<unknown> | undefined;
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(
|
||||
entered.then(() => {}),
|
||||
inspection,
|
||||
"provider inspection",
|
||||
);
|
||||
const signal = await entered;
|
||||
closing = host.close!().then(
|
||||
() => {
|
||||
closed = true;
|
||||
return undefined;
|
||||
},
|
||||
(cause: unknown) => {
|
||||
closed = true;
|
||||
return cause;
|
||||
},
|
||||
);
|
||||
expect(signal.aborted).toBe(true);
|
||||
expect(closed).toBe(false);
|
||||
handOff();
|
||||
@@ -78,14 +86,20 @@ it("joins a late provider inspection and retries only its unresolved cleanup own
|
||||
await expect(host.providerInspections!()).rejects.toThrow(
|
||||
"scope is closed",
|
||||
);
|
||||
} finally {
|
||||
denied = false;
|
||||
handOff();
|
||||
finish();
|
||||
await inspection;
|
||||
await closing;
|
||||
await host.close!();
|
||||
}
|
||||
}, [
|
||||
() => {
|
||||
denied = false;
|
||||
handOff();
|
||||
finish();
|
||||
},
|
||||
async () => {
|
||||
await inspection;
|
||||
},
|
||||
async () => {
|
||||
await closing;
|
||||
},
|
||||
() => host.close!(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("retains a completed healthy report when cancellation arrives during final cleanup", async () => {
|
||||
|
||||
@@ -216,7 +216,12 @@ describe("optional Hopper installation failures", () => {
|
||||
host,
|
||||
);
|
||||
expect(result.code).toBe("download_failed");
|
||||
expect(result.remediation).toMatch(/^Download failed\. ./);
|
||||
expect(result.remediation).toContain("Download failed.");
|
||||
expect(result.remediation).toContain("could not be updated");
|
||||
expect(result.clients.cursor).toMatchObject({
|
||||
status: "failed",
|
||||
reason: "write",
|
||||
});
|
||||
expect(host.configurations).toBe(1);
|
||||
});
|
||||
|
||||
@@ -234,7 +239,8 @@ describe("optional Hopper installation failures", () => {
|
||||
host,
|
||||
);
|
||||
expect(result.code).toBe("skill_install_failed");
|
||||
expect(result.remediation).toMatch(/^Download failed\. ./);
|
||||
expect(result.remediation).toContain("Download failed.");
|
||||
expect(result.remediation).toContain("injected skill write failure");
|
||||
expect(host.skillInstalls).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -540,9 +540,19 @@ it("resolves same-session retained JavaScript Evidence without re-running analys
|
||||
lookup,
|
||||
);
|
||||
if (!item.ok) throw item.error;
|
||||
expect(JSON.stringify(item.value.normalized_result)).toContain(
|
||||
"export const secret = 1;",
|
||||
);
|
||||
expect(item.value.normalized_result).toMatchObject({
|
||||
kind: "item",
|
||||
item: {
|
||||
path: "renderer.js",
|
||||
observations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
source: "export const secret = 1;\n",
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects stale retained IDs and unsupported parent operations", async () => {
|
||||
@@ -859,10 +869,14 @@ it("seals an inferred environment state whose values are the recorded ones", asy
|
||||
name: "grafana-example",
|
||||
parent_evidence_id: parent.evidence_id,
|
||||
timeline: "recorded-start-time",
|
||||
entities: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: "/api/folders",
|
||||
id: "fg1jo91e57e2ob",
|
||||
fields: expect.objectContaining({ title: "probe folder" }),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
expect(JSON.stringify(sealed.value.normalized_result)).toContain(
|
||||
"probe folder",
|
||||
);
|
||||
expect(parseEvidence(sealed.value)).toEqual(sealed.value);
|
||||
});
|
||||
|
||||
|
||||
@@ -375,16 +375,47 @@ describe("browser scenario retained boundaries", () => {
|
||||
rows: [{ transaction_id: "request-2" }],
|
||||
},
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
items: {
|
||||
rows: [
|
||||
{
|
||||
events: expect.not.arrayContaining([
|
||||
expect.objectContaining({
|
||||
event: expect.objectContaining({ kind: "network-content" }),
|
||||
}),
|
||||
]),
|
||||
omitted_events: expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "network-content" }),
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(JSON.stringify(result)).not.toContain('"content":');
|
||||
const selected = await view(capture, {
|
||||
...request,
|
||||
event_kinds: ["network-content"],
|
||||
});
|
||||
expect(JSON.stringify(selected)).toContain('"content":');
|
||||
expect(selected).toMatchObject({
|
||||
items: {
|
||||
rows: [
|
||||
{
|
||||
transaction_id: "request-2",
|
||||
events: [
|
||||
expect.objectContaining({
|
||||
event: expect.objectContaining({
|
||||
kind: "network-content",
|
||||
transaction_id: "request-2",
|
||||
phase: "response",
|
||||
body: expect.objectContaining({
|
||||
state: "captured",
|
||||
content: Buffer.from('{"id":2,"login":"probe"}').toString(
|
||||
"base64",
|
||||
),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
omitted_events: expect.arrayContaining([
|
||||
{ item_index: expect.any(Number), kind: "request" },
|
||||
]),
|
||||
@@ -393,7 +424,9 @@ describe("browser scenario retained boundaries", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("browser scenario network observation", () => {
|
||||
it("distinguishes unrequested and legacy-unknown network observation from an observed empty inventory", () => {
|
||||
const base = scenarioCaptureFixture([], {
|
||||
request_body: false,
|
||||
|
||||
@@ -326,7 +326,9 @@ it("keeps bundle-selection errors useful when operation labels exceed the reply
|
||||
if (read.ok) throw new Error("Expected a record-selection error");
|
||||
const encoded = JSON.stringify(read);
|
||||
expect(Buffer.byteLength(encoded, "utf8")).toBeLessThan(8192);
|
||||
for (const record of records) expect(encoded).toContain(record.evidence_id);
|
||||
expect(encoded).toContain("operation omitted");
|
||||
for (const record of records)
|
||||
expect(read.error.userMessage).toContain(
|
||||
`${record.evidence_id} (operation omitted)`,
|
||||
);
|
||||
expect(encoded).not.toContain(operation);
|
||||
});
|
||||
@@ -1,4 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withVerifierCleanup } from "../../../scripts/lib/verifier-cleanup.mjs";
|
||||
import { awaitAcknowledgement } from "../../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
|
||||
import { AnalysisProviderRegistry } from "./AnalysisProviderRegistry.js";
|
||||
import {
|
||||
@@ -651,14 +653,15 @@ describe("analysis provider registry: profile and cleanup failures", () => {
|
||||
selected = true;
|
||||
return result;
|
||||
});
|
||||
await pending;
|
||||
controller.abort(reason);
|
||||
let closed = false;
|
||||
const closing = registry.closeOwnedResources().then((result) => {
|
||||
closed = true;
|
||||
return result;
|
||||
});
|
||||
try {
|
||||
let closing: ReturnType<typeof registry.closeOwnedResources> | undefined;
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(pending, selecting, "profile probe");
|
||||
controller.abort(reason);
|
||||
closing = registry.closeOwnedResources().then((result) => {
|
||||
closed = true;
|
||||
return result;
|
||||
});
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(selected).toBe(false);
|
||||
expect(closed).toBe(false);
|
||||
@@ -674,10 +677,18 @@ describe("analysis provider registry: profile and cleanup failures", () => {
|
||||
expect(await closing).toEqual(ok(null));
|
||||
expect(registry.hasPendingOwnedResources()).toBe(false);
|
||||
expect(alpha.created).toEqual([]);
|
||||
} finally {
|
||||
finish?.();
|
||||
await Promise.all([selecting, closing]);
|
||||
}
|
||||
}, [
|
||||
() => {
|
||||
controller.abort(reason);
|
||||
finish?.();
|
||||
},
|
||||
async () => {
|
||||
await Promise.allSettled([selecting, closing]);
|
||||
},
|
||||
async () => {
|
||||
expect(await registry.closeOwnedResources()).toEqual(ok(null));
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -970,41 +981,62 @@ describe("automatic profile cleanup ownership", () => {
|
||||
};
|
||||
}),
|
||||
);
|
||||
const result = await registry.select(DATABASE_TARGET, "auto", {
|
||||
const selecting = registry.select(DATABASE_TARGET, "auto", {
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (result.ok) throw new Error("Mixed failed profiles admitted");
|
||||
expect(result.error.cleanupIncomplete).toBe(true);
|
||||
expect(result.error.cleanupResources).toEqual([
|
||||
"alpha-file",
|
||||
"beta-file",
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(
|
||||
admitted,
|
||||
selecting,
|
||||
"profile children admission",
|
||||
);
|
||||
const result = await selecting;
|
||||
if (result.ok) throw new Error("Mixed failed profiles admitted");
|
||||
expect(result.error.cleanupIncomplete).toBe(true);
|
||||
expect(result.error.cleanupResources).toEqual([
|
||||
"alpha-file",
|
||||
"beta-file",
|
||||
]);
|
||||
expect(result.error.diagnostics?.profile_errors).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ provider_id: "alpha" }),
|
||||
expect.objectContaining({ provider_id: "beta" }),
|
||||
]),
|
||||
);
|
||||
const close1 = await registry.closeOwnedResources();
|
||||
expect(close1.ok).toBe(false);
|
||||
released.add(first);
|
||||
const close2 = await registry.closeOwnedResources();
|
||||
expect(close2.ok).toBe(false);
|
||||
expect(result.error.cleanupIncomplete).toBe(true);
|
||||
expect(result.error.cleanupResources).toEqual([
|
||||
first === "alpha" ? "beta-file" : "alpha-file",
|
||||
]);
|
||||
expect(close1.ok ? [] : close1.error.cleanupResources).toEqual([
|
||||
first === "alpha" ? "beta-file" : "alpha-file",
|
||||
]);
|
||||
released.add(first === "alpha" ? "beta" : "alpha");
|
||||
expect(await registry.closeOwnedResources()).toEqual(ok(null));
|
||||
expect(result.error.cleanupIncomplete).toBe(false);
|
||||
if (close1.ok || close2.ok)
|
||||
throw new Error("Cleanup unexpectedly succeeded");
|
||||
expect(close1.error.cleanupIncomplete).toBe(false);
|
||||
expect(close2.error.cleanupIncomplete).toBe(false);
|
||||
expect(registry.hasPendingOwnedResources()).toBe(false);
|
||||
}, [
|
||||
() => {
|
||||
released.add("alpha");
|
||||
released.add("beta");
|
||||
controller.abort(reason);
|
||||
admitChildren?.();
|
||||
},
|
||||
async () => {
|
||||
await Promise.allSettled([selecting]);
|
||||
},
|
||||
async () => {
|
||||
expect(await registry.closeOwnedResources()).toEqual(ok(null));
|
||||
},
|
||||
]);
|
||||
expect(result.error.diagnostics?.profile_errors).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ provider_id: "alpha" }),
|
||||
expect.objectContaining({ provider_id: "beta" }),
|
||||
]),
|
||||
);
|
||||
const close1 = await registry.closeOwnedResources();
|
||||
expect(close1.ok).toBe(false);
|
||||
released.add(first);
|
||||
const close2 = await registry.closeOwnedResources();
|
||||
expect(close2.ok).toBe(false);
|
||||
expect(result.error.cleanupIncomplete).toBe(true);
|
||||
expect(result.error.cleanupResources).toEqual([
|
||||
first === "alpha" ? "beta-file" : "alpha-file",
|
||||
]);
|
||||
expect(close1.ok ? [] : close1.error.cleanupResources).toEqual([
|
||||
first === "alpha" ? "beta-file" : "alpha-file",
|
||||
]);
|
||||
released.add(first === "alpha" ? "beta" : "alpha");
|
||||
expect(await registry.closeOwnedResources()).toEqual(ok(null));
|
||||
expect(result.error.cleanupIncomplete).toBe(false);
|
||||
if (close1.ok || close2.ok)
|
||||
throw new Error("Cleanup unexpectedly succeeded");
|
||||
expect(close1.error.cleanupIncomplete).toBe(false);
|
||||
expect(close2.error.cleanupIncomplete).toBe(false);
|
||||
expect(registry.hasPendingOwnedResources()).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1034,10 +1066,12 @@ it("registers before production and gates fresh selections until shutdown has jo
|
||||
},
|
||||
]);
|
||||
const selecting = registry.select(DATABASE_TARGET, "alpha");
|
||||
await entered;
|
||||
const closing = registry.closeOwnedResources();
|
||||
const next = registry.select(DATABASE_TARGET, "alpha");
|
||||
try {
|
||||
let closing: ReturnType<typeof registry.closeOwnedResources> | undefined;
|
||||
let next: ReturnType<typeof registry.select> | undefined;
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(entered, selecting, "profile finalization");
|
||||
closing = registry.closeOwnedResources();
|
||||
next = registry.select(DATABASE_TARGET, "alpha");
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(calls).toBe(1);
|
||||
expect(registry.hasPendingOwnedResources()).toBe(true);
|
||||
@@ -1050,11 +1084,15 @@ it("registers before production and gates fresh selections until shutdown has jo
|
||||
expect((await next).ok).toBe(true);
|
||||
expect(calls).toBe(2);
|
||||
expect(registry.hasPendingOwnedResources()).toBe(false);
|
||||
} finally {
|
||||
finish?.();
|
||||
await Promise.all([selecting, closing, next]);
|
||||
await registry.closeOwnedResources();
|
||||
}
|
||||
}, [
|
||||
() => finish?.(),
|
||||
async () => {
|
||||
await Promise.allSettled([selecting, closing, next]);
|
||||
},
|
||||
async () => {
|
||||
expect(await registry.closeOwnedResources()).toEqual(ok(null));
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("joins cancelled readiness finalization and shutdown before admitting another observation", async () => {
|
||||
@@ -1094,15 +1132,17 @@ it("joins cancelled readiness finalization and shutdown before admitting another
|
||||
settled = true;
|
||||
return cause;
|
||||
});
|
||||
await entered;
|
||||
controller.abort(reason);
|
||||
let closed = false;
|
||||
const closing = registry.closeOwnedResources().then((value) => {
|
||||
closed = true;
|
||||
return value;
|
||||
});
|
||||
const next = registry.candidates();
|
||||
try {
|
||||
let closing: ReturnType<typeof registry.closeOwnedResources> | undefined;
|
||||
let next: ReturnType<typeof registry.candidates> | undefined;
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(entered, observing, "readiness observation");
|
||||
controller.abort(reason);
|
||||
closing = registry.closeOwnedResources().then((value) => {
|
||||
closed = true;
|
||||
return value;
|
||||
});
|
||||
next = registry.candidates();
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(settled).toBe(false);
|
||||
expect(closed).toBe(false);
|
||||
@@ -1125,11 +1165,18 @@ it("joins cancelled readiness finalization and shutdown before admitting another
|
||||
expect(await registry.candidates()).toHaveLength(1);
|
||||
expect(calls).toBe(2);
|
||||
expect(registry.hasPendingOwnedResources()).toBe(false);
|
||||
} finally {
|
||||
finish();
|
||||
await Promise.allSettled([observing, closing, next]);
|
||||
await registry.closeOwnedResources();
|
||||
}
|
||||
}, [
|
||||
() => {
|
||||
controller.abort(reason);
|
||||
finish();
|
||||
},
|
||||
async () => {
|
||||
await Promise.allSettled([observing, closing, next]);
|
||||
},
|
||||
async () => {
|
||||
expect(await registry.closeOwnedResources()).toEqual(ok(null));
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(["alpha", "beta"])(
|
||||
@@ -1208,8 +1255,12 @@ it("joins every candidate and retains physical failure with exact mixed cleanup
|
||||
settled = true;
|
||||
return cause;
|
||||
});
|
||||
await entered;
|
||||
try {
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(
|
||||
entered,
|
||||
observing,
|
||||
"mixed readiness observation",
|
||||
);
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(settled).toBe(false);
|
||||
finish();
|
||||
@@ -1245,11 +1296,15 @@ it("joins every candidate and retains physical failure with exact mixed cleanup
|
||||
details: { diagnostics: { reason: "physical observation failed" } },
|
||||
});
|
||||
expect(registry.hasPendingOwnedResources()).toBe(false);
|
||||
} finally {
|
||||
finish();
|
||||
await observing;
|
||||
await registry.closeOwnedResources();
|
||||
}
|
||||
}, [
|
||||
finish,
|
||||
async () => {
|
||||
await observing;
|
||||
},
|
||||
async () => {
|
||||
expect(await registry.closeOwnedResources()).toEqual(ok(null));
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("registers selection before async readiness can race shutdown", async () => {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import { awaitAcknowledgement } from "../../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import { withVerifierCleanup } from "../../../scripts/lib/verifier-cleanup.mjs";
|
||||
import { elf } from "../../domain/binaryTarget.fixture.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, onTestFinished } from "vitest";
|
||||
|
||||
import {
|
||||
createAnalysisExecution,
|
||||
@@ -42,76 +44,68 @@ import { SessionProviderRouter } from "./SessionProviderRouter.js";
|
||||
*/
|
||||
it("binds omitted provider subjects to the selected target", async () => {
|
||||
const { session } = openSession();
|
||||
try {
|
||||
const opened = await session.open(process.execPath);
|
||||
expect(opened.ok).toBe(true);
|
||||
const execution = await session.execute("address_name", {});
|
||||
expect(execution.ok).toBe(true);
|
||||
if (!execution.ok || !opened.ok) return;
|
||||
expect(execution.value.subject).toEqual({
|
||||
path: opened.value.path,
|
||||
sha256: opened.value.sha256,
|
||||
format: opened.value.format,
|
||||
...(opened.value.architecture === undefined
|
||||
? {}
|
||||
: { architecture: opened.value.architecture }),
|
||||
});
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
const opened = await session.open(process.execPath);
|
||||
expect(opened.ok).toBe(true);
|
||||
const execution = await session.execute("address_name", {});
|
||||
expect(execution.ok).toBe(true);
|
||||
if (!execution.ok || !opened.ok) return;
|
||||
expect(execution.value.subject).toEqual({
|
||||
path: opened.value.path,
|
||||
sha256: opened.value.sha256,
|
||||
format: opened.value.format,
|
||||
...(opened.value.architecture === undefined
|
||||
? {}
|
||||
: { architecture: opened.value.architecture }),
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves an explicitly unbound provider observation", async () => {
|
||||
const { session } = openSession({ explicitNullSubject: true });
|
||||
try {
|
||||
expect((await session.open(process.execPath)).ok).toBe(true);
|
||||
const execution = await session.execute("address_name", {});
|
||||
expect(execution.ok).toBe(true);
|
||||
if (execution.ok) expect(execution.value.subject).toBeNull();
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
expect((await session.open(process.execPath)).ok).toBe(true);
|
||||
const execution = await session.execute("address_name", {});
|
||||
expect(execution.ok).toBe(true);
|
||||
if (execution.ok) expect(execution.value.subject).toBeNull();
|
||||
});
|
||||
|
||||
it("retains live source Evidence without replaying a prior observation", async () => {
|
||||
const { session, deep } = openSession({ live: true });
|
||||
try {
|
||||
expect((await session.open(process.execPath)).ok).toBe(true);
|
||||
expect(
|
||||
(
|
||||
await session.execute("address_name", {
|
||||
document: "0",
|
||||
address: "0x9999",
|
||||
})
|
||||
).ok,
|
||||
).toBe(true);
|
||||
const child = deep.created[0];
|
||||
if (child === undefined) throw new Error("expected provider child");
|
||||
let reads = 0;
|
||||
child.execute = () =>
|
||||
Promise.resolve(
|
||||
ok(createAnalysisExecution(`live ${++reads}`, deepIdentity)),
|
||||
);
|
||||
const parameters = { document: "0", address: "0x1000" };
|
||||
const first = await session.execute("address_name", parameters);
|
||||
const second = await session.execute("address_name", parameters);
|
||||
expect(first.ok && first.value.result).toBe("live 1");
|
||||
expect(second.ok && second.value.result).toBe("live 2");
|
||||
if (!first.ok || !second.ok) return;
|
||||
expect(first.value.evidenceId).toBeDefined();
|
||||
expect(second.value.evidenceId).toBeDefined();
|
||||
expect(first.value.evidenceId).not.toBe(second.value.evidenceId);
|
||||
expect(session.exportEvidenceBundle().records).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ normalized_result: "live 1" }),
|
||||
expect.objectContaining({ normalized_result: "live 2" }),
|
||||
]),
|
||||
expect((await session.open(process.execPath)).ok).toBe(true);
|
||||
expect(
|
||||
(
|
||||
await session.execute("address_name", {
|
||||
document: "0",
|
||||
address: "0x9999",
|
||||
})
|
||||
).ok,
|
||||
).toBe(true);
|
||||
const child = deep.created[0];
|
||||
if (child === undefined) throw new Error("expected provider child");
|
||||
let reads = 0;
|
||||
child.execute = () =>
|
||||
Promise.resolve(
|
||||
ok(createAnalysisExecution(`live ${++reads}`, deepIdentity)),
|
||||
);
|
||||
const snapshot = session.exportAnalysisSnapshot();
|
||||
if (snapshot.ok) expect(snapshot.value.entries).toHaveLength(0);
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
const parameters = { document: "0", address: "0x1000" };
|
||||
const first = await session.execute("address_name", parameters);
|
||||
const second = await session.execute("address_name", parameters);
|
||||
expect(first.ok && first.value.result).toBe("live 1");
|
||||
expect(second.ok && second.value.result).toBe("live 2");
|
||||
if (!first.ok || !second.ok) return;
|
||||
expect(first.value.evidenceId).toBeDefined();
|
||||
expect(second.value.evidenceId).toBeDefined();
|
||||
expect(first.value.evidenceId).not.toBe(second.value.evidenceId);
|
||||
expect(session.exportEvidenceBundle().records).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ normalized_result: "live 1" }),
|
||||
expect.objectContaining({ normalized_result: "live 2" }),
|
||||
]),
|
||||
);
|
||||
const snapshot = session.exportAnalysisSnapshot();
|
||||
expect(snapshot.ok, snapshot.ok ? undefined : snapshot.error.message).toBe(
|
||||
true,
|
||||
);
|
||||
if (!snapshot.ok) return;
|
||||
expect(snapshot.value.entries).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps unbound observations out of persisted target snapshots", async () => {
|
||||
@@ -119,93 +113,84 @@ it("keeps unbound observations out of persisted target snapshots", async () => {
|
||||
const directory = await createTestTempDirectory("rea-unbound-snapshot-");
|
||||
const path = join(directory, "snapshot.json");
|
||||
const parameters = { document: "0", address: "0x1000" };
|
||||
try {
|
||||
expect((await session.open(process.execPath)).ok).toBe(true);
|
||||
const bound = await session.execute("address_name", parameters);
|
||||
expect(bound.ok).toBe(true);
|
||||
const child = deep.created[0];
|
||||
if (child === undefined || !bound.ok)
|
||||
throw new Error("Expected bound read");
|
||||
child.execute = () =>
|
||||
Promise.resolve(
|
||||
ok(
|
||||
createAnalysisExecution("unbound observation", deepIdentity, {
|
||||
subject: null,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const execution = await session.execute("address_name", {
|
||||
...parameters,
|
||||
address: "0x2000",
|
||||
});
|
||||
expect(execution.ok).toBe(true);
|
||||
if (!execution.ok) return;
|
||||
expect(execution.value.subject).toBeNull();
|
||||
expect(session.exportEvidenceBundle().records).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expect((await session.open(process.execPath)).ok).toBe(true);
|
||||
const bound = await session.execute("address_name", parameters);
|
||||
expect(bound.ok).toBe(true);
|
||||
const child = deep.created[0];
|
||||
if (child === undefined || !bound.ok) throw new Error("Expected bound read");
|
||||
child.execute = () =>
|
||||
Promise.resolve(
|
||||
ok(
|
||||
createAnalysisExecution("unbound observation", deepIdentity, {
|
||||
subject: null,
|
||||
normalized_result: "unbound observation",
|
||||
}),
|
||||
]),
|
||||
),
|
||||
);
|
||||
const snapshot = session.exportAnalysisSnapshot();
|
||||
expect(snapshot.ok, snapshot.ok ? undefined : snapshot.error.message).toBe(
|
||||
true,
|
||||
const execution = await session.execute("address_name", {
|
||||
...parameters,
|
||||
address: "0x2000",
|
||||
});
|
||||
expect(execution.ok).toBe(true);
|
||||
if (!execution.ok) return;
|
||||
expect(execution.value.subject).toBeNull();
|
||||
expect(session.exportEvidenceBundle().records).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
subject: null,
|
||||
normalized_result: "unbound observation",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
const snapshot = session.exportAnalysisSnapshot();
|
||||
expect(snapshot.ok, snapshot.ok ? undefined : snapshot.error.message).toBe(
|
||||
true,
|
||||
);
|
||||
if (!snapshot.ok) return;
|
||||
expect(snapshot.value.entries).toHaveLength(1);
|
||||
expect(snapshot.value.entries[0]?.parameters).toEqual(parameters);
|
||||
const unboundReplay = await session.execute("address_name", {
|
||||
...parameters,
|
||||
address: "0x2000",
|
||||
});
|
||||
expect(unboundReplay.ok).toBe(true);
|
||||
if (unboundReplay.ok) {
|
||||
expect(unboundReplay.value.subject).toBeNull();
|
||||
expect(unboundReplay.value.evidenceId).toBe(execution.value.evidenceId);
|
||||
}
|
||||
expect(
|
||||
(await session.writeAnalysisSnapshot(snapshot.value, path, false)).ok,
|
||||
).toBe(true);
|
||||
expect((await session.close()).ok).toBe(true);
|
||||
expect(
|
||||
(
|
||||
await session.open(process.execPath, {
|
||||
loadSnapshot: () => session.readAnalysisSnapshot(path),
|
||||
})
|
||||
).ok,
|
||||
).toBe(true);
|
||||
const replay = await session.execute("address_name", parameters);
|
||||
expect(replay.ok).toBe(true);
|
||||
if (replay.ok) {
|
||||
expect(replay.value.evidenceId).toBe(bound.value.evidenceId);
|
||||
expect(replay.value.limitations).toContain(
|
||||
"Loaded from a local REA analysis snapshot; this call did not re-run the provider.",
|
||||
);
|
||||
if (!snapshot.ok) return;
|
||||
expect(snapshot.value.entries).toHaveLength(1);
|
||||
expect(snapshot.value.entries[0]?.parameters).toEqual(parameters);
|
||||
const unboundReplay = await session.execute("address_name", {
|
||||
...parameters,
|
||||
address: "0x2000",
|
||||
});
|
||||
expect(unboundReplay.ok).toBe(true);
|
||||
if (unboundReplay.ok) {
|
||||
expect(unboundReplay.value.subject).toBeNull();
|
||||
expect(unboundReplay.value.evidenceId).toBe(execution.value.evidenceId);
|
||||
}
|
||||
expect(
|
||||
(await session.writeAnalysisSnapshot(snapshot.value, path, false)).ok,
|
||||
).toBe(true);
|
||||
expect((await session.close()).ok).toBe(true);
|
||||
expect(
|
||||
(
|
||||
await session.open(process.execPath, {
|
||||
loadSnapshot: () => session.readAnalysisSnapshot(path),
|
||||
})
|
||||
).ok,
|
||||
).toBe(true);
|
||||
const replay = await session.execute("address_name", parameters);
|
||||
expect(replay.ok).toBe(true);
|
||||
if (replay.ok) {
|
||||
expect(replay.value.evidenceId).toBe(bound.value.evidenceId);
|
||||
expect(replay.value.limitations).toContain(
|
||||
"Loaded from a local REA analysis snapshot; this call did not re-run the provider.",
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
});
|
||||
|
||||
describe("BinarySession open liveness", () => {
|
||||
it("retires a locally blocked owner even when its native health is idle", async () => {
|
||||
const { session, deep } = openSession();
|
||||
try {
|
||||
expect((await session.open(process.execPath)).ok).toBe(true);
|
||||
expect((await session.execute("address_name", {})).ok).toBe(true);
|
||||
const retained = deep.created[0];
|
||||
if (retained === undefined) throw new Error("Expected an acquired child");
|
||||
retained.reuseReadiness = () => "blocked";
|
||||
expect(retained.operationHealthSnapshot?.()?.state).toBe("idle");
|
||||
expect((await session.open(process.execPath)).ok).toBe(true);
|
||||
expect(retained.closeCalled).toBe(true);
|
||||
expect((await session.execute("address_name", {})).ok).toBe(true);
|
||||
expect(deep.created).toHaveLength(2);
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
expect((await session.open(process.execPath)).ok).toBe(true);
|
||||
expect((await session.execute("address_name", {})).ok).toBe(true);
|
||||
const retained = deep.created[0];
|
||||
if (retained === undefined) throw new Error("Expected an acquired child");
|
||||
retained.reuseReadiness = () => "blocked";
|
||||
expect(retained.operationHealthSnapshot?.()?.state).toBe("idle");
|
||||
expect((await session.open(process.execPath)).ok).toBe(true);
|
||||
expect(retained.closeCalled).toBe(true);
|
||||
expect((await session.execute("address_name", {})).ok).toBe(true);
|
||||
expect(deep.created).toHaveLength(2);
|
||||
});
|
||||
it("reuses the retained client while the child stays live", async () => {
|
||||
const { session, deep } = openSession();
|
||||
@@ -368,6 +353,7 @@ it("clears retired runtime availability after cancelled-switch restoration", asy
|
||||
});
|
||||
const { session, deep } = openSession({
|
||||
failOperationInitially: true,
|
||||
releaseBeforeCleanup: releaseClose,
|
||||
beforeFirstClose: async () => {
|
||||
closeStarted();
|
||||
await closeGate;
|
||||
@@ -394,11 +380,17 @@ it("clears retired runtime availability after cancelled-switch restoration", asy
|
||||
);
|
||||
await writeFile(alternate, elf(2, 1, 62));
|
||||
const switching = session.open(alternate, { signal: controller.signal });
|
||||
await closing;
|
||||
controller.abort();
|
||||
releaseClose();
|
||||
|
||||
const cancelled = await switching;
|
||||
const cancelled = await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(closing, switching, "provider close");
|
||||
controller.abort();
|
||||
releaseClose();
|
||||
return await switching;
|
||||
}, [
|
||||
releaseClose,
|
||||
async () => {
|
||||
await switching;
|
||||
},
|
||||
]);
|
||||
expect(cancelled).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisCancelledError" },
|
||||
@@ -435,7 +427,6 @@ it("clears retired runtime availability after cancelled-switch restoration", asy
|
||||
reason: "available",
|
||||
remediation: null,
|
||||
});
|
||||
await session.close();
|
||||
});
|
||||
|
||||
const sessionRunId = (
|
||||
@@ -498,6 +489,7 @@ const openSession = (
|
||||
operationHealth?: boolean;
|
||||
failOperationInitially?: boolean;
|
||||
beforeFirstClose?: () => Promise<void>;
|
||||
releaseBeforeCleanup?: () => void;
|
||||
explicitNullSubject?: boolean;
|
||||
live?: boolean;
|
||||
} = {},
|
||||
@@ -527,6 +519,11 @@ const openSession = (
|
||||
]),
|
||||
new InvestigationRecords(),
|
||||
);
|
||||
onTestFinished(async () => {
|
||||
options.releaseBeforeCleanup?.();
|
||||
const closed = await session.close();
|
||||
if (!closed.ok) throw closed.error;
|
||||
});
|
||||
return { session, deep: { created }, onProbe };
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { withVerifierCleanup } from "../../../scripts/lib/verifier-cleanup.mjs";
|
||||
import { awaitAcknowledgement } from "../../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import { DeferredAnalysisClient } from "../../providers/DeferredAnalysisClient.js";
|
||||
import {
|
||||
createAnalysisExecution,
|
||||
@@ -164,8 +166,12 @@ it("keeps a shared creation alive for an interested peer", async () => {
|
||||
reason = { request: "one peer leaves" };
|
||||
const first = client.execute("health", {}, { signal: controller.signal });
|
||||
const second = client.execute("health", {});
|
||||
try {
|
||||
await entered.promise;
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(
|
||||
entered.promise,
|
||||
first,
|
||||
"shared client creation",
|
||||
);
|
||||
controller.abort(reason);
|
||||
const cancelled = await first;
|
||||
if (cancelled.ok) throw new Error("Expected peer cancellation");
|
||||
@@ -181,11 +187,17 @@ it("keeps a shared creation alive for an interested peer", async () => {
|
||||
expect(creates).toBe(1);
|
||||
expect(await client.close()).toEqual(ok(null));
|
||||
expect(closes).toBe(1);
|
||||
} finally {
|
||||
loading.resolve(child());
|
||||
await Promise.allSettled([first, second]);
|
||||
await client.close();
|
||||
}
|
||||
}, [
|
||||
() => controller.abort(),
|
||||
() => loading.resolve(child()),
|
||||
async () => {
|
||||
await Promise.allSettled([first, second]);
|
||||
},
|
||||
async () => {
|
||||
const closed = await client.close();
|
||||
if (!closed.ok) throw closed.error;
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("joins a late acquired client's final cleanup before cancellation settles", async () => {
|
||||
@@ -207,8 +219,8 @@ it("joins a late acquired client's final cleanup before cancellation settles", a
|
||||
settled = true;
|
||||
return result;
|
||||
});
|
||||
try {
|
||||
await entered.promise;
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(entered.promise, pending, "client creation");
|
||||
controller.abort(reason);
|
||||
loading.resolve(
|
||||
child(async () => {
|
||||
@@ -218,7 +230,7 @@ it("joins a late acquired client's final cleanup before cancellation settles", a
|
||||
return ok(null);
|
||||
}),
|
||||
);
|
||||
await closeEntered.promise;
|
||||
await awaitAcknowledgement(closeEntered.promise, pending, "client cleanup");
|
||||
expect(settled).toBe(false);
|
||||
expect(client.operationHealthSnapshot()?.state).toBe("idle");
|
||||
expect(client.reuseReadiness()).toBe("blocked");
|
||||
@@ -229,12 +241,18 @@ it("joins a late acquired client's final cleanup before cancellation settles", a
|
||||
expect(result.error.cleanupIncomplete).toBe(false);
|
||||
expect(await client.close()).toEqual(ok(null));
|
||||
expect(closes).toBe(1);
|
||||
} finally {
|
||||
loading.resolve(child());
|
||||
closing.resolve();
|
||||
await pending;
|
||||
await client.close();
|
||||
}
|
||||
}, [
|
||||
() => controller.abort(),
|
||||
() => loading.resolve(child()),
|
||||
() => closing.resolve(),
|
||||
async () => {
|
||||
await pending;
|
||||
},
|
||||
async () => {
|
||||
const closed = await client.close();
|
||||
if (!closed.ok) throw closed.error;
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps a producer-authored physical failure primary even when it is the caller's abort reason", async () => {
|
||||
@@ -256,8 +274,8 @@ it("keeps a producer-authored physical failure primary even when it is the calle
|
||||
{},
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
try {
|
||||
await entered.promise;
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(entered.promise, pending, "client creation");
|
||||
controller.abort(physical);
|
||||
finishing.resolve();
|
||||
const result = await pending;
|
||||
@@ -272,11 +290,17 @@ it("keeps a producer-authored physical failure primary even when it is the calle
|
||||
expect(result.error.cause).toBe(physical);
|
||||
expect(physical.cause).toBeUndefined();
|
||||
expect(projectAnalysisError(result.error).code).toBe("execution_failure");
|
||||
} finally {
|
||||
finishing.resolve();
|
||||
await pending;
|
||||
await client.close();
|
||||
}
|
||||
}, [
|
||||
() => controller.abort(),
|
||||
() => finishing.resolve(),
|
||||
async () => {
|
||||
await pending;
|
||||
},
|
||||
async () => {
|
||||
const closed = await client.close();
|
||||
if (!closed.ok) throw closed.error;
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("retains a failed late client's exact close owner and restores cancellation after confirmed retry", async () => {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { withVerifierCleanup } from "../../../scripts/lib/verifier-cleanup.mjs";
|
||||
import { awaitAcknowledgement } from "../../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
|
||||
import { parseBinaryTarget } from "../BinaryTargetResolver.js";
|
||||
import type {
|
||||
@@ -7,7 +9,6 @@ import type {
|
||||
} from "../AnalysisProvider.js";
|
||||
import { createAnalysisExecution } from "../AnalysisProvider.js";
|
||||
import { LazyAnalysisProvider } from "./LazyAnalysisProvider.js";
|
||||
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
|
||||
import { ok as resultOk } from "../../domain/result.js";
|
||||
|
||||
const identity: ProviderIdentity = {
|
||||
@@ -66,14 +67,15 @@ it("joins a cancelled unabortable module load and refuses late client acquisitio
|
||||
const entered = new Promise<void>((resolve) => {
|
||||
enter = resolve;
|
||||
});
|
||||
const loading = new Promise<AnalysisProvider>((resolve) => {
|
||||
finishLoading = resolve;
|
||||
});
|
||||
const provider = new LazyAnalysisProvider({
|
||||
identity,
|
||||
capabilities: [],
|
||||
load: () => {
|
||||
enter();
|
||||
return new Promise((resolve) => {
|
||||
finishLoading = resolve;
|
||||
});
|
||||
return loading;
|
||||
},
|
||||
});
|
||||
const controller = new AbortController();
|
||||
@@ -86,9 +88,9 @@ it("joins a cancelled unabortable module load and refuses late client acquisitio
|
||||
settled = true;
|
||||
return value;
|
||||
});
|
||||
await entered;
|
||||
controller.abort(reason);
|
||||
try {
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(entered, pending, "provider module load");
|
||||
controller.abort(reason);
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(settled).toBe(false);
|
||||
finishLoading(
|
||||
@@ -105,11 +107,17 @@ it("joins a cancelled unabortable module load and refuses late client acquisitio
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(creates).toBe(0);
|
||||
expect(await client.close()).toEqual(resultOk(null));
|
||||
} finally {
|
||||
finishLoading(providerWithClient());
|
||||
await pending;
|
||||
await client.close();
|
||||
}
|
||||
}, [
|
||||
() => controller.abort(),
|
||||
() => finishLoading(providerWithClient()),
|
||||
async () => {
|
||||
await pending;
|
||||
},
|
||||
async () => {
|
||||
const closed = await client.close();
|
||||
if (!closed.ok) throw closed.error;
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports that a lazy client was closed before its first operation", async () => {
|
||||
|
||||
@@ -121,7 +121,7 @@ it("preserves the caller's exact abort reason before invocation", async () => {
|
||||
expect(calls).toBe(0);
|
||||
});
|
||||
|
||||
it("cancels before invocation and after an acquired observation", async () => {
|
||||
it("retains the acquired observation when cancellation arrives during inspection", async () => {
|
||||
const controller = new AbortController();
|
||||
const service = new BinaryLayoutService({
|
||||
inspectAndClose(input, options) {
|
||||
@@ -154,21 +154,6 @@ it("cancels before invocation and after an acquired observation", async () => {
|
||||
normalized_result: binaryLayoutFixture(),
|
||||
});
|
||||
}
|
||||
const untouched = new BinaryLayoutService({
|
||||
inspectAndClose(input, options) {
|
||||
return this.inspect(input, options);
|
||||
},
|
||||
identity: BINARY_LAYOUT_TEST_PROVIDER,
|
||||
inspect: () => {
|
||||
throw new Error("must not run");
|
||||
},
|
||||
});
|
||||
expect(
|
||||
await untouched.inspect(
|
||||
{ path: "/source.elf" },
|
||||
{ signal: controller.signal },
|
||||
),
|
||||
).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } });
|
||||
});
|
||||
|
||||
it.each(["different-path", "invalid-digest", "outside-range"])(
|
||||
|
||||
@@ -49,20 +49,6 @@ it("rejects a relative path and extra arguments before reading", async () => {
|
||||
expect(calls).toBe(0);
|
||||
});
|
||||
|
||||
it("records the selected PE and an absent certificate table", async () => {
|
||||
const result = await new PeCertificatesService({
|
||||
identity,
|
||||
inspect: () => Promise.resolve(ok(report)),
|
||||
}).inspect({ path: report.artifact.path });
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value).toMatchObject({
|
||||
confidence: "observed",
|
||||
subject: { format: "pe", architecture: "x86" },
|
||||
normalized_result: report,
|
||||
});
|
||||
});
|
||||
|
||||
it("discards an observation if the caller cancels while the provider completes", async () => {
|
||||
const controller = new AbortController();
|
||||
const service = new PeCertificatesService({
|
||||
|
||||
@@ -62,20 +62,6 @@ it("rejects a relative path and extra arguments before reading", async () => {
|
||||
expect(calls).toBe(0);
|
||||
});
|
||||
|
||||
it("records the selected PE and an absent import table", async () => {
|
||||
const result = await new PeImportsService({
|
||||
identity,
|
||||
inspect: () => Promise.resolve(ok(report)),
|
||||
}).inspect({ path: report.artifact.path });
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value).toMatchObject({
|
||||
confidence: "observed",
|
||||
subject: { format: "pe", architecture: "x86_64" },
|
||||
normalized_result: report,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["imports", "delay_imports", "examined_dlls"] as const)(
|
||||
"rejects contradictory complete %s coverage",
|
||||
async (field) => {
|
||||
|
||||
@@ -128,7 +128,7 @@ it("preserves the caller's exact abort reason before invocation", async () => {
|
||||
expect(calls).toBe(0);
|
||||
});
|
||||
|
||||
it("cancels before invocation and after an acquired observation", async () => {
|
||||
it("retains the acquired observation when cancellation arrives during inspection", async () => {
|
||||
const controller = new AbortController();
|
||||
const service = new RecordedCrashService({
|
||||
inspectAndClose(input, options) {
|
||||
@@ -161,21 +161,6 @@ it("cancels before invocation and after an acquired observation", async () => {
|
||||
normalized_result: recordedCrashFixture(),
|
||||
});
|
||||
}
|
||||
const untouched = new RecordedCrashService({
|
||||
inspectAndClose(input, options) {
|
||||
return this.inspect(input, options);
|
||||
},
|
||||
identity: RECORDED_CRASH_TEST_PROVIDER,
|
||||
inspect: () => {
|
||||
throw new Error("must not run");
|
||||
},
|
||||
});
|
||||
expect(
|
||||
await untouched.inspect(
|
||||
{ path: "/source.elf" },
|
||||
{ signal: controller.signal },
|
||||
),
|
||||
).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } });
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -378,10 +378,15 @@ describe("buildEnvironment cancellation and input checks", () => {
|
||||
const built = await buildEnvironment(input);
|
||||
if (built.ok) throw new Error("Expected a cancellation");
|
||||
const projected = projectAnalysisError(built.error);
|
||||
expect(projected.code).toBe("cancelled");
|
||||
expect(JSON.stringify(projected)).toContain(
|
||||
`"output_directory":${JSON.stringify(input.directory)}`,
|
||||
);
|
||||
expect(projected).toMatchObject({
|
||||
code: "cancelled",
|
||||
details: {
|
||||
diagnostics: {
|
||||
output_directory: input.directory,
|
||||
bundle_written: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(await listFiles(input.directory!)).toHaveLength(7);
|
||||
});
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ describe("loadEnvironmentState", () => {
|
||||
const notEvidence = loadEnvironmentState({}, source);
|
||||
if (notEvidence.ok || !(notEvidence.error instanceof AnalysisInputError))
|
||||
throw new Error("Expected an input refusal");
|
||||
expect(notEvidence.error.issues.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
notEvidence.error.issues.every((issue) => issue.path[0] === "state"),
|
||||
).toBe(true);
|
||||
|
||||
@@ -61,21 +61,6 @@ it("rejects a relative path and extra arguments before reading", async () => {
|
||||
expect(calls).toBe(0);
|
||||
});
|
||||
|
||||
it("records the selected BEAM form", async () => {
|
||||
const result = await new BeamService({
|
||||
close: async () => {},
|
||||
identity,
|
||||
inspect: () => Promise.resolve(ok(report)),
|
||||
}).inspect({ path: report.artifact.path });
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value).toMatchObject({
|
||||
confidence: "observed",
|
||||
subject: { format: "file" },
|
||||
normalized_result: report,
|
||||
});
|
||||
});
|
||||
|
||||
it("discards an observation if the caller cancels while the provider completes", async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = { source: "caller-request", operation: "cancel" };
|
||||
|
||||
@@ -77,9 +77,13 @@ it("queries an empty event name as an exact retained event identity", () => {
|
||||
}),
|
||||
);
|
||||
expect(trace.seed_node_ids.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
trace.nodes
|
||||
.filter(({ node_id }) => trace.seed_node_ids.includes(node_id))
|
||||
.every(({ properties }) => properties.event_name === ""),
|
||||
).toBe(true);
|
||||
const seeds = trace.nodes.filter(({ node_id }) =>
|
||||
trace.seed_node_ids.includes(node_id),
|
||||
);
|
||||
expect(seeds.map(({ node_id }) => node_id).sort()).toEqual(
|
||||
[...trace.seed_node_ids].sort(),
|
||||
);
|
||||
expect(seeds.every(({ properties }) => properties.event_name === "")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
@@ -63,25 +63,6 @@ it.each([
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves the selected worker budget in provider input and Evidence", async () => {
|
||||
const report = { ...fixture(), worker_timeout_ms: 1234 };
|
||||
const service = new SqliteDatabaseService({
|
||||
inspect: async (input) => {
|
||||
expect(input.worker_timeout_ms).toBe(1234);
|
||||
return ok(execution(report));
|
||||
},
|
||||
});
|
||||
const result = await service.inspect({
|
||||
path: "/selected.db",
|
||||
worker_timeout_ms: 1234,
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.parameters).toMatchObject({ worker_timeout_ms: 1234 });
|
||||
expect(result.value.normalized_result).toMatchObject({
|
||||
worker_timeout_ms: 1234,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ selected: undefined, reported: undefined },
|
||||
{ selected: undefined, reported: 1234 },
|
||||
@@ -109,18 +90,6 @@ it.each([
|
||||
},
|
||||
);
|
||||
|
||||
it("retains the actual default without inventing an explicit caller selection", async () => {
|
||||
const service = new SqliteDatabaseService({
|
||||
inspect: async () => ok(execution(fixture())),
|
||||
});
|
||||
const result = await service.inspect({ path: "/selected.db" });
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.normalized_result).toMatchObject({
|
||||
worker_timeout_ms: 30_000,
|
||||
});
|
||||
expect(result.value.parameters).not.toHaveProperty("worker_timeout_ms");
|
||||
});
|
||||
|
||||
it("accepts sqlite_schema rows without requiring a fabricated declaration", async () => {
|
||||
const report = {
|
||||
...fixture(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
@@ -14,9 +14,6 @@ import {
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { scanCanonicalArtifactInventory } from "../../tests/fixtures/artifactInventory.js";
|
||||
|
||||
const INVENTORY_LIMITATION =
|
||||
"Intel HEX/S-record inventory decodes data records into address-contiguous regions. It does not establish CPU, byte order, memory map, or that a declared start address is executable; gaps are unwritten, not zero.";
|
||||
|
||||
const writeImage = async (name: string, text: string): Promise<string> => {
|
||||
const directory = await createTestTempDirectory("rea-hex-records-");
|
||||
const path = join(directory, name);
|
||||
@@ -24,60 +21,6 @@ const writeImage = async (name: string, text: string): Promise<string> => {
|
||||
return path;
|
||||
};
|
||||
|
||||
it("inventories Intel HEX regions with root and region facts", async () => {
|
||||
const snapshot = await scanCanonicalArtifactInventory(
|
||||
await writeImage("firmware.hex", INTEL_HEX_I32_GAP),
|
||||
);
|
||||
expect(snapshot.manifest.root_format).toBe("intel-hex");
|
||||
expect(snapshot.coverage).toMatchObject({
|
||||
scope: "container-entries",
|
||||
status: "complete",
|
||||
});
|
||||
expect(snapshot.limitations).toContain(INVENTORY_LIMITATION);
|
||||
const root = snapshot.occurrences.find((row) => row.logical_path === ".");
|
||||
expect(root).toMatchObject({
|
||||
artifact_kind: "container",
|
||||
format_metadata: {
|
||||
kind: "hex-records",
|
||||
variant: "I32HEX",
|
||||
start_address: 0x0800_0101,
|
||||
region_count: 2,
|
||||
},
|
||||
});
|
||||
expect(
|
||||
snapshot.occurrences
|
||||
.filter((row) => row.logical_path !== ".")
|
||||
.map((row) => [row.logical_path, row.declared_size, row.format_metadata]),
|
||||
).toEqual([
|
||||
[
|
||||
"0x08000000-0x0800001f.bin",
|
||||
32,
|
||||
{
|
||||
kind: "hex-record-region",
|
||||
start_address: 0x0800_0000,
|
||||
end_address: 0x0800_001f,
|
||||
length: 32,
|
||||
record_count: 2,
|
||||
first_line: 2,
|
||||
last_line: 3,
|
||||
},
|
||||
],
|
||||
[
|
||||
"0x08000100-0x08000103.bin",
|
||||
4,
|
||||
{
|
||||
kind: "hex-record-region",
|
||||
start_address: 0x0800_0100,
|
||||
end_address: 0x0800_0103,
|
||||
length: 4,
|
||||
record_count: 1,
|
||||
first_line: 4,
|
||||
last_line: 4,
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports unknown coverage and termination facts as limitations", async () => {
|
||||
const truncated = await scanCanonicalArtifactInventory(
|
||||
await writeImage("truncated.s19", SREC_S19.replace("S9030000FC\n", "")),
|
||||
@@ -103,19 +46,7 @@ it("reports unknown coverage and termination facts as limitations", async () =>
|
||||
);
|
||||
});
|
||||
|
||||
it("fails closed on malformed or over-limit records", async () => {
|
||||
await expect(
|
||||
scanCanonicalArtifactInventory(
|
||||
await writeImage(
|
||||
"checksum.hex",
|
||||
INTEL_HEX_I32_GAP.replace("BEEFC3", "BEEFC4"),
|
||||
),
|
||||
),
|
||||
).rejects.toMatchObject({
|
||||
reason: "format",
|
||||
message:
|
||||
"Intel HEX is malformed: line 4: record type 00 checksum 0xc4 does not match computed 0xc3",
|
||||
});
|
||||
it("fails closed on over-limit records", async () => {
|
||||
const runs = [intelHexRecord(0, 0, [1])];
|
||||
for (let index = 1; index <= 65_536; index += 1) {
|
||||
const address = index * 2;
|
||||
@@ -137,7 +68,7 @@ it("leaves an invalid first record to plain file classification", async () => {
|
||||
expect(snapshot.manifest.root_format).toBe("file");
|
||||
});
|
||||
|
||||
it("extracts byte-exact regions that re-inventory as raw files", async () => {
|
||||
it("re-inventories extracted regions as raw files", async () => {
|
||||
const directory = await createTestTempDirectory("rea-hex-extract-");
|
||||
const path = join(directory, "firmware.hex");
|
||||
await writeFile(path, INTEL_HEX_I32_GAP);
|
||||
@@ -151,23 +82,6 @@ it("extracts byte-exact regions that re-inventory as raw files", async () => {
|
||||
integrityPolicy: "fail",
|
||||
resourceScope,
|
||||
});
|
||||
expect(extracted.containment_verified).toBe(true);
|
||||
expect(
|
||||
Buffer.compare(
|
||||
await readFile(
|
||||
join(extracted.output_root, "0x08000000-0x0800001f.bin"),
|
||||
),
|
||||
Buffer.from(Array.from({ length: 32 }, (_, index) => index)),
|
||||
),
|
||||
).toBe(0);
|
||||
expect(
|
||||
Buffer.compare(
|
||||
await readFile(
|
||||
join(extracted.output_root, "0x08000100-0x08000103.bin"),
|
||||
),
|
||||
Buffer.from("deadbeef", "hex"),
|
||||
),
|
||||
).toBe(0);
|
||||
const reinventoried = await scanCanonicalArtifactInventory(
|
||||
extracted.output_root,
|
||||
);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { Uf2ArtifactReader } from "./Uf2ArtifactReader.js";
|
||||
import { ArtifactResourceScope } from "./ArtifactResourceScope.js";
|
||||
import { extractArtifact } from "./extraction/ArtifactExtraction.js";
|
||||
import {
|
||||
RP2040_FAMILY,
|
||||
RP2XXX_ABSOLUTE_FAMILY,
|
||||
@@ -135,78 +133,13 @@ it("leaves coverage unknown when numbered blocks are absent", async () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("fails closed on a conflicting rewrite and leaves short files unrecognized", async () => {
|
||||
const rewrite = uf2Block({
|
||||
address: 0x1000_0010,
|
||||
payload: [0xff],
|
||||
blockNo: 0,
|
||||
numBlocks: 1,
|
||||
familyId: RP2040_FAMILY,
|
||||
});
|
||||
await expect(
|
||||
scanCanonicalArtifactInventory(
|
||||
await writeImage(
|
||||
"conflict.uf2",
|
||||
uf2File([...uf2Image(RP2040_FAMILY, 0x1000_0000, APP), rewrite]),
|
||||
),
|
||||
),
|
||||
).rejects.toMatchObject({
|
||||
reason: "format",
|
||||
message:
|
||||
"UF2 is malformed: family 0xe48bff56 (RP2040) address 0x10000010 is written with different values by blocks 0-2 (0x75) and block 3 (0xff)",
|
||||
});
|
||||
it("leaves short files unrecognized", async () => {
|
||||
const truncated = await scanCanonicalArtifactInventory(
|
||||
await writeImage("short.uf2", ABSOLUTE.subarray(0, 511)),
|
||||
);
|
||||
expect(truncated.manifest.root_format).toBe("file");
|
||||
});
|
||||
|
||||
it("extracts byte-exact regions into family directories", async () => {
|
||||
const directory = await createTestTempDirectory("rea-uf2-extract-");
|
||||
const path = join(directory, "firmware.uf2");
|
||||
await writeFile(
|
||||
path,
|
||||
uf2File([ABSOLUTE, ...uf2Image(RP2040_FAMILY, 0x1000_0000, APP)]),
|
||||
);
|
||||
const resourceScope = new ArtifactResourceScope();
|
||||
try {
|
||||
const extracted = await extractArtifact({
|
||||
inputPath: path,
|
||||
inputFormat: "uf2",
|
||||
outputRoot: join(directory, "out"),
|
||||
environment: process.env,
|
||||
integrityPolicy: "fail",
|
||||
resourceScope,
|
||||
});
|
||||
expect(extracted.containment_verified).toBe(true);
|
||||
expect(extracted.coverage.status).toBe("unknown");
|
||||
expect(
|
||||
Buffer.compare(
|
||||
await readFile(
|
||||
join(
|
||||
extracted.output_root,
|
||||
"family-0xe48bff56/0x10000000-0x10000257.bin",
|
||||
),
|
||||
),
|
||||
APP,
|
||||
),
|
||||
).toBe(0);
|
||||
expect(
|
||||
Buffer.compare(
|
||||
await readFile(
|
||||
join(
|
||||
extracted.output_root,
|
||||
"family-0xe48bff57/0x10ffff00-0x10ffffff.bin",
|
||||
),
|
||||
),
|
||||
Buffer.alloc(256, 0xef),
|
||||
),
|
||||
).toBe(0);
|
||||
} finally {
|
||||
await resourceScope.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a forged region entry and a changed carrier", async () => {
|
||||
const path = await writeImage(
|
||||
"identity.uf2",
|
||||
|
||||
@@ -593,27 +593,6 @@ describe("ZipArtifactReader decompression budget", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("streams a valid small member through open", async () => {
|
||||
const directory = await createTestTempDirectory("rea-zip-budget-valid-");
|
||||
const path = join(directory, "valid.zip");
|
||||
const writer = new ZipWriter(new Uint8ArrayWriter());
|
||||
await writer.add("member.bin", new TextReader("valid member bytes"));
|
||||
await writeFile(path, await writer.close());
|
||||
const reader = new ZipArtifactReader(path, "zip");
|
||||
try {
|
||||
const entries = [];
|
||||
for await (const entry of reader.entries()) entries.push(entry);
|
||||
const entry = entries[0];
|
||||
if (entry === undefined) throw new Error("Expected a ZIP member");
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of await reader.open(entry))
|
||||
chunks.push(Buffer.from(chunk));
|
||||
expect(Buffer.concat(chunks).toString()).toBe("valid member bytes");
|
||||
} finally {
|
||||
await reader.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects under-declared member output before delivering its complete actual bytes", async () => {
|
||||
const directory = await createTestTempDirectory("rea-zip-underdeclared-");
|
||||
const path = join(directory, "underdeclared.zip");
|
||||
|
||||
@@ -220,8 +220,6 @@ describe("safe CDP response metadata", () => {
|
||||
"agent-header-secret",
|
||||
])
|
||||
expect(serialized).not.toContain(secret);
|
||||
expect(serialized).toContain("link-secret");
|
||||
expect(serialized).toContain("private.example.test/x");
|
||||
});
|
||||
|
||||
it("reports well-known agent resources as untrusted observations", () => {
|
||||
|
||||
@@ -73,32 +73,30 @@ describe("request step schema", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts a same-origin request with query, headers and a JSON body", () => {
|
||||
it("accepts a declared secret as a request header value", () => {
|
||||
const parsed = scenario(
|
||||
[
|
||||
requestStep({
|
||||
query: [{ name: "org", value: { source: "literal", value: "1" } }],
|
||||
headers: [
|
||||
{
|
||||
name: "X-CSRFToken",
|
||||
value: { source: "cookie", name: "csrftoken" },
|
||||
},
|
||||
{
|
||||
name: "Authorization",
|
||||
value: { source: "secret", secret_id: "token" },
|
||||
prefix: "Bearer ",
|
||||
},
|
||||
],
|
||||
json: { title: "Scratch folder 1" },
|
||||
expected_status: [200, 201],
|
||||
}),
|
||||
],
|
||||
[{ secret_id: "token", environment_variable: "APP_TOKEN" }],
|
||||
);
|
||||
expect(parsed.actions[0]).toMatchObject({
|
||||
action: "request",
|
||||
method: "POST",
|
||||
path: "/api/folders",
|
||||
headers: [
|
||||
{
|
||||
name: "Authorization",
|
||||
value: { source: "secret", secret_id: "token" },
|
||||
prefix: "Bearer ",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -220,66 +218,12 @@ describe("request step execution", () => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("sends the declared request through the page's fetch with its cookies", async () => {
|
||||
const calls = stubFetch(201);
|
||||
const parsed = scenario([
|
||||
requestStep({
|
||||
query: [{ name: "org", value: { source: "literal", value: "1" } }],
|
||||
json: { title: "Scratch folder 1" },
|
||||
}),
|
||||
]);
|
||||
await perform(fakePage({}), parsed.actions[0]!, resolved(parsed));
|
||||
expect(calls).toEqual([
|
||||
{
|
||||
url: `${ORIGIN}/api/folders?org=1`,
|
||||
init: expect.objectContaining({
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: [["content-type", "application/json"]],
|
||||
body: '{"title":"Scratch folder 1"}',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("reads header values from a cookie and from stored JSON, and redacts them afterwards", async () => {
|
||||
it("sends requests with same-origin credentials", async () => {
|
||||
const calls = stubFetch(200);
|
||||
const parsed = scenario([
|
||||
requestStep({
|
||||
headers: [
|
||||
{
|
||||
name: "X-CSRFToken",
|
||||
value: { source: "cookie", name: "csrftoken" },
|
||||
},
|
||||
{
|
||||
name: "Authorization",
|
||||
value: {
|
||||
source: "local_storage",
|
||||
key: "auth",
|
||||
json_pointer: "/token",
|
||||
},
|
||||
prefix: "Bearer ",
|
||||
},
|
||||
],
|
||||
}),
|
||||
]);
|
||||
const secrets = resolved(parsed);
|
||||
await perform(
|
||||
fakePage({
|
||||
cookies: [{ name: "csrftoken", value: "csrf-value-123" }],
|
||||
storage: { auth: JSON.stringify({ token: "jwt-value-456" }) },
|
||||
}),
|
||||
parsed.actions[0]!,
|
||||
secrets,
|
||||
);
|
||||
expect(calls[0]!.init.headers).toEqual([
|
||||
["X-CSRFToken", "csrf-value-123"],
|
||||
["Authorization", "Bearer jwt-value-456"],
|
||||
]);
|
||||
const redacted = secrets.redact("csrf-value-123 and jwt-value-456");
|
||||
expect(redacted).not.toContain("csrf-value-123");
|
||||
expect(redacted).not.toContain("jwt-value-456");
|
||||
expect(redacted).toContain("[REDACTED:page:create.X-CSRFToken]");
|
||||
const parsed = scenario([requestStep({})]);
|
||||
await perform(fakePage({}), parsed.actions[0]!, resolved(parsed));
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0]!.init.credentials).toBe("same-origin");
|
||||
});
|
||||
|
||||
it("refuses to run off the start origin, and fails on an unexpected status or a missing cookie", async () => {
|
||||
|
||||
@@ -47,15 +47,27 @@ it.each([
|
||||
});
|
||||
|
||||
it("retains U+FFFD in well-formed metadata as an ordinary XML character", () => {
|
||||
const decoded = JSON.stringify(
|
||||
decodeSazMetadata(
|
||||
Buffer.from(
|
||||
'<Session><SessionFlag N="ui-comments" V="caf�"/>�</Session>',
|
||||
),
|
||||
),
|
||||
const decoded = decodeSazMetadata(
|
||||
Buffer.from('<Session><SessionFlag N="ui-comments" V="caf�"/>�</Session>'),
|
||||
);
|
||||
expect(decoded).toContain('{"name":"V","value":"caf�"}');
|
||||
expect(decoded).toContain('"value":"�"');
|
||||
expect(decoded).toMatchObject({
|
||||
name: "#document",
|
||||
children: [
|
||||
{
|
||||
name: "Session",
|
||||
children: [
|
||||
{
|
||||
name: "SessionFlag",
|
||||
attributes: [
|
||||
{ name: "N", value: "ui-comments" },
|
||||
{ name: "V", value: "caf�" },
|
||||
],
|
||||
},
|
||||
{ name: "#text", value: "�" },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -74,10 +86,20 @@ it.each([
|
||||
it("preserves valid comments, CDATA, declarations, and predefined entities", () => {
|
||||
const xml =
|
||||
'<?xml version="1.0"?><Session><!-- note & literal --><Value>A & B</Value><C><![CDATA[C & D]]></C></Session>';
|
||||
const decoded = JSON.stringify(decodeSazMetadata(Buffer.from(xml)));
|
||||
expect(decoded).toContain("note & literal");
|
||||
expect(decoded).toContain("A & B");
|
||||
expect(decoded).toContain("C & D");
|
||||
const decoded = decodeSazMetadata(Buffer.from(xml));
|
||||
expect(decoded).toMatchObject({
|
||||
children: [
|
||||
{ name: "xml" },
|
||||
{
|
||||
name: "Session",
|
||||
children: [
|
||||
{ name: "#comment", value: " note & literal " },
|
||||
{ name: "Value", children: [{ name: "#text", value: "A & B" }] },
|
||||
{ name: "C", children: [{ name: "#cdata-section", value: "C & D" }] },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects DTDs without resolving entities", () => {
|
||||
|
||||
@@ -108,12 +108,14 @@ describe("CLI JSON path normalization", () => {
|
||||
"FILE:electron",
|
||||
"https://example.test/app",
|
||||
])("keeps URL %j distinguishable from a local path", (value) => {
|
||||
const input = {
|
||||
const expected = {
|
||||
executable_path: value,
|
||||
application_path: resolve("/synthetic/app"),
|
||||
};
|
||||
const input = { ...expected };
|
||||
const normalized = resolveCliJsonPaths(input, paths);
|
||||
expect(normalized).toEqual(input);
|
||||
expect(normalized).toEqual(expected);
|
||||
expect(input).toEqual(expected);
|
||||
const parsed = electronActiveObservationInputSchema.safeParse(normalized);
|
||||
expect(parsed.success).toBe(false);
|
||||
if (parsed.success) throw new Error("Expected URL path rejection");
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
|
||||
import {
|
||||
binaryLayoutSummaryViewResultSchema,
|
||||
javascriptApplicationSummaryViewResultSchema,
|
||||
projectAnalysisView,
|
||||
analysisViewRequestSchema,
|
||||
} from "../domain/analysisView/analysisView.js";
|
||||
import {
|
||||
analysisViewCaptureEvidence,
|
||||
analysisViewJavaScriptEvidence,
|
||||
analysisViewLayoutEvidence,
|
||||
environmentStateCaptureFixture,
|
||||
} from "../../tests/fixtures/analysisView.js";
|
||||
import { toolContract } from "./toolContracts.js";
|
||||
|
||||
@@ -29,25 +33,6 @@ const projectSummary = (parent: {
|
||||
);
|
||||
|
||||
it("advertises only that operation's summary from analysis detail outputs", () => {
|
||||
for (const name of [
|
||||
"inspect_binary_layout",
|
||||
"analyze_javascript_application",
|
||||
] as const) {
|
||||
const schema = JSON.stringify(
|
||||
z.toJSONSchema(toolContract(name).outputSchema),
|
||||
);
|
||||
expect(schema).not.toContain("api-surface");
|
||||
expect(schema).not.toContain("environment-manifest");
|
||||
expect(schema).not.toContain("environment-state");
|
||||
expect(schema).not.toContain("inspect_web_network_capture");
|
||||
}
|
||||
const viewSchema = JSON.stringify(
|
||||
z.toJSONSchema(toolContract("inspect_analysis_view").outputSchema),
|
||||
);
|
||||
expect(viewSchema).toContain("api-surface");
|
||||
expect(viewSchema).toContain("environment-manifest");
|
||||
expect(viewSchema).toContain("environment-state");
|
||||
|
||||
const layout = projectSummary(analysisViewLayoutEvidence());
|
||||
const javascript = projectSummary(analysisViewJavaScriptEvidence());
|
||||
if (!layout.ok || !javascript.ok) throw new Error("Expected summaries");
|
||||
@@ -61,4 +46,84 @@ it("advertises only that operation's summary from analysis detail outputs", () =
|
||||
expect(
|
||||
binaryLayoutSummaryViewResultSchema.safeParse(javascript.value).success,
|
||||
).toBe(false);
|
||||
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
const validators = Object.fromEntries(
|
||||
(
|
||||
[
|
||||
"inspect_binary_layout",
|
||||
"analyze_javascript_application",
|
||||
"inspect_analysis_view",
|
||||
] as const
|
||||
).map((name) => [
|
||||
name,
|
||||
ajv.compile(z.toJSONSchema(toolContract(name).outputSchema)),
|
||||
]),
|
||||
);
|
||||
const layoutReceipt = {
|
||||
...analysisViewLayoutEvidence(),
|
||||
normalized_result: layout.value,
|
||||
};
|
||||
const javascriptReceipt = {
|
||||
...analysisViewJavaScriptEvidence(),
|
||||
normalized_result: javascript.value,
|
||||
};
|
||||
expect(validators.inspect_binary_layout!(layoutReceipt)).toBe(true);
|
||||
expect(validators.analyze_javascript_application!(javascriptReceipt)).toBe(
|
||||
true,
|
||||
);
|
||||
const parent = analysisViewCaptureEvidence(environmentStateCaptureFixture());
|
||||
const captureParent = {
|
||||
evidenceId: parent.evidence_id,
|
||||
operation: parent.operation,
|
||||
normalizedResult: parent.normalized_result,
|
||||
parameters: parent.parameters,
|
||||
subject: parent.subject,
|
||||
limitations: parent.limitations,
|
||||
};
|
||||
const captureSummary = projectAnalysisView(captureParent, {
|
||||
kind: "summary",
|
||||
});
|
||||
if (!captureSummary.ok) throw captureSummary.error;
|
||||
const captureReceipt = { ...parent, normalized_result: captureSummary.value };
|
||||
expect(validators.inspect_analysis_view!(captureReceipt)).toBe(true);
|
||||
expect(
|
||||
validators.inspect_binary_layout!({
|
||||
...layoutReceipt,
|
||||
normalized_result: captureSummary.value,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
validators.analyze_javascript_application!({
|
||||
...javascriptReceipt,
|
||||
normalized_result: captureSummary.value,
|
||||
}),
|
||||
).toBe(false);
|
||||
for (const kind of [
|
||||
"api-surface",
|
||||
"environment-manifest",
|
||||
"environment-state",
|
||||
] as const) {
|
||||
const projected = projectAnalysisView(
|
||||
captureParent,
|
||||
analysisViewRequestSchema.parse({ kind }),
|
||||
);
|
||||
if (!projected.ok) throw projected.error;
|
||||
const receipt = { ...parent, normalized_result: projected.value };
|
||||
expect(validators.inspect_analysis_view!(receipt), kind).toBe(true);
|
||||
expect(
|
||||
validators.inspect_binary_layout!({
|
||||
...layoutReceipt,
|
||||
normalized_result: projected.value,
|
||||
}),
|
||||
kind,
|
||||
).toBe(false);
|
||||
expect(
|
||||
validators.analyze_javascript_application!({
|
||||
...javascriptReceipt,
|
||||
normalized_result: projected.value,
|
||||
}),
|
||||
kind,
|
||||
).toBe(false);
|
||||
}
|
||||
});
|
||||
+28
-18
@@ -2,6 +2,8 @@ import { chmod, mkdir, realpath, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it as test, vi } from "vitest";
|
||||
import { withVerifierCleanup } from "../scripts/lib/verifier-cleanup.mjs";
|
||||
import { awaitAcknowledgement } from "../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
|
||||
import { createSystemDoctorHost } from "./doctorRuntime.js";
|
||||
import { DOCTOR_PROVIDER_IDS } from "./application/DoctorProviderIds.js";
|
||||
@@ -273,8 +275,8 @@ test.each([true, false])(
|
||||
.finally(() => {
|
||||
settled = true;
|
||||
});
|
||||
try {
|
||||
await entered;
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(entered, pending, "display selection");
|
||||
expect(selected.mock.calls).toHaveLength(1);
|
||||
if (cancel) {
|
||||
controller.abort(reason);
|
||||
@@ -295,13 +297,17 @@ test.each([true, false])(
|
||||
expect.objectContaining({ name: "hopper-demo-runtime", ok: true }),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
release();
|
||||
await pending;
|
||||
await system.close?.();
|
||||
inspected.mockRestore();
|
||||
selected.mockRestore();
|
||||
}
|
||||
}, [
|
||||
release,
|
||||
async () => {
|
||||
await pending;
|
||||
},
|
||||
async () => {
|
||||
await system.close?.();
|
||||
},
|
||||
() => inspected.mockRestore(),
|
||||
() => selected.mockRestore(),
|
||||
]);
|
||||
},
|
||||
5_000,
|
||||
);
|
||||
@@ -340,8 +346,8 @@ test("joins display finalization before preserving a late cancellation", async (
|
||||
.finally(() => {
|
||||
settled = true;
|
||||
});
|
||||
try {
|
||||
await entered;
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(entered, pending, "display finalization");
|
||||
controller.abort(reason);
|
||||
await Promise.resolve();
|
||||
expect(settled).toBe(false);
|
||||
@@ -351,13 +357,17 @@ test("joins display finalization before preserving a late cancellation", async (
|
||||
expect(failure).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect((failure as Error).cause).toBe(reason);
|
||||
expect(inspected).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
release();
|
||||
await pending;
|
||||
await system.close?.();
|
||||
inspected.mockRestore();
|
||||
selected.mockRestore();
|
||||
}
|
||||
}, [
|
||||
release,
|
||||
async () => {
|
||||
await pending;
|
||||
},
|
||||
async () => {
|
||||
await system.close?.();
|
||||
},
|
||||
() => inspected.mockRestore(),
|
||||
() => selected.mockRestore(),
|
||||
]);
|
||||
});
|
||||
|
||||
test("rejects a pre-aborted caller before the Hopper display check", async () => {
|
||||
|
||||
@@ -782,9 +782,15 @@ it("projects JavaScript summary and preserves selected module observations", ()
|
||||
expect(item.value.item).toMatchObject({
|
||||
path: "renderer.js",
|
||||
kind: "javascript-asset",
|
||||
observations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
source: "export const secret = 1;\n",
|
||||
text: "should not leak",
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
expect(JSON.stringify(item.value.item)).toContain("export const secret = 1;");
|
||||
expect(JSON.stringify(item.value.item)).toContain("should not leak");
|
||||
const page = projectAnalysisView(parent, {
|
||||
kind: "page",
|
||||
collection: "modules",
|
||||
|
||||
@@ -351,6 +351,9 @@ it("derives value-free body shapes, observed query names and record pointers", (
|
||||
"status",
|
||||
]);
|
||||
const silent = manifest([], environmentManifestCaptureFixture(), false);
|
||||
expect(silent.tools.map((tool) => tool.name)).toEqual(
|
||||
projected.tools.map((tool) => tool.name),
|
||||
);
|
||||
expect(
|
||||
silent.tools.every((tool) => tool.query_parameter_names.length === 0),
|
||||
).toBe(true);
|
||||
|
||||
@@ -180,6 +180,7 @@ describe("call path reconstruction", () => {
|
||||
expect(
|
||||
result.paths.map((path) => path.nodes.map(({ address }) => address)),
|
||||
).toEqual([["0x1000", "0x2000", "0x4000"]]);
|
||||
expect(result.paths[0]?.edges).toHaveLength(2);
|
||||
expect(
|
||||
result.paths[0]?.edges.every((edge) => edge.evidence_links.length > 0),
|
||||
).toBe(true);
|
||||
|
||||
@@ -155,15 +155,7 @@ describe("Electron fuse wire decoding", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not treat a missing sentinel or a short wire as disabled fuses", () => {
|
||||
const missing = describeElectronFuseFindings([slice([])], "mach-o");
|
||||
expect(missing.runtime_implications).toEqual([]);
|
||||
expect(missing.limitations.join(" ")).toContain(
|
||||
"unknown, not that every fuse is disabled",
|
||||
);
|
||||
expect(missing.limitations.join(" ")).toContain(
|
||||
"fuse state for that slice is unknown, not disabled",
|
||||
);
|
||||
it("does not treat a short wire as disabled fuses", () => {
|
||||
const truncated = describeElectronFuseFindings(
|
||||
[
|
||||
slice([
|
||||
|
||||
@@ -56,6 +56,10 @@ describe("Evidence completion ledger", () => {
|
||||
]);
|
||||
|
||||
expect(left).toEqual(right);
|
||||
expect(left.records).toEqual([
|
||||
record("scenario.a", "unknown", [evidenceId("3")]),
|
||||
record("scenario.z", "pass", [evidenceId("1"), evidenceId("2")]),
|
||||
]);
|
||||
expect(left.ledger_id).toMatch(/^ecl_[a-f0-9]{64}$/u);
|
||||
});
|
||||
|
||||
@@ -67,9 +71,13 @@ describe("Evidence completion ledger", () => {
|
||||
|
||||
expect(
|
||||
createEvidenceCompletionLedger([
|
||||
record("scenario.many-evidence-items", "pass", evidenceIds),
|
||||
record(
|
||||
"scenario.many-evidence-items",
|
||||
"pass",
|
||||
[...evidenceIds].reverse(),
|
||||
),
|
||||
]).records[0]?.evidence_ids,
|
||||
).toHaveLength(evidenceIds.length);
|
||||
).toEqual(evidenceIds);
|
||||
});
|
||||
|
||||
it("rejects missing, malformed, and duplicate Evidence IDs", () => {
|
||||
|
||||
@@ -113,11 +113,15 @@ it("preserves cleanup failure authority and resources while excluding diagnostic
|
||||
const projected = projectAnalysisError(
|
||||
redactExplicitFailure(error, ["private", "REDACTED"]),
|
||||
);
|
||||
expect(projected.code).toBe("cleanup_incomplete");
|
||||
expect(projected).toMatchObject({
|
||||
code: "cleanup_incomplete",
|
||||
details: {
|
||||
resources: expect.arrayContaining(["pid:123"]),
|
||||
diagnostics: { exit_code: 2 },
|
||||
},
|
||||
});
|
||||
expect(JSON.stringify(projected)).not.toContain("private");
|
||||
expect(JSON.stringify(projected)).not.toContain("REDACTED");
|
||||
expect(JSON.stringify(projected)).toContain("pid:123");
|
||||
expect(JSON.stringify(projected)).toContain('"exit_code":2');
|
||||
});
|
||||
|
||||
it("redacts cleanup primaries and mirrors the source error's live status", () => {
|
||||
|
||||
@@ -108,82 +108,8 @@ describe("ESP image recognition", () => {
|
||||
});
|
||||
|
||||
describe("ESP image decoding", () => {
|
||||
it("reports header, chip, segment, integrity and descriptor facts", () => {
|
||||
const decoded = image(APP);
|
||||
expect(decoded.metadata).toEqual({
|
||||
kind: "esp-image",
|
||||
header: {
|
||||
segment_count: 4,
|
||||
spi_mode: 2,
|
||||
spi_speed: 0,
|
||||
spi_size: 2,
|
||||
entry_addr: 0x4038_0000,
|
||||
wp_pin: 0xee,
|
||||
spi_pin_drv: "000000",
|
||||
chip_id: 5,
|
||||
min_chip_rev: 0,
|
||||
min_chip_rev_full: 0,
|
||||
max_chip_rev_full: 199,
|
||||
reserved: "00000000",
|
||||
hash_appended: true,
|
||||
},
|
||||
chip: { target: "esp32c3", isa: "riscv", source: "ESP-IDF v6.1" },
|
||||
// Segments end at 456; the checksum ends the next 16-byte block.
|
||||
checksum: {
|
||||
offset: 463,
|
||||
stored: espChecksum(ESP32C3_APP_SEGMENTS),
|
||||
computed: espChecksum(ESP32C3_APP_SEGMENTS),
|
||||
status: "valid",
|
||||
},
|
||||
appended_sha256: {
|
||||
offset: 464,
|
||||
stored: sha256(APP.subarray(0, 464)),
|
||||
computed: sha256(APP.subarray(0, 464)),
|
||||
status: "valid",
|
||||
},
|
||||
image_end: 496,
|
||||
signature_sector_offset: null,
|
||||
undescribed_trailing: [],
|
||||
descriptor: {
|
||||
kind: "app",
|
||||
offset: 32,
|
||||
observed_length: 256,
|
||||
secure_version: 0,
|
||||
version: "1.2.3",
|
||||
project_name: "rea_esp_fixture",
|
||||
time: "12:34:56",
|
||||
date: "Oct 27 2026",
|
||||
idf_ver: "v6.1",
|
||||
app_elf_sha256: "ab".repeat(32),
|
||||
min_efuse_blk_rev_full: 0,
|
||||
max_efuse_blk_rev_full: 199,
|
||||
mmu_page_size: 16,
|
||||
spi_flash_mode: 2,
|
||||
},
|
||||
flash_dump: null,
|
||||
});
|
||||
expect(
|
||||
decoded.segments.map(({ dataOffset, availableLength, metadata }) => [
|
||||
metadata.index,
|
||||
metadata.load_addr,
|
||||
metadata.data_len,
|
||||
metadata.header_offset,
|
||||
dataOffset,
|
||||
availableLength,
|
||||
metadata.classification,
|
||||
]),
|
||||
).toEqual([
|
||||
[0, 0x3c00_0020, 288, 24, 32, 288, "mapped"],
|
||||
[1, 0x3fc8_8000, 16, 320, 328, 16, "loaded"],
|
||||
[2, 0x4038_0000, 32, 344, 352, 32, "loaded"],
|
||||
[3, 0x4200_0020, 64, 384, 392, 64, "mapped"],
|
||||
]);
|
||||
expect(decoded.coverage).toBe("complete");
|
||||
expect(decoded.limitations).toEqual([]);
|
||||
expect(decoded.signatureBlocks).toEqual([]);
|
||||
});
|
||||
|
||||
it("decodes identically from any chunking", () => {
|
||||
expect(image(APP).metadata.flash_dump).toBeNull();
|
||||
const signed = signedEspImage(APP, ["rsa", null, "ecdsa-p384"]);
|
||||
const expected = image(signed, signed.length);
|
||||
for (const chunkBytes of [1, 3, 16, 4096])
|
||||
|
||||
@@ -399,21 +399,6 @@ describe("hex record recognition", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([3, 5])(
|
||||
"rejects a nonzero reserved offset in Intel HEX start record %i",
|
||||
(type) => {
|
||||
const text = [
|
||||
intelHexRecord(0, 0x20, [0xa5]),
|
||||
intelHexRecord(type, 0x1234, [0, 0, 0, 1]),
|
||||
intelHexRecord(1, 0),
|
||||
].join("\n");
|
||||
expect(decode(text)).toMatchObject({
|
||||
ok: false,
|
||||
error: { kind: "format" },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps data offsets, start payloads and the ignored EOF offset distinct", () => {
|
||||
const parsed = image(
|
||||
[
|
||||
|
||||
@@ -596,7 +596,11 @@ describe("function collection collation ties", () => {
|
||||
const calls = result.dimensions.find(
|
||||
({ dimension }) => dimension === "calls",
|
||||
);
|
||||
expect(calls).toMatchObject({ status: "changed" });
|
||||
expect(calls).toMatchObject({
|
||||
status: "changed",
|
||||
left_digest: expect.any(String),
|
||||
right_digest: expect.any(String),
|
||||
});
|
||||
expect(calls?.left_digest).not.toBe(calls?.right_digest);
|
||||
});
|
||||
|
||||
@@ -1143,7 +1147,11 @@ describe("function comparison identity and malformed CFG", () => {
|
||||
const pseudocode = comparison.dimensions.find(
|
||||
({ dimension }) => dimension === "pseudocode",
|
||||
);
|
||||
expect(pseudocode).toMatchObject({ status: "changed" });
|
||||
expect(pseudocode).toMatchObject({
|
||||
status: "changed",
|
||||
left_digest: expect.any(String),
|
||||
right_digest: expect.any(String),
|
||||
});
|
||||
expect(pseudocode?.left_digest).not.toBe(pseudocode?.right_digest);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -175,9 +175,9 @@ it.each([
|
||||
require('node:fs').readFileSync('also-wrong');
|
||||
`);
|
||||
expect(ir.configurationOperations).toEqual([]);
|
||||
expect(
|
||||
ir.bindings.find(({ name }) => name === "fs")?.provenance.status,
|
||||
).not.toBe("module");
|
||||
const binding = ir.bindings.find(({ name }) => name === "fs");
|
||||
expect(binding).toBeDefined();
|
||||
expect(binding?.provenance.status).not.toBe("module");
|
||||
});
|
||||
|
||||
it("keeps local loader reassignment separate from the ambient loader", () => {
|
||||
|
||||
@@ -336,25 +336,25 @@ describe("JavaScript semantic analysis: computed method keys", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("JavaScript semantic analysis: calls 2", () => {
|
||||
it("does not treat a shadowed Promise binding as the intrinsic", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
it("does not treat a shadowed Promise binding as the intrinsic", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
function run(Promise) {
|
||||
return Promise.resolve(1).then(work);
|
||||
}
|
||||
`);
|
||||
|
||||
expect(ir.promiseOperations).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "chain",
|
||||
method: "then",
|
||||
ownership: "returned",
|
||||
sourcePromiseIds: [],
|
||||
sourceResolution: "unresolved",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
expect(ir.promiseOperations).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "chain",
|
||||
method: "then",
|
||||
ownership: "returned",
|
||||
sourcePromiseIds: [],
|
||||
sourceResolution: "unresolved",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
describe("JavaScript semantic analysis: callable fingerprints", () => {
|
||||
it("fingerprints formatting and local-name changes identically", () => {
|
||||
const left = analyzeJavaScriptSemantics(`
|
||||
function calculate(value) {
|
||||
@@ -371,9 +371,16 @@ describe("JavaScript semantic analysis: calls 2", () => {
|
||||
|
||||
expect(left.functionFingerprints).toHaveLength(1);
|
||||
expect(right.functionFingerprints).toHaveLength(1);
|
||||
expect(changed.functionFingerprints).toHaveLength(1);
|
||||
expect(left.functionFingerprints[0]?.components).toEqual(
|
||||
right.functionFingerprints[0]?.components,
|
||||
);
|
||||
expect(
|
||||
changed.functionFingerprints[0]?.components.literalSetSha256,
|
||||
).toEqual(expect.any(String));
|
||||
expect(left.functionFingerprints[0]?.components.literalSetSha256).toEqual(
|
||||
expect.any(String),
|
||||
);
|
||||
expect(
|
||||
changed.functionFingerprints[0]?.components.literalSetSha256,
|
||||
).not.toBe(left.functionFingerprints[0]?.components.literalSetSha256);
|
||||
@@ -432,11 +439,13 @@ describe("JavaScript semantic analysis: calls 2", () => {
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
fingerprint(ordered, "ordered")?.components.relationShapeSha256,
|
||||
).not.toBe(
|
||||
fingerprint(reversed, "ordered")?.components.relationShapeSha256,
|
||||
);
|
||||
const orderedDigest = fingerprint(ordered, "ordered")?.components
|
||||
.relationShapeSha256;
|
||||
const reversedDigest = fingerprint(reversed, "ordered")?.components
|
||||
.relationShapeSha256;
|
||||
expect(orderedDigest).toEqual(expect.any(String));
|
||||
expect(reversedDigest).toEqual(expect.any(String));
|
||||
expect(orderedDigest).not.toBe(reversedDigest);
|
||||
expect(fingerprint(ordered, "timerOwner")?.components.effects).toContain(
|
||||
"timer",
|
||||
);
|
||||
@@ -451,9 +460,10 @@ describe("JavaScript semantic analysis: calls 2", () => {
|
||||
fingerprint(ordered, "networkOwner")?.components.effects,
|
||||
).not.toContain("timer");
|
||||
});
|
||||
});
|
||||
|
||||
it("retains ambiguous local callees and explicit dynamic frontiers", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
it("retains ambiguous local callees and explicit dynamic frontiers", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
function left(value) { return value; }
|
||||
function right(value) { return value; }
|
||||
const selected = chooseLeft ? left : right;
|
||||
@@ -462,37 +472,36 @@ describe("JavaScript semantic analysis: calls 2", () => {
|
||||
external(input);
|
||||
`);
|
||||
|
||||
const selected = ir.callSites.find(
|
||||
({ resolution }) => resolution === "ambiguous",
|
||||
);
|
||||
expect(selected?.calleeCallableIds).toEqual(
|
||||
[
|
||||
onlyCallable(ir, "left").callableId,
|
||||
onlyCallable(ir, "right").callableId,
|
||||
].sort(),
|
||||
);
|
||||
expect(
|
||||
ir.argumentFlows.filter(
|
||||
({ callSiteId }) => callSiteId === selected?.callSiteId,
|
||||
),
|
||||
).toHaveLength(2);
|
||||
expect(ir.frontiers).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "dynamic-call",
|
||||
reason: expect.stringMatching(/ambiguous/iu),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "dynamic-property",
|
||||
reason: expect.stringMatching(/computed member/iu),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "dynamic-call",
|
||||
reason: "Unresolved call target external.",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
const selected = ir.callSites.find(
|
||||
({ resolution }) => resolution === "ambiguous",
|
||||
);
|
||||
expect(selected?.calleeCallableIds).toEqual(
|
||||
[
|
||||
onlyCallable(ir, "left").callableId,
|
||||
onlyCallable(ir, "right").callableId,
|
||||
].sort(),
|
||||
);
|
||||
expect(
|
||||
ir.argumentFlows.filter(
|
||||
({ callSiteId }) => callSiteId === selected?.callSiteId,
|
||||
),
|
||||
).toHaveLength(2);
|
||||
expect(ir.frontiers).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "dynamic-call",
|
||||
reason: expect.stringMatching(/ambiguous/iu),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "dynamic-property",
|
||||
reason: expect.stringMatching(/computed member/iu),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "dynamic-call",
|
||||
reason: "Unresolved call target external.",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
describe("JavaScript semantic rest parameter flow", () => {
|
||||
|
||||
@@ -473,9 +473,9 @@ describe("JavaScript semantic analysis: mapped arguments writes", () => {
|
||||
.returnSites[0]?.value;
|
||||
if (value?.status !== "object")
|
||||
throw new Error("Expected an object return value");
|
||||
expect(
|
||||
value.properties.find(({ name }) => name === "value")?.value.status,
|
||||
).not.toBe("literal");
|
||||
const property = value.properties.find(({ name }) => name === "value");
|
||||
expect(property).toBeDefined();
|
||||
expect(property?.value.status).not.toBe("literal");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -59,6 +59,8 @@ it("keeps a nested function's directive out of its owner fingerprint", () => {
|
||||
const second = analyze('"ordinary"');
|
||||
expect(first.size).toBe(2);
|
||||
expect(second.get("outer")).toEqual(first.get("outer"));
|
||||
expect(first.get("inner")?.literalSetSha256).toEqual(expect.any(String));
|
||||
expect(second.get("inner")?.literalSetSha256).toEqual(expect.any(String));
|
||||
expect(second.get("inner")?.literalSetSha256).not.toBe(
|
||||
first.get("inner")?.literalSetSha256,
|
||||
);
|
||||
|
||||
@@ -53,9 +53,9 @@ it.each([
|
||||
])(
|
||||
"indexes the legacy block-function write on its outer owner: %s",
|
||||
(source) => {
|
||||
expect(
|
||||
onlyCallable(analyze(source), "read").returnSites[0]?.value.status,
|
||||
).not.toBe("literal");
|
||||
const returnSite = onlyCallable(analyze(source), "read").returnSites[0];
|
||||
expect(returnSite).toBeDefined();
|
||||
expect(returnSite?.value.status).not.toBe("literal");
|
||||
},
|
||||
);
|
||||
|
||||
@@ -246,5 +246,6 @@ it("owns a class declaration's self reference separately from its outer variable
|
||||
|
||||
it("does not initialize a class self name during computed-key evaluation", () => {
|
||||
const ir = analyze(`class Service { [Service()]() {} }`, "fixture.mjs");
|
||||
expect(ir.callSites[0]).toBeDefined();
|
||||
expect(ir.callSites[0]?.resolution).not.toBe("exact");
|
||||
});
|
||||
@@ -210,6 +210,7 @@ describe("JavaScript semantic analysis: loop lexical names", () => {
|
||||
({ name, role }) => name === "value" && role === "read",
|
||||
);
|
||||
expect(reads).toHaveLength(2);
|
||||
expect(reads[0]?.bindingId).toEqual(expect.any(String));
|
||||
expect(reads[0]?.bindingId).not.toBe(reads[1]?.bindingId);
|
||||
expect(reads[1]?.bindingId).toBe(topLevelBinding(ir, "value").bindingId);
|
||||
});
|
||||
@@ -224,6 +225,7 @@ describe("JavaScript semantic analysis: loop lexical names", () => {
|
||||
({ name, role }) => name === "value" && role === "read",
|
||||
);
|
||||
expect(reads).toHaveLength(2);
|
||||
expect(reads[0]?.bindingId).toEqual(expect.any(String));
|
||||
expect(reads[0]?.bindingId).not.toBe(
|
||||
topLevelBinding(ir, "value").bindingId,
|
||||
);
|
||||
@@ -242,11 +244,14 @@ describe("JavaScript semantic analysis: loop lexical names", () => {
|
||||
const binding = onlyBinding(ir, "value");
|
||||
const callable = ir.callables.find(({ name }) => name === "run");
|
||||
expect(binding.scopeId).toBe(callable?.bodyScopeId);
|
||||
expect(
|
||||
ir.references
|
||||
.filter(({ name, role }) => name === "value" && role === "read")
|
||||
.every(({ bindingId }) => bindingId === binding.bindingId),
|
||||
).toBe(true);
|
||||
const reads = ir.references.filter(
|
||||
({ name, role }) => name === "value" && role === "read",
|
||||
);
|
||||
expect(reads).toHaveLength(2);
|
||||
expect(reads.map(({ bindingId }) => bindingId)).toEqual([
|
||||
binding.bindingId,
|
||||
binding.bindingId,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -268,6 +273,9 @@ describe("JavaScript semantic analysis: class expression lexical names", () => {
|
||||
({ name, role }) => name === "Service" && role === "read",
|
||||
);
|
||||
expect(references).toHaveLength(2);
|
||||
expect(references[1]?.bindingId).toBe(
|
||||
topLevelBinding(ir, "Service").bindingId,
|
||||
);
|
||||
expect(references[0]?.bindingId).not.toBe(references[1]?.bindingId);
|
||||
expect(references[0]?.resolution).toBe("resolved");
|
||||
const classScope = ir.scopes.find(({ kind }) => kind === "class");
|
||||
|
||||
@@ -54,6 +54,7 @@ it("orders exported callable initializers within their enclosing declaration", (
|
||||
resolution: "exact",
|
||||
calleeCallableIds: [original.callableId],
|
||||
});
|
||||
expect(ir.callSites[1]).toBeDefined();
|
||||
expect(ir.callSites[1]?.resolution).not.toBe("exact");
|
||||
});
|
||||
|
||||
|
||||
@@ -202,12 +202,14 @@ it.each([
|
||||
: (child: { loader: unknown }) => {
|
||||
if (!preserveHost) child.loader = {};
|
||||
};
|
||||
let thrown: unknown;
|
||||
try {
|
||||
runInNewContext(source, { module, external });
|
||||
} catch (error) {
|
||||
expect(error).toMatchObject({ name: "TypeError" });
|
||||
expect(preserveHost).toBe(false);
|
||||
thrown = error;
|
||||
}
|
||||
if (preserveHost) expect(thrown).toBeUndefined();
|
||||
else expect(thrown).toMatchObject({ name: "TypeError" });
|
||||
expect(calls).toEqual(preserveHost ? ["./uncertain.cjs"] : []);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -368,13 +368,31 @@ it("shares completed factory control flow across repeated loads within one analy
|
||||
},
|
||||
});
|
||||
expect(results).toHaveLength(64);
|
||||
expect(
|
||||
results.every((result) =>
|
||||
symbolicVariants(result).every(
|
||||
(variant, index) => variant === symbolicVariants(results[0]!)[index],
|
||||
const variants = symbolicVariants(results[0]!);
|
||||
expect(variants).toHaveLength(2);
|
||||
expect(variants).toEqual(
|
||||
expect.arrayContaining(
|
||||
[true, false].map((positive) =>
|
||||
expect.objectContaining({
|
||||
kind: "namespace",
|
||||
conditions: expect.arrayContaining([
|
||||
{
|
||||
node: expect.objectContaining({
|
||||
type: "Identifier",
|
||||
name: "flag",
|
||||
}),
|
||||
positive,
|
||||
},
|
||||
]),
|
||||
}),
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
);
|
||||
for (const result of results) {
|
||||
const actual = symbolicVariants(result);
|
||||
expect(actual).toHaveLength(variants.length);
|
||||
actual.forEach((variant, index) => expect(variant).toBe(variants[index]));
|
||||
}
|
||||
expect(
|
||||
evaluated.mock.calls.filter(([node]) =>
|
||||
t.isIdentifier(node, { name: "flag" }),
|
||||
|
||||
@@ -201,7 +201,7 @@ describe("NativeAOT detail and summary byte accounting", () => {
|
||||
budget,
|
||||
);
|
||||
const complete = detail(REPORT_BUDGET);
|
||||
expect(complete?.derived_memory).not.toBeNull();
|
||||
expect(complete?.derived_memory).toEqual(expect.any(Object));
|
||||
let low = 0;
|
||||
let high = Buffer.byteLength(JSON.stringify(complete));
|
||||
while (low < high) {
|
||||
@@ -211,7 +211,7 @@ describe("NativeAOT detail and summary byte accounting", () => {
|
||||
}
|
||||
const atBoundary = detail(low);
|
||||
const belowBoundary = detail(low - 1);
|
||||
expect(atBoundary?.derived_memory).not.toBeNull();
|
||||
expect(atBoundary?.derived_memory).toEqual(expect.any(Object));
|
||||
expect(Buffer.byteLength(JSON.stringify(atBoundary))).toBeLessThanOrEqual(
|
||||
low,
|
||||
);
|
||||
|
||||
@@ -228,6 +228,9 @@ describe("historical source graph", () => {
|
||||
it("omits absent local diagnostics and rejects explicitly undefined non-JSON metadata", () => {
|
||||
const input = graphInput();
|
||||
const graph = createHistoricalSourceGraph(input);
|
||||
expect(graph.entries.map((entry) => entry.path)).toEqual(
|
||||
input.entries.map((entry) => entry.path),
|
||||
);
|
||||
expect(
|
||||
graph.entries.every(
|
||||
(entry) => !Object.hasOwn(entry, "local_diagnostics"),
|
||||
|
||||
@@ -467,7 +467,6 @@ describe("web bundle analyzer", () => {
|
||||
expect.objectContaining({ value: "Vue" }),
|
||||
]),
|
||||
);
|
||||
expect(JSON.stringify(result)).toContain("authorization=secret");
|
||||
expect(result.completeness.status).toBe("complete");
|
||||
});
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ describe("captured script path projection", () => {
|
||||
const paths = planned.map(({ record }) =>
|
||||
record.content.state === "exported" ? record.content.relative_path : "",
|
||||
);
|
||||
expect(paths[0]).toMatch(/^origins\/[a-f0-9]{64}\/app\/main.js$/u);
|
||||
expect(paths[0]).toMatch(/^origins\/[a-f0-9]{64}\/app\/main\.js$/u);
|
||||
expect(paths[1]?.replace("lib/value.mjs", "main.js")).toBe(paths[0]);
|
||||
expect(paths[2]).not.toBe(paths[0]);
|
||||
});
|
||||
|
||||
@@ -20,62 +20,6 @@ const metadataLayout = (bytes: Buffer) => {
|
||||
};
|
||||
|
||||
describe("managed member pointer ownership", () => {
|
||||
it.each([
|
||||
{ table: "fields" as const, prefix: "0x040000" },
|
||||
{ table: "methods" as const, prefix: "0x060000" },
|
||||
])("maps reordered pointer slots to physical $table", ({ table, prefix }) => {
|
||||
const bytes = buildManagedMemberPointerFixture();
|
||||
const result = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
);
|
||||
|
||||
expect(result.coverage).toEqual({ state: "complete", issues: [] });
|
||||
expect(result.types.map((type) => type.full_name)).toEqual([
|
||||
"<Module>",
|
||||
"Fixture.A",
|
||||
"Fixture.B",
|
||||
]);
|
||||
expect(
|
||||
result[table].map(({ token, declaring_type, declaring_type_token }) => ({
|
||||
token,
|
||||
declaring_type,
|
||||
declaring_type_token,
|
||||
})),
|
||||
).toEqual([
|
||||
{
|
||||
token: `${prefix}01`,
|
||||
declaring_type: "Fixture.B",
|
||||
declaring_type_token: "0x02000003",
|
||||
},
|
||||
{
|
||||
token: `${prefix}02`,
|
||||
declaring_type: "Fixture.A",
|
||||
declaring_type_token: "0x02000002",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves a local custom-attribute constructor through MethodPtr", () => {
|
||||
const bytes = buildManagedMemberPointerFixture();
|
||||
const target = managedPeFixtureTarget(bytes);
|
||||
const artifact = inspectManagedArtifactBytes(bytes, target);
|
||||
const members = inspectManagedMembersBytes(bytes, target);
|
||||
|
||||
expect(artifact.metadata.stream_names).toContain("#-");
|
||||
expect(artifact.coverage).toEqual({ state: "complete", issues: [] });
|
||||
expect(artifact.attributes).toEqual([
|
||||
expect.objectContaining({
|
||||
parent_token: "0x20000001",
|
||||
constructor_token: "0x06000001",
|
||||
type_name: "Fixture.B",
|
||||
}),
|
||||
]);
|
||||
expect(artifact.attributes[0]?.type_name).toBe(
|
||||
members.methods[0]?.declaring_type,
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: "identity pointer tables",
|
||||
|
||||
@@ -5,6 +5,8 @@ import { syncBuiltinESMExports } from "node:module";
|
||||
import * as fileSystem from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { withVerifierCleanup } from "../../scripts/lib/verifier-cleanup.mjs";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
|
||||
import { thinFuseImage } from "../../tests/fixtures/electron/fuseImage.js";
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
@@ -375,8 +377,8 @@ it("awaits an admitted preflight's final close before projecting cancellation",
|
||||
settled = true;
|
||||
return result;
|
||||
});
|
||||
try {
|
||||
await closeEntered;
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(closeEntered, running, "preflight close");
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(settled).toBe(false);
|
||||
releaseClose();
|
||||
@@ -396,13 +398,17 @@ it("awaits an admitted preflight's final close before projecting cancellation",
|
||||
await provider.close();
|
||||
expect(result.error.cleanupIncomplete).toBe(false);
|
||||
expect(retry.error.cleanupIncomplete).toBe(false);
|
||||
} finally {
|
||||
releaseClose();
|
||||
denied = false;
|
||||
await running;
|
||||
await provider.close();
|
||||
await fixture.release();
|
||||
}
|
||||
}, [
|
||||
() => {
|
||||
releaseClose();
|
||||
denied = false;
|
||||
},
|
||||
async () => {
|
||||
await running;
|
||||
},
|
||||
() => provider.close(),
|
||||
() => fixture.release(),
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(["admission", "close"] as const)(
|
||||
|
||||
@@ -33,54 +33,6 @@ const parse = (bytes: Buffer, input = defaults) =>
|
||||
input,
|
||||
);
|
||||
|
||||
it("reads atoms, exports, imports, lambdas and the Code header", () => {
|
||||
const report = parse(erlangBeamFixture("demo"));
|
||||
expect(report.atom_encoding).toBe("utf8");
|
||||
expect(report.module).toBe("demo");
|
||||
expect(report.atoms?.map((atom) => atom.name)).toEqual([
|
||||
"demo",
|
||||
"module_info",
|
||||
"start",
|
||||
"erlang",
|
||||
"get_module_info",
|
||||
]);
|
||||
expect(report.exports).toEqual([
|
||||
{ function_index: 3, function: "start", arity: 0, label: 2 },
|
||||
{ function_index: 2, function: "module_info", arity: 0, label: 1 },
|
||||
{ function_index: 2, function: "module_info", arity: 1, label: 3 },
|
||||
]);
|
||||
expect(report.imports).toEqual([
|
||||
{
|
||||
module_index: 4,
|
||||
module: "erlang",
|
||||
function_index: 5,
|
||||
function: "get_module_info",
|
||||
arity: 1,
|
||||
},
|
||||
]);
|
||||
expect(report.lambdas?.[0]).toMatchObject({
|
||||
function: "start",
|
||||
arity: 1,
|
||||
label: 4,
|
||||
old_unique: 42,
|
||||
});
|
||||
expect(report.code).toMatchObject({
|
||||
instruction_set: 0,
|
||||
opcode_max: 169,
|
||||
labels: 4,
|
||||
functions: 2,
|
||||
extra_header_bytes: 0,
|
||||
instructions: { bytes: 4 },
|
||||
});
|
||||
expect(report.chunks.map((chunk) => chunk.name)).toEqual([
|
||||
"AtU8",
|
||||
"Code",
|
||||
"ExpT",
|
||||
"ImpT",
|
||||
"FunT",
|
||||
]);
|
||||
});
|
||||
|
||||
it("selects the last table and Code payload for every repeated chunk name", () => {
|
||||
const original = erlangBeamFixture("selected");
|
||||
const decoy = erlangBeamFixture("decoy");
|
||||
@@ -236,41 +188,6 @@ it.each([Buffer.from([0]), Buffer.from([0, 0, 0, 1])])(
|
||||
},
|
||||
);
|
||||
|
||||
// Expected values come from beam_lib:chunks/2 on the same file under OTP 29.
|
||||
it("reads a compiler-produced OTP 29 BEAM with the compact atom table", () => {
|
||||
const report = parse(erlangBeamOtp29Fixture());
|
||||
expect(report.module).toBe("rea_beam_fixture");
|
||||
expect(report.atoms?.map((atom) => atom.name)).toEqual([
|
||||
"rea_beam_fixture",
|
||||
"hello",
|
||||
"lists",
|
||||
"reverse",
|
||||
"module_info",
|
||||
"erlang",
|
||||
"get_module_info",
|
||||
]);
|
||||
expect(report.exports).toEqual([
|
||||
{ function_index: 5, function: "module_info", arity: 1, label: 6 },
|
||||
{ function_index: 5, function: "module_info", arity: 0, label: 4 },
|
||||
{ function_index: 2, function: "hello", arity: 1, label: 2 },
|
||||
]);
|
||||
expect(
|
||||
report.imports?.map((row) => [row.module, row.function, row.arity]),
|
||||
).toEqual([
|
||||
["lists", "reverse", 1],
|
||||
["erlang", "get_module_info", 1],
|
||||
["erlang", "get_module_info", 2],
|
||||
]);
|
||||
expect(report.code).toMatchObject({
|
||||
instruction_set: 0,
|
||||
opcode_max: 181,
|
||||
labels: 7,
|
||||
functions: 3,
|
||||
extra_header_bytes: 0,
|
||||
instructions: { bytes: 49 },
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts extended compact atom lengths and rejects truncated or overflowing values", () => {
|
||||
const compactAtom = (length: Buffer, name: Buffer): Buffer =>
|
||||
erlangBeamChunksFixture([
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, it } from "vitest";
|
||||
import {
|
||||
decodeEvmBytecodeCarrier,
|
||||
@@ -22,14 +24,23 @@ it.each(["0x1", "60 00", "0xzz", "\uFEFF6000", "\u00a06000"])(
|
||||
);
|
||||
},
|
||||
);
|
||||
it("rejects a long interior separator run in linear time", () => {
|
||||
const text = `60${" ".repeat(200_000)}00`;
|
||||
const started = performance.now();
|
||||
expect(() => decodeEvmBytecodeCarrier(Buffer.from(text), "hex")).toThrow(
|
||||
EvmCarrierFailure,
|
||||
it("rejects a long interior separator run without pathological backtracking", async () => {
|
||||
const moduleUrl = new URL("./EvmBytecodeCarrier.ts", import.meta.url).href;
|
||||
const source = `
|
||||
import assert from "node:assert/strict";
|
||||
import { decodeEvmBytecodeCarrier, EvmCarrierFailure } from ${JSON.stringify(moduleUrl)};
|
||||
const text = "60" + " ".repeat(1_000_000) + "00";
|
||||
assert.throws(() => decodeEvmBytecodeCarrier(Buffer.from(text), "hex"), EvmCarrierFailure);
|
||||
`;
|
||||
await promisify(execFile)(
|
||||
process.execPath,
|
||||
["--input-type=module", "-e", source],
|
||||
{
|
||||
timeout: 15_000,
|
||||
killSignal: "SIGKILL",
|
||||
},
|
||||
);
|
||||
expect(performance.now() - started).toBeLessThan(1_000);
|
||||
});
|
||||
}, 20_000);
|
||||
it("distinguishes raw bytes, empty code and malformed UTF-8", () => {
|
||||
const bytes = Uint8Array.from([0xff, 0x00]);
|
||||
const decoded = decodeEvmBytecodeCarrier(bytes, "raw");
|
||||
|
||||
@@ -88,11 +88,6 @@ describe("flattened devicetree", () => {
|
||||
] as const)("rejects %s", (_name, mutate) =>
|
||||
expect(() => parse(mutate(devicetreeFixture()))).toThrow(),
|
||||
);
|
||||
it("distinguishes unsupported versions", () => {
|
||||
expect(() => parse(devicetreeFixture(undefined, 18))).toThrow(
|
||||
expect.objectContaining({ kind: "unsupported" }),
|
||||
);
|
||||
});
|
||||
it("bounds depth, nodes and properties", () => {
|
||||
let root: FixtureNode = { name: "leaf" };
|
||||
for (let i = 0; i < 10; i++) root = { name: `n${i}`, children: [root] };
|
||||
@@ -190,7 +185,6 @@ it("leaves conflicting phandle declarations unresolved", () => {
|
||||
]);
|
||||
});
|
||||
it.each([
|
||||
["empty", Buffer.alloc(0)],
|
||||
["short", Buffer.from([0, 0])],
|
||||
["multiple cells", cells(1, 2)],
|
||||
])(
|
||||
|
||||
@@ -109,6 +109,8 @@ describe("GameCube DOL Ghidra profile", () => {
|
||||
);
|
||||
if (!left.ok) throw left.error;
|
||||
if (!right.ok) throw right.error;
|
||||
expect(left.value.profile?.digest).toEqual(expect.any(String));
|
||||
expect(right.value.profile?.digest).toEqual(expect.any(String));
|
||||
expect(left.value.profile?.digest).not.toBe(right.value.profile?.digest);
|
||||
});
|
||||
|
||||
|
||||
@@ -110,9 +110,7 @@ describe("native load-image result projection", () => {
|
||||
);
|
||||
expect(parsed.ok).toBe(true);
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
expect(JSON.stringify(parsed.value.value)).toBe(
|
||||
JSON.stringify(observations),
|
||||
);
|
||||
expect(parsed.value.value).toEqual(observations);
|
||||
expect(parsed.value.rawResult).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -129,7 +127,7 @@ describe("native load-image result projection", () => {
|
||||
);
|
||||
expect(parsed.ok).toBe(true);
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
expect(JSON.stringify(parsed.value.value)).toContain('"entry_context":[]');
|
||||
expect(parsed.value.value).toMatchObject({ entry_context: [] });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -419,6 +419,8 @@ describe("Ghidra language override profile", () => {
|
||||
});
|
||||
expect(parameters).not.toHaveProperty("language_selection");
|
||||
expect(ghidraProfileLanguageOverride(parameters)).toBeUndefined();
|
||||
expect(automatic.value.profile?.digest).toEqual(expect.any(String));
|
||||
expect(configured.value.profile?.digest).toEqual(expect.any(String));
|
||||
expect(automatic.value.profile?.digest).not.toBe(
|
||||
configured.value.profile?.digest,
|
||||
);
|
||||
|
||||
@@ -126,6 +126,8 @@ describe("bounded Ghidra MIPS admission", () => {
|
||||
},
|
||||
gnu_attributes: null,
|
||||
});
|
||||
expect(left.value.profile?.digest).toEqual(expect.any(String));
|
||||
expect(right.value.profile?.digest).toEqual(expect.any(String));
|
||||
expect(left.value.profile?.digest).not.toBe(right.value.profile?.digest);
|
||||
const windows = new GhidraProvider(
|
||||
config,
|
||||
|
||||
@@ -124,6 +124,8 @@ describe("bounded Ghidra PowerPC admission", () => {
|
||||
);
|
||||
if (!left.ok) throw left.error;
|
||||
if (!right.ok) throw right.error;
|
||||
expect(left.value.profile?.digest).toEqual(expect.any(String));
|
||||
expect(right.value.profile?.digest).toEqual(expect.any(String));
|
||||
expect(left.value.profile?.digest).not.toBe(right.value.profile?.digest);
|
||||
});
|
||||
|
||||
|
||||
@@ -482,6 +482,7 @@ describe("Ghidra raw-binary profile", () => {
|
||||
expect(resolved.ok && shifted.ok).toBe(true);
|
||||
if (!resolved.ok || !shifted.ok || resolved.value.profile === null)
|
||||
throw new Error("expected raw-binary profiles");
|
||||
expect(shifted.value.profile?.digest).toEqual(expect.any(String));
|
||||
expect(shifted.value.profile?.digest).not.toBe(
|
||||
resolved.value.profile.digest,
|
||||
);
|
||||
@@ -4864,14 +4865,14 @@ it("reports every Windows existing-project capability unavailable before open",
|
||||
REA_GHIDRA_PROJECT_PATH: join(process.cwd(), "selected.gpr"),
|
||||
});
|
||||
try {
|
||||
const capabilities = ghidra.capabilities();
|
||||
expect(capabilities.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
ghidra
|
||||
.capabilities()
|
||||
.every(
|
||||
(capability) =>
|
||||
!capability.available &&
|
||||
capability.availabilityCode === "unsupported_host",
|
||||
),
|
||||
capabilities.every(
|
||||
(capability) =>
|
||||
!capability.available &&
|
||||
capability.availabilityCode === "unsupported_host",
|
||||
),
|
||||
).toBe(true);
|
||||
const readiness = await ghidra.inspectReadinessSnapshot();
|
||||
expect(readiness.availability).toMatchObject({
|
||||
@@ -4879,6 +4880,9 @@ it("reports every Windows existing-project capability unavailable before open",
|
||||
code: "unsupported_host",
|
||||
reason: EXISTING_PROJECT_WINDOWS_UNAVAILABLE,
|
||||
});
|
||||
expect(readiness.capabilities.map(({ operation }) => operation)).toEqual(
|
||||
capabilities.map(({ operation }) => operation),
|
||||
);
|
||||
expect(
|
||||
readiness.capabilities.every(
|
||||
(capability) =>
|
||||
|
||||
@@ -166,6 +166,8 @@ const registerRiscvLanguageCommitmentCases = () => {
|
||||
elf_class: 32,
|
||||
rvc_declared: false,
|
||||
});
|
||||
expect(wide.value.profile?.digest).toEqual(expect.any(String));
|
||||
expect(narrow.value.profile?.digest).toEqual(expect.any(String));
|
||||
expect(wide.value.profile?.digest).not.toBe(narrow.value.profile?.digest);
|
||||
});
|
||||
|
||||
|
||||
@@ -52,7 +52,13 @@ it("reports truncated cyclic causes in serializable diagnostics", () => {
|
||||
cause,
|
||||
).diagnostics;
|
||||
|
||||
expect(JSON.stringify(diagnostics)).toContain('"cause_truncated":true');
|
||||
const serialized = JSON.parse(JSON.stringify(diagnostics)) as {
|
||||
failure_cause: unknown;
|
||||
};
|
||||
let nested = serialized.failure_cause;
|
||||
while (typeof nested === "object" && nested !== null && "cause" in nested)
|
||||
nested = nested.cause;
|
||||
expect(nested).toMatchObject({ cause_truncated: true });
|
||||
});
|
||||
|
||||
it("keeps bounded compiler and nested launcher outcomes in diagnostics", () => {
|
||||
|
||||
@@ -292,30 +292,26 @@ describe("Ghidra extension profile and producer validation", () => {
|
||||
it("requires exact inventory, artifact digest and consistent producer status", async () => {
|
||||
const { extension } = await artifact();
|
||||
const valid = result(extension.sha256);
|
||||
const inventoryMismatch =
|
||||
"Ghidra extension result inventory differs from its committed profile.";
|
||||
expect(validateGhidraExtensionResults([extension], [valid])).toBeNull();
|
||||
expect(validateGhidraExtensionResults([extension], [])).toBe(
|
||||
inventoryMismatch,
|
||||
);
|
||||
expect(validateGhidraExtensionResults([], [valid])).toBe(inventoryMismatch);
|
||||
expect(validateGhidraExtensionResults([extension], [valid, valid])).toBe(
|
||||
inventoryMismatch,
|
||||
expect(validateGhidraExtensionResults([extension], [])).toContain(
|
||||
"inventory",
|
||||
);
|
||||
expect(validateGhidraExtensionResults([], [valid])).toContain("inventory");
|
||||
expect(
|
||||
validateGhidraExtensionResults(
|
||||
[extension],
|
||||
[{ ...valid, sha256: "b".repeat(64) }],
|
||||
),
|
||||
).toBe("Ghidra extension identity mismatch: nativeaot");
|
||||
validateGhidraExtensionResults([extension], [valid, valid]),
|
||||
).toContain("inventory");
|
||||
const identityMismatch = validateGhidraExtensionResults(
|
||||
[extension],
|
||||
[{ ...valid, sha256: "b".repeat(64) }],
|
||||
);
|
||||
expect(identityMismatch).toContain("identity");
|
||||
expect(identityMismatch).toContain(extension.id);
|
||||
expect(
|
||||
validateGhidraExtensionResults(
|
||||
[extension],
|
||||
[{ ...valid, status: "complete" }],
|
||||
),
|
||||
).toBe(
|
||||
"NativeAOT extension returned a malformed or inconsistent producer report.",
|
||||
);
|
||||
).toContain("producer report");
|
||||
});
|
||||
it("requires matching explicit configuration for imported executable profiles", async () => {
|
||||
const { extension, path } = await artifact();
|
||||
@@ -329,39 +325,28 @@ describe("Ghidra extension profile and producer validation", () => {
|
||||
).toBeNull();
|
||||
expect(
|
||||
validateGhidraExtensionProfile([extension], config(), target, "linux"),
|
||||
).toBe(
|
||||
"Configured Ghidra extension inventory differs from the resolved analysis profile.",
|
||||
).toContain("inventory");
|
||||
const unsupportedHost = validateGhidraExtensionProfile(
|
||||
[extension],
|
||||
config(path),
|
||||
target,
|
||||
"darwin",
|
||||
);
|
||||
expect(
|
||||
validateGhidraExtensionProfile(
|
||||
[extension],
|
||||
config(path),
|
||||
target,
|
||||
"darwin",
|
||||
),
|
||||
).toBe(
|
||||
"NativeAOT recovery is verified on Linux only; omit REA_GHIDRA_NATIVEAOT_JAR for ordinary native analysis.",
|
||||
);
|
||||
expect(
|
||||
validateGhidraExtensionProfile(
|
||||
[{ ...extension, entry_class: "arbitrary.Code" }],
|
||||
expect(unsupportedHost).toContain("Linux");
|
||||
expect(unsupportedHost).toContain("REA_GHIDRA_NATIVEAOT_JAR");
|
||||
for (const changed of [
|
||||
{ ...extension, entry_class: "arbitrary.Code" },
|
||||
{ ...extension, configured_path: "/another.jar" },
|
||||
]) {
|
||||
const invalid = validateGhidraExtensionProfile(
|
||||
[changed],
|
||||
config(path),
|
||||
target,
|
||||
"linux",
|
||||
),
|
||||
).toBe(
|
||||
"Ghidra extension profile lacks matching explicit local configuration: nativeaot",
|
||||
);
|
||||
expect(
|
||||
validateGhidraExtensionProfile(
|
||||
[{ ...extension, configured_path: "/another.jar" }],
|
||||
config(path),
|
||||
target,
|
||||
"linux",
|
||||
),
|
||||
).toBe(
|
||||
"Ghidra extension profile lacks matching explicit local configuration: nativeaot",
|
||||
);
|
||||
);
|
||||
expect(invalid).toContain("configuration");
|
||||
expect(invalid).toContain(extension.id);
|
||||
}
|
||||
});
|
||||
it("does not classify PE/CLI or ReadyToRun as NativeAOT", async () => {
|
||||
const resolved = await resolveGhidraExtensions(
|
||||
@@ -465,8 +450,8 @@ describe("Ghidra extension recovery coverage", () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(validateGhidraExtensionResults([extension], [incomplete])).toBe(
|
||||
"NativeAOT recovery coverage contradicts its reported completion status.",
|
||||
expect(validateGhidraExtensionResults([extension], [incomplete])).toContain(
|
||||
"coverage",
|
||||
);
|
||||
expect(
|
||||
validateGhidraExtensionResults(
|
||||
@@ -485,9 +470,7 @@ describe("Ghidra extension recovery coverage", () => {
|
||||
[extension],
|
||||
[{ ...positive, result: { ...positive.result, discovery: null } }],
|
||||
),
|
||||
).toBe(
|
||||
"NativeAOT extension returned a malformed or inconsistent producer report.",
|
||||
);
|
||||
).toContain("producer report");
|
||||
});
|
||||
it("preserves real loader failures without requiring a successful analyzer report", async () => {
|
||||
const { extension } = await artifact();
|
||||
@@ -512,14 +495,13 @@ describe("Ghidra extension recovery coverage", () => {
|
||||
const incomplete = result(extension.sha256);
|
||||
incomplete.result.status = "complete";
|
||||
incomplete.result.reason = null;
|
||||
expect(
|
||||
validateGhidraExtensionResults(
|
||||
[extension],
|
||||
[{ ...incomplete, status: "complete", reason: null }],
|
||||
),
|
||||
).toBe(
|
||||
"NativeAOT recovery omitted its supported format, method-table inventory or derived-memory identity.",
|
||||
const rejection = validateGhidraExtensionResults(
|
||||
[extension],
|
||||
[{ ...incomplete, status: "complete", reason: null }],
|
||||
);
|
||||
expect(rejection).toContain("format");
|
||||
expect(rejection).toContain("method-table");
|
||||
expect(rejection).toContain("derived-memory");
|
||||
});
|
||||
it.each(["not_applicable", "unsupported", "failed"] as const)(
|
||||
"rejects contradictory %s reports without discarding discovery context",
|
||||
@@ -544,7 +526,7 @@ describe("Ghidra extension recovery coverage", () => {
|
||||
[extension],
|
||||
[{ ...report, result: { ...report.result, method_tables: 1 } }],
|
||||
),
|
||||
).toBe("NativeAOT non-recovery status carries recovered metadata.");
|
||||
).toContain("metadata");
|
||||
for (const reason of [null, "", " "] as const)
|
||||
expect(
|
||||
validateGhidraExtensionResults(
|
||||
@@ -557,7 +539,7 @@ describe("Ghidra extension recovery coverage", () => {
|
||||
},
|
||||
],
|
||||
),
|
||||
).toBe("NativeAOT non-recovery status omits its required reason.");
|
||||
).toContain("reason");
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -87,10 +87,12 @@ describe("Hopper name inventory labels", () => {
|
||||
|
||||
it("leaves malformed provider inventories untouched for the consumer to reject", () => {
|
||||
const value = [{ address: 1, value: "__objc_class_HuntRoot_class" }];
|
||||
const original = structuredClone(value);
|
||||
expect(projectHopperNameInventory(value)).toEqual({
|
||||
result: value,
|
||||
result: original,
|
||||
changed: false,
|
||||
});
|
||||
expect(value).toEqual(original);
|
||||
});
|
||||
|
||||
it("preserves source rows and the exact raw reply through the provider boundary", async () => {
|
||||
@@ -110,6 +112,7 @@ describe("Hopper name inventory labels", () => {
|
||||
objc_class_label: "hopper_legacy_class_object",
|
||||
},
|
||||
];
|
||||
const originalSource = structuredClone(source);
|
||||
const callTool = vi
|
||||
.spyOn(HopperClient.prototype, "callTool")
|
||||
.mockResolvedValue(ok(source));
|
||||
@@ -126,14 +129,13 @@ describe("Hopper name inventory labels", () => {
|
||||
format: "analysis-database",
|
||||
});
|
||||
|
||||
const result = await client.execute("list_names", {});
|
||||
|
||||
expect(callTool).toHaveBeenCalledOnce();
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
try {
|
||||
const result = await client.execute("list_names", {});
|
||||
expect(callTool).toHaveBeenCalledOnce();
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.result).toEqual([
|
||||
{
|
||||
...source[0],
|
||||
...originalSource[0],
|
||||
objc_class_label: "hopper_legacy_class_object",
|
||||
},
|
||||
{
|
||||
@@ -142,9 +144,14 @@ describe("Hopper name inventory labels", () => {
|
||||
provider_metadata: { ordinal: 2 },
|
||||
},
|
||||
]);
|
||||
expect(result.value.rawResult).toEqual(source);
|
||||
expect(result.value.rawResult).toEqual(originalSource);
|
||||
expect(source).toEqual(originalSource);
|
||||
} finally {
|
||||
try {
|
||||
await client.close();
|
||||
} finally {
|
||||
callTool.mockRestore();
|
||||
}
|
||||
}
|
||||
await client.close();
|
||||
callTool.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -230,6 +230,7 @@ describe("Hopper relationship coverage", () => {
|
||||
: operation === "procedure_callees"
|
||||
? ["0x2000"]
|
||||
: [];
|
||||
const originalSource = structuredClone(source);
|
||||
const callTool = vi
|
||||
.spyOn(HopperClient.prototype, "callTool")
|
||||
.mockImplementation((method) =>
|
||||
@@ -277,7 +278,8 @@ describe("Hopper relationship coverage", () => {
|
||||
procedure: "0x1000",
|
||||
});
|
||||
if (!observed.ok) throw observed.error;
|
||||
expect(observed.value.result).toEqual(source);
|
||||
expect(observed.value.result).toEqual(originalSource);
|
||||
expect(source).toEqual(originalSource);
|
||||
if (operation === "procedure_callers") {
|
||||
expect(observed.value.rawResult).toMatchObject({
|
||||
native_result: {
|
||||
|
||||
@@ -61,6 +61,7 @@ it.each(["search_strings", "search_procedures"] as const)(
|
||||
selected,
|
||||
{ ...selected, value: "matched alternative" },
|
||||
];
|
||||
const originalSource = structuredClone(source);
|
||||
const callTool = vi
|
||||
.spyOn(HopperClient.prototype, "callTool")
|
||||
.mockResolvedValue(ok(source));
|
||||
@@ -83,12 +84,12 @@ it.each(["search_strings", "search_procedures"] as const)(
|
||||
case_sensitive: true,
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) expect(result.value.result).toEqual([selected]);
|
||||
if (result.ok) expect(result.value.result).toEqual([originalSource[1]]);
|
||||
expect(callTool).toHaveBeenCalledOnce();
|
||||
expect(callTool.mock.calls[0]?.[0]).toBe(
|
||||
operation === "search_strings" ? "list_strings" : "list_procedures",
|
||||
);
|
||||
expect(source[1]).toEqual(selected);
|
||||
expect(source).toEqual(originalSource);
|
||||
} finally {
|
||||
await client.close();
|
||||
callTool.mockRestore();
|
||||
|
||||
@@ -32,9 +32,9 @@ it.each([true, false])(
|
||||
workspaceRoot: rawRoot,
|
||||
}),
|
||||
);
|
||||
const before = await readdir(root);
|
||||
const before = (await readdir(root)).sort();
|
||||
const inspection = await inspectIdaRegistration(path, resourceScope);
|
||||
const after = await readdir(root);
|
||||
const after = (await readdir(root)).sort();
|
||||
expect(inspection.available).toBe(exists);
|
||||
expect(
|
||||
inspection.checks.find(({ name }) => name === "private_workspace")
|
||||
@@ -62,7 +62,7 @@ it.skipIf(process.platform === "win32")(
|
||||
workspaceRoot: selected,
|
||||
}),
|
||||
);
|
||||
const before = await readdir(root);
|
||||
const before = (await readdir(root)).sort();
|
||||
const inspection = await inspectIdaRegistration(path, resourceScope);
|
||||
expect(inspection.available).toBe(false);
|
||||
const check = inspection.checks.find(
|
||||
@@ -71,7 +71,7 @@ it.skipIf(process.platform === "win32")(
|
||||
expect(check).toMatchObject({ ok: false, code: "invalid_configuration" });
|
||||
expect(check?.detail).toContain(selected);
|
||||
expect(check?.detail).toContain("ENOTDIR");
|
||||
expect(await readdir(root)).toEqual(before);
|
||||
expect((await readdir(root)).sort()).toEqual(before);
|
||||
expect(await readdir(workspace)).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
+14
-13
@@ -112,7 +112,7 @@ it.each(["missing", "file", "directory", "omitted"] as const)(
|
||||
throw new Error("Readiness must not connect to IDA");
|
||||
});
|
||||
const provider = new IdaProvider(config.value, {}, connect);
|
||||
const before = await readdir(root);
|
||||
const before = (await readdir(root)).sort();
|
||||
try {
|
||||
const snapshot = await provider.inspectReadinessSnapshot();
|
||||
if (kind === "missing" || kind === "file") {
|
||||
@@ -135,7 +135,7 @@ it.each(["missing", "file", "directory", "omitted"] as const)(
|
||||
)?.reason,
|
||||
).toContain("direct call edges");
|
||||
} else expect(snapshot.availability.status).toBe("available");
|
||||
expect(await readdir(root)).toEqual(before);
|
||||
expect((await readdir(root)).sort()).toEqual(before);
|
||||
expect(connect).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
const closed = await provider.closeOwnedResources();
|
||||
@@ -200,20 +200,20 @@ describe("IDA provider target support", () => {
|
||||
const config = parseConfig({});
|
||||
if (!config.ok) throw new Error("expected valid configuration");
|
||||
const provider = new IdaProvider(config.value, {});
|
||||
expect(
|
||||
provider.inspectTargetSupport({
|
||||
path: "/tmp/chre_pal.so",
|
||||
sha256: "a".repeat(64),
|
||||
kind: "executable",
|
||||
format: "elf",
|
||||
architecture: "hexagon",
|
||||
availableArchitectures: ["hexagon"],
|
||||
}),
|
||||
).toMatchObject({
|
||||
const support = provider.inspectTargetSupport({
|
||||
path: "/tmp/chre_pal.so",
|
||||
sha256: "a".repeat(64),
|
||||
kind: "executable",
|
||||
format: "elf",
|
||||
architecture: "hexagon",
|
||||
availableArchitectures: ["hexagon"],
|
||||
});
|
||||
expect(support).toMatchObject({
|
||||
status: "unsupported",
|
||||
code: "architecture_unsupported",
|
||||
reason: expect.stringMatching(/hexagon.*Ghidra/su),
|
||||
});
|
||||
expect(support.reason).toContain("hexagon");
|
||||
expect(support.reason).toContain("Ghidra");
|
||||
});
|
||||
it("admits only the families IDA admits today, so new families stay refused", () => {
|
||||
const config = parseConfig({});
|
||||
@@ -336,6 +336,7 @@ it.each(["http", "stdio"] as const)(
|
||||
);
|
||||
const changed = await provider.resolveAnalysisProfile(target);
|
||||
if (!changed.ok) throw changed.error;
|
||||
expect(changed.value.profile?.digest).toEqual(expect.any(String));
|
||||
expect(changed.value.profile?.digest).not.toBe(profiles[0]);
|
||||
} finally {
|
||||
await provider.closeOwnedResources();
|
||||
|
||||
@@ -1938,9 +1938,11 @@ describe("headless IDA cleanup verification", () => {
|
||||
});
|
||||
it("releases a privately owned worker after malformed open output using verified session inventory", async () => {
|
||||
const { producer, client, root } = await fixture("headless");
|
||||
let ownedId: string | undefined;
|
||||
producer.beforeCall = async (name, args) => {
|
||||
if (name === "idb_open") {
|
||||
producer.database = z.string().parse(args.preferred_session_id);
|
||||
ownedId = z.string().parse(args.preferred_session_id);
|
||||
producer.database = ownedId;
|
||||
producer.inputPath = z.string().parse(args.input_path);
|
||||
}
|
||||
};
|
||||
@@ -1953,9 +1955,10 @@ describe("headless IDA cleanup verification", () => {
|
||||
: undefined;
|
||||
expect((await client.execute("health", {})).ok).toBe(false);
|
||||
expect((await client.close()).ok).toBe(true);
|
||||
expect(ownedId).toEqual(expect.any(String));
|
||||
expect(
|
||||
producer.calls.find(({ name }) => name === "idb_close")?.args.database,
|
||||
).toMatch(/^rea-/u);
|
||||
).toBe(ownedId);
|
||||
expect(await readdir(root)).toEqual(["target.elf"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@ import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { withVerifierCleanup } from "../../scripts/lib/verifier-cleanup.mjs";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
|
||||
const fileAccess = vi.hoisted(() => ({
|
||||
path: undefined as string | undefined,
|
||||
@@ -248,31 +250,43 @@ it("drains pending Electron discovery before shutdown cleanup completes", async
|
||||
const listing = provider.listTargets(
|
||||
listElectronTargetsInputSchema.parse({ cdp_endpoint: fake.endpoint }),
|
||||
);
|
||||
await started;
|
||||
let closed = false;
|
||||
const closing = provider.close().then(() => {
|
||||
closed = true;
|
||||
});
|
||||
const closedBeforeDiscovery = await Promise.race([
|
||||
closing.then(() => true),
|
||||
new Promise<boolean>((resolve) => setImmediate(() => resolve(false))),
|
||||
]);
|
||||
expect(closedBeforeDiscovery).toBe(false);
|
||||
expect(closed).toBe(false);
|
||||
releaseDiscovery();
|
||||
let closing: Promise<void> | undefined;
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(started, listing, "Electron discovery");
|
||||
closing = provider.close().then(() => {
|
||||
closed = true;
|
||||
});
|
||||
const closedBeforeDiscovery = await Promise.race([
|
||||
closing.then(() => true),
|
||||
new Promise<boolean>((resolve) => setImmediate(() => resolve(false))),
|
||||
]);
|
||||
expect(closedBeforeDiscovery).toBe(false);
|
||||
expect(closed).toBe(false);
|
||||
releaseDiscovery();
|
||||
|
||||
const [listed] = await Promise.all([listing, closing]);
|
||||
expect(closed).toBe(true);
|
||||
expect(listed.ok).toBe(true);
|
||||
expect(fileAccess.calls).toContain("stat");
|
||||
const rejected = await provider.listTargets(
|
||||
listElectronTargetsInputSchema.parse({ cdp_endpoint: fake.endpoint }),
|
||||
);
|
||||
expect(rejected.ok).toBe(false);
|
||||
if (rejected.ok)
|
||||
throw new Error("Expected closed provider admission failure");
|
||||
expect(rejected.error.cleanupIncomplete).toBe(false);
|
||||
expect(rejected.error.cleanupResources).toEqual([]);
|
||||
const [listed] = await Promise.all([listing, closing]);
|
||||
expect(closed).toBe(true);
|
||||
expect(listed.ok).toBe(true);
|
||||
expect(fileAccess.calls).toContain("stat");
|
||||
const rejected = await provider.listTargets(
|
||||
listElectronTargetsInputSchema.parse({ cdp_endpoint: fake.endpoint }),
|
||||
);
|
||||
expect(rejected.ok).toBe(false);
|
||||
if (rejected.ok)
|
||||
throw new Error("Expected closed provider admission failure");
|
||||
expect(rejected.error.cleanupIncomplete).toBe(false);
|
||||
expect(rejected.error.cleanupResources).toEqual([]);
|
||||
}, [
|
||||
releaseDiscovery,
|
||||
async () => {
|
||||
await listing;
|
||||
},
|
||||
async () => {
|
||||
await closing;
|
||||
},
|
||||
() => provider.close(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps ordinary and malformed file-location authorization unchanged", async () => {
|
||||
|
||||
@@ -78,8 +78,8 @@ it.each([
|
||||
expect(diagnostic).toMatchObject({
|
||||
code: "target_unavailable",
|
||||
message: expect.stringContaining(message),
|
||||
remediation: { action: expect.stringContaining(action) },
|
||||
});
|
||||
expect(JSON.stringify(diagnostic)).toContain(action);
|
||||
expect(inspector.commands).toEqual([]);
|
||||
} finally {
|
||||
await inspector.close();
|
||||
|
||||
@@ -6,6 +6,11 @@ import { pathToFileURL } from "node:url";
|
||||
|
||||
import { expect, it, onTestFinished, vi } from "vitest";
|
||||
import { WebSocketServer } from "ws";
|
||||
import {
|
||||
withVerifierCleanup,
|
||||
closeVerifierResources,
|
||||
} from "../../scripts/lib/verifier-cleanup.mjs";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
|
||||
import { CdpConnection } from "../browser/CdpConnection.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
@@ -74,6 +79,8 @@ const inspectorCloseFixture = async () => {
|
||||
const firstClose = new Promise<void>((_resolve, reject) => {
|
||||
rejectFirstClose = reject;
|
||||
});
|
||||
// Cleanup may reject the gate before a transport consumes it.
|
||||
void firstClose.catch(() => undefined);
|
||||
const originalClose = CdpConnection.prototype.close;
|
||||
const closeSpy = vi
|
||||
.spyOn(CdpConnection.prototype, "close")
|
||||
@@ -89,14 +96,20 @@ const inspectorCloseFixture = async () => {
|
||||
});
|
||||
|
||||
onTestFinished(async () => {
|
||||
closeSpy.mockRestore();
|
||||
if (closeCalls > 0) rejectFirstClose?.(new Error("fixture finalization"));
|
||||
await new V8InspectorProvider(undefined, admission).close();
|
||||
for (const socket of sockets.clients) socket.terminate();
|
||||
await new Promise<void>((resolve) => sockets.close(() => resolve()));
|
||||
if (server.listening)
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
await rm(root, { recursive: true, force: true });
|
||||
await closeVerifierResources([
|
||||
() => rejectFirstClose?.(new Error("fixture finalization")),
|
||||
() => closeSpy.mockRestore(),
|
||||
() => new V8InspectorProvider(undefined, admission).close(),
|
||||
() => {
|
||||
for (const socket of sockets.clients) socket.terminate();
|
||||
},
|
||||
() => new Promise<void>((resolve) => sockets.close(() => resolve())),
|
||||
async () => {
|
||||
if (server.listening)
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
},
|
||||
() => rm(root, { recursive: true, force: true }),
|
||||
]);
|
||||
});
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
@@ -141,17 +154,33 @@ it("retains cancelled final-close ownership and retries the exact transport befo
|
||||
const observing = observeJavaScriptRuntime(provider(), input, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
await closeStarted;
|
||||
controller.abort(cancellationReason);
|
||||
let firstObservationSettled = false;
|
||||
void observing.finally(() => {
|
||||
firstObservationSettled = true;
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(firstObservationSettled).toBe(false);
|
||||
rejectFirstClose?.(new Error("controlled final socket close failure"));
|
||||
|
||||
const result = await observing;
|
||||
const result = await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(
|
||||
closeStarted,
|
||||
observing,
|
||||
"inspector final close",
|
||||
);
|
||||
controller.abort(cancellationReason);
|
||||
let firstObservationSettled = false;
|
||||
void observing.then(
|
||||
() => {
|
||||
firstObservationSettled = true;
|
||||
},
|
||||
() => {
|
||||
firstObservationSettled = true;
|
||||
},
|
||||
);
|
||||
await Promise.resolve();
|
||||
expect(firstObservationSettled).toBe(false);
|
||||
rejectFirstClose(new Error("controlled final socket close failure"));
|
||||
return await observing;
|
||||
}, [
|
||||
() => controller.abort(new Error("fixture finalization")),
|
||||
() => rejectFirstClose(new Error("fixture finalization")),
|
||||
async () => {
|
||||
await observing;
|
||||
},
|
||||
]);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cleanup-incomplete cancellation");
|
||||
const error = result.error;
|
||||
@@ -202,15 +231,26 @@ it("retains cancelled final-close ownership and retries the exact transport befo
|
||||
const activeObservation = observeJavaScriptRuntime(provider(), activeInput, {
|
||||
signal: activeController.signal,
|
||||
});
|
||||
try {
|
||||
await waitForConnections(4);
|
||||
const queuedController = new AbortController();
|
||||
let shutdown: Promise<void> | undefined;
|
||||
let queuedObservation:
|
||||
| ReturnType<typeof observeJavaScriptRuntime>
|
||||
| undefined;
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(
|
||||
waitForConnections(4),
|
||||
activeObservation,
|
||||
"active inspector connection",
|
||||
);
|
||||
let shutdownSettled = false;
|
||||
const shutdown = provider()
|
||||
shutdown = provider()
|
||||
.close()
|
||||
.then(() => {
|
||||
shutdownSettled = true;
|
||||
});
|
||||
const queuedObservation = observeJavaScriptRuntime(provider(), input);
|
||||
queuedObservation = observeJavaScriptRuntime(provider(), input, {
|
||||
signal: queuedController.signal,
|
||||
});
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(shutdownSettled).toBe(false);
|
||||
expect(fixture.acceptedConnections).toBe(4);
|
||||
@@ -221,12 +261,26 @@ it("retains cancelled final-close ownership and retries the exact transport befo
|
||||
expect(activeResult.ok).toBe(false);
|
||||
await shutdown;
|
||||
expect(shutdownSettled).toBe(true);
|
||||
await waitForConnections(5);
|
||||
await awaitAcknowledgement(
|
||||
waitForConnections(5),
|
||||
queuedObservation,
|
||||
"queued inspector connection",
|
||||
);
|
||||
const queuedResult = await queuedObservation;
|
||||
expect(queuedResult.ok).toBe(true);
|
||||
expect(fixture.acceptedConnections).toBe(5);
|
||||
expect(closeSpy).toHaveBeenCalledTimes(8);
|
||||
} finally {
|
||||
activeController.abort(new Error("fixture finalization"));
|
||||
}
|
||||
}, [
|
||||
() => activeController.abort(new Error("fixture finalization")),
|
||||
() => queuedController.abort(new Error("fixture finalization")),
|
||||
async () => {
|
||||
await activeObservation;
|
||||
},
|
||||
async () => {
|
||||
await shutdown;
|
||||
},
|
||||
async () => {
|
||||
await queuedObservation;
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -61,7 +61,7 @@ const tracer: NativeCallTracer = {
|
||||
};
|
||||
|
||||
describe("private otool operand lifecycle", () => {
|
||||
it("uses a private alias only for otool and removes it after all commands settle", async () => {
|
||||
it("uses a private alias for native commands and removes it after all commands settle", async () => {
|
||||
const path = await mkdtemp(join(tmpdir(), "rea-macho-operand-test-"));
|
||||
const selected = await parenthesizedTarget();
|
||||
const target = selected.target.path;
|
||||
@@ -90,10 +90,13 @@ describe("private otool operand lifecycle", () => {
|
||||
try {
|
||||
const result = await client.execute("inspect_macho", {});
|
||||
if (!result.ok) throw result.error;
|
||||
expect(JSON.stringify(result.value.result)).toContain(
|
||||
join(path, "artifact.macho"),
|
||||
);
|
||||
const alias = join(path, "artifact.macho");
|
||||
const report = inspectMachoSchema.parse(result.value.result);
|
||||
expect(
|
||||
report.provenance
|
||||
.find((entry) => entry.tool === "lipo")
|
||||
?.command.at(-1),
|
||||
).toBe(alias);
|
||||
expect(calls.some(({ operand }) => operand === alias)).toBe(true);
|
||||
expect(calls.some(({ operand }) => operand === target)).toBe(false);
|
||||
await expect(stat(path)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
@@ -166,7 +169,9 @@ describe("private otool operand lifecycle", () => {
|
||||
await rm(selected.directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("private operand command cleanup", () => {
|
||||
it("retains the alias until command cleanup is confirmed", async () => {
|
||||
const path = await mkdtemp(join(tmpdir(), "rea-macho-operand-test-"));
|
||||
let unresolved = true;
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, it, vi, onTestFinished } from "vitest";
|
||||
import { withVerifierCleanup } from "../../scripts/lib/verifier-cleanup.mjs";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
|
||||
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
@@ -246,22 +248,41 @@ it("waits for an in-flight signature binding before provider close", async () =>
|
||||
);
|
||||
const client = provider.createClient(target);
|
||||
const operation = client.execute("inspect_signature", {});
|
||||
await entered;
|
||||
|
||||
let closeSettled = false;
|
||||
const closing = client.close().finally(() => {
|
||||
closeSettled = true;
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(closeSettled).toBe(false);
|
||||
releaseRootFactory();
|
||||
await expect(operation).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisArtifactChangedError" },
|
||||
});
|
||||
await expect(closing).resolves.toMatchObject({ ok: true, value: null });
|
||||
expect(closeSettled).toBe(true);
|
||||
await expect(stat(roots[0]!)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
let closing: ReturnType<typeof client.close> | undefined;
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(
|
||||
entered,
|
||||
operation,
|
||||
"signature root acquisition",
|
||||
);
|
||||
closing = client.close().finally(() => {
|
||||
closeSettled = true;
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(closeSettled).toBe(false);
|
||||
releaseRootFactory();
|
||||
await expect(operation).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisArtifactChangedError" },
|
||||
});
|
||||
await expect(closing).resolves.toMatchObject({ ok: true, value: null });
|
||||
expect(closeSettled).toBe(true);
|
||||
await expect(stat(roots[0]!)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
}, [
|
||||
releaseRootFactory,
|
||||
async () => {
|
||||
await operation;
|
||||
},
|
||||
async () => {
|
||||
const result = await closing;
|
||||
if (result !== undefined && !result.ok) throw result.error;
|
||||
},
|
||||
async () => {
|
||||
const result = await client.close();
|
||||
if (!result.ok) throw result.error;
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("releases retained descriptors before retrying their containing root", async () => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { readFileSync } from "node:fs";
|
||||
import { expect, it } from "vitest";
|
||||
import { inspectElfNotesInputSchema } from "../../domain/native/elfNotes.js";
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import { BinaryFormatFailure } from "../metadata/BinaryContainer.js";
|
||||
import { decodeElfSectionNameTable } from "../metadata/ElfSectionNameTable.js";
|
||||
import { readElfDirectoryFromBuffer } from "../metadata/NativeSectionDirectory.js";
|
||||
import {
|
||||
@@ -243,20 +242,6 @@ it("keeps note facts but marks section names unavailable for unknown compression
|
||||
);
|
||||
});
|
||||
|
||||
it("tags valid extended section numbering as unsupported", () => {
|
||||
let failure: unknown;
|
||||
try {
|
||||
parse(elfExtendedSectionCountFixture());
|
||||
} catch (cause) {
|
||||
failure = cause;
|
||||
}
|
||||
expect(failure).toBeInstanceOf(BinaryFormatFailure);
|
||||
expect(failure).toMatchObject({
|
||||
kind: "unsupported",
|
||||
message: "ELF extended numbering is unsupported.",
|
||||
});
|
||||
});
|
||||
|
||||
it.each([1, 2])(
|
||||
"admits algorithm %i section names when max_file_bytes includes expansion",
|
||||
(compressionType) => {
|
||||
|
||||
@@ -7,6 +7,8 @@ import { withArtifactRegularFile } from "../artifacts/ArtifactFileOperation.js";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import { withVerifierCleanup } from "../../scripts/lib/verifier-cleanup.mjs";
|
||||
import { OwnedCommandFailure } from "./OwnedCommand.js";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { PassThrough } from "node:stream";
|
||||
@@ -837,7 +839,9 @@ it("observes cancellation after pending descriptor cleanup before admitting repl
|
||||
{ message: "busy", details: {} },
|
||||
);
|
||||
let calls = 0;
|
||||
try {
|
||||
const caller = new AbortController();
|
||||
let pending: ReturnType<typeof resources.run> | undefined;
|
||||
await withVerifierCleanup(async () => {
|
||||
const first = await resources.run(async () =>
|
||||
ok(await resources.read("fixture", 1)),
|
||||
);
|
||||
@@ -845,13 +849,12 @@ it("observes cancellation after pending descriptor cleanup before admitting repl
|
||||
ok: false,
|
||||
error: { cleanupIncomplete: true },
|
||||
});
|
||||
const caller = new AbortController();
|
||||
const reason = { request: "cancel while old descriptor closes" };
|
||||
const pending = resources.run(async () => {
|
||||
pending = resources.run(async () => {
|
||||
calls++;
|
||||
return ok("Must not run");
|
||||
}, caller.signal);
|
||||
await started;
|
||||
await awaitAcknowledgement(started, pending, "descriptor cleanup");
|
||||
caller.abort(reason);
|
||||
releaseCleanup();
|
||||
const result = await pending;
|
||||
@@ -859,10 +862,14 @@ it("observes cancellation after pending descriptor cleanup before admitting repl
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(calls).toBe(0);
|
||||
if (!first.ok) expect(first.error.cleanupIncomplete).toBe(false);
|
||||
} finally {
|
||||
releaseCleanup();
|
||||
await resources.close();
|
||||
}
|
||||
}, [
|
||||
() => caller.abort(),
|
||||
releaseCleanup,
|
||||
async () => {
|
||||
await pending;
|
||||
},
|
||||
() => resources.close(),
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([false, true])(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { access } from "node:fs/promises";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { expect, it } from "vitest";
|
||||
import { withVerifierCleanup } from "../../scripts/lib/verifier-cleanup.mjs";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import { cleanupOwnedProcessGroup } from "./ProcessOwnership.js";
|
||||
@@ -48,22 +50,34 @@ it("waits for an admitted late handshake before closing its process and root", a
|
||||
announceInitialize();
|
||||
await held;
|
||||
});
|
||||
await initializing;
|
||||
await waitFor(() => runtime.supervisor !== undefined);
|
||||
const rootPath = runtime.rootPath;
|
||||
expect(rootPath).toBeDefined();
|
||||
const closing = runtime.close().then(() => {
|
||||
closeSettled = true;
|
||||
});
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(closeSettled).toBe(false);
|
||||
expect(runtime.supervisor).toBeDefined();
|
||||
releaseInitialize();
|
||||
await expect(starting).rejects.toThrow("closing");
|
||||
await closing;
|
||||
expect(runtime.supervisor).toBeUndefined();
|
||||
expect(runtime.rootPath).toBeUndefined();
|
||||
await expect(access(rootPath as string)).rejects.toThrow();
|
||||
let closing: Promise<void> | undefined;
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(initializing, starting, "worker initialization");
|
||||
await waitFor(() => runtime.supervisor !== undefined);
|
||||
const rootPath = runtime.rootPath;
|
||||
expect(rootPath).toBeDefined();
|
||||
closing = runtime.close().then(() => {
|
||||
closeSettled = true;
|
||||
});
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(closeSettled).toBe(false);
|
||||
expect(runtime.supervisor).toBeDefined();
|
||||
releaseInitialize();
|
||||
await expect(starting).rejects.toThrow("closing");
|
||||
await closing;
|
||||
expect(runtime.supervisor).toBeUndefined();
|
||||
expect(runtime.rootPath).toBeUndefined();
|
||||
await expect(access(rootPath as string)).rejects.toThrow();
|
||||
}, [
|
||||
releaseInitialize,
|
||||
async () => {
|
||||
await starting.catch(() => undefined);
|
||||
},
|
||||
async () => {
|
||||
await closing;
|
||||
},
|
||||
() => runtime.close(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves a bound cleanup error after shared pending-owner retry succeeds", async () => {
|
||||
@@ -87,22 +101,31 @@ it("resolves a bound cleanup error after shared pending-owner retry succeeds", a
|
||||
},
|
||||
}),
|
||||
);
|
||||
await runtime.start(startOptions());
|
||||
const rootPath = runtime.rootPath;
|
||||
const error = new ProviderCleanupError("owned-worker-test", [], {
|
||||
reason: "controlled cleanup retry",
|
||||
});
|
||||
await expect(runtime.stop()).resolves.toMatchObject({ status: "incomplete" });
|
||||
runtime.bindCleanupFailure(error);
|
||||
expect(error.cleanupIncomplete).toBe(true);
|
||||
expect(error.cleanupResources).toContain(rootPath);
|
||||
await withVerifierCleanup(async () => {
|
||||
await runtime.start(startOptions());
|
||||
const rootPath = runtime.rootPath;
|
||||
const error = new ProviderCleanupError("owned-worker-test", [], {
|
||||
reason: "controlled cleanup retry",
|
||||
});
|
||||
await expect(runtime.stop()).resolves.toMatchObject({
|
||||
status: "incomplete",
|
||||
});
|
||||
runtime.bindCleanupFailure(error);
|
||||
expect(error.cleanupIncomplete).toBe(true);
|
||||
expect(error.cleanupResources).toContain(rootPath);
|
||||
|
||||
await OwnedWorkerRuntime.retryPending();
|
||||
await OwnedWorkerRuntime.retryPending();
|
||||
|
||||
expect(error.cleanupIncomplete).toBe(false);
|
||||
expect(error.cleanupResources).toEqual([]);
|
||||
expect(runtime.rootPath).toBeUndefined();
|
||||
await expect(access(rootPath as string)).rejects.toThrow();
|
||||
expect(error.cleanupIncomplete).toBe(false);
|
||||
expect(error.cleanupResources).toEqual([]);
|
||||
expect(runtime.rootPath).toBeUndefined();
|
||||
await expect(access(rootPath as string)).rejects.toThrow();
|
||||
}, [
|
||||
() => {
|
||||
failCleanup = false;
|
||||
},
|
||||
() => runtime.close(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("observes request-stream errors and resets them for a fresh child", async () => {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { withVerifierCleanup } from "../../scripts/lib/verifier-cleanup.mjs";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import {
|
||||
cleanupOwnedProcessGroup,
|
||||
type ProcessOwnershipHost,
|
||||
@@ -287,14 +289,22 @@ describe("multi-group ownership observation cancellation", () => {
|
||||
return outcome;
|
||||
});
|
||||
let settledBeforeRelease = false;
|
||||
try {
|
||||
await secondListStarted;
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(
|
||||
secondListStarted,
|
||||
observedOutcome,
|
||||
"liveness recheck",
|
||||
);
|
||||
controller.abort(reason);
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
settledBeforeRelease = settled;
|
||||
} finally {
|
||||
releaseSecondList?.();
|
||||
}
|
||||
}, [
|
||||
() => controller.abort(reason),
|
||||
() => releaseSecondList?.(),
|
||||
async () => {
|
||||
await observedOutcome;
|
||||
},
|
||||
]);
|
||||
|
||||
const outcome = await observedOutcome;
|
||||
expect(settledBeforeRelease).toBe(true);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { withVerifierCleanup } from "../../scripts/lib/verifier-cleanup.mjs";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import {
|
||||
ProviderProcessSupervisor,
|
||||
type ProcessDiagnosticBudget,
|
||||
@@ -29,19 +31,33 @@ it("retains final stderr between verified exit and output close during stop", as
|
||||
},
|
||||
});
|
||||
const stopped = owner.stop();
|
||||
expect(owner.stop()).toBe(stopped);
|
||||
await started;
|
||||
// Let cleanup observe exit before delivering the separately acknowledged output.
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
producer.stderr.end("final launcher diagnostic");
|
||||
producer.stdout.end();
|
||||
producer.emit("close", 2, null);
|
||||
await expect(stopped).resolves.toEqual({ status: "verified-cleanup" });
|
||||
expect(owner.snapshot()).toMatchObject({
|
||||
stderr: { text: "final launcher diagnostic" },
|
||||
exitCode: 2,
|
||||
});
|
||||
expect(await owner.waitForOutputClose(0)).toBe(true);
|
||||
await withVerifierCleanup(async () => {
|
||||
expect(owner.stop()).toBe(stopped);
|
||||
await awaitAcknowledgement(started, stopped, "process cleanup");
|
||||
// Let cleanup observe exit before delivering the separately acknowledged output.
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
producer.stderr.end("final launcher diagnostic");
|
||||
producer.stdout.end();
|
||||
producer.emit("close", 2, null);
|
||||
await expect(stopped).resolves.toEqual({ status: "verified-cleanup" });
|
||||
expect(owner.snapshot()).toMatchObject({
|
||||
stderr: { text: "final launcher diagnostic" },
|
||||
exitCode: 2,
|
||||
});
|
||||
expect(await owner.waitForOutputClose(0)).toBe(true);
|
||||
}, [
|
||||
() => {
|
||||
producer.stderr.end();
|
||||
producer.stdout.end();
|
||||
if (!owner.observedOutputClose()) producer.emit("close", 2, null);
|
||||
},
|
||||
async () => {
|
||||
await stopped;
|
||||
},
|
||||
() => owner.dispose(),
|
||||
() => producer.stderr.destroy(),
|
||||
() => producer.stdout.destroy(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("bounds late output retention and drain without changing verified cleanup", async () => {
|
||||
|
||||
@@ -100,24 +100,6 @@ it.each([
|
||||
nodeOptions: '"--max-old-space-size=256',
|
||||
heap: effective(80, true),
|
||||
},
|
||||
{
|
||||
name: "leaves a percentage flag unestablished instead of the 128 MiB request",
|
||||
execArgv: ["--max-old-space-size-percentage=50"],
|
||||
nodeOptions: undefined,
|
||||
heap: { kind: "unknown" } as const,
|
||||
},
|
||||
{
|
||||
name: "lets a percentage flag win over a later absolute flag",
|
||||
execArgv: ["--max-old-space-size-percentage=50", "--max-old-space-size=64"],
|
||||
nodeOptions: undefined,
|
||||
heap: { kind: "unknown" } as const,
|
||||
},
|
||||
{
|
||||
name: "lets a percentage flag win over an earlier absolute flag",
|
||||
execArgv: ["--max-old-space-size=64", "--max-old-space-size-percentage=50"],
|
||||
nodeOptions: undefined,
|
||||
heap: { kind: "unknown" } as const,
|
||||
},
|
||||
{
|
||||
name: "lets a NODE_OPTIONS percentage win over an execArgv absolute flag",
|
||||
execArgv: ["--max-old-space-size=64"],
|
||||
@@ -164,36 +146,12 @@ it.each([
|
||||
nodeOptions: undefined,
|
||||
heap: { kind: "unknown" } as const,
|
||||
},
|
||||
{
|
||||
name: "leaves an env-file launch unestablished when NODE_OPTIONS was absent",
|
||||
execArgv: ["--env-file=rea.env"],
|
||||
nodeOptions: undefined,
|
||||
heap: { kind: "unknown" } as const,
|
||||
},
|
||||
{
|
||||
name: "leaves an env-file-if-exists launch unestablished when NODE_OPTIONS was absent",
|
||||
execArgv: ["--env-file-if-exists=.env"],
|
||||
nodeOptions: undefined,
|
||||
heap: { kind: "unknown" } as const,
|
||||
},
|
||||
{
|
||||
name: "does not let an absolute flag hide NODE_OPTIONS supplied by an env file",
|
||||
execArgv: ["--env-file=rea.env", "--max-old-space-size=64"],
|
||||
nodeOptions: undefined,
|
||||
heap: { kind: "unknown" } as const,
|
||||
},
|
||||
{
|
||||
name: "recognizes an env-file flag split from its path",
|
||||
execArgv: ["--env-file", "rea.env"],
|
||||
nodeOptions: undefined,
|
||||
heap: { kind: "unknown" } as const,
|
||||
},
|
||||
{
|
||||
name: "keeps an inherited NODE_OPTIONS value when an env file cannot override it",
|
||||
execArgv: ["--env-file=rea.env"],
|
||||
nodeOptions: "--max-old-space-size=64",
|
||||
heap: effective(64, true),
|
||||
},
|
||||
{
|
||||
name: "lets a later absolute flag win when an inherited NODE_OPTIONS blocks the env file",
|
||||
execArgv: ["--max-old-space-size=32", "--env-file-if-exists=rea.env"],
|
||||
@@ -206,33 +164,6 @@ it.each([
|
||||
nodeOptions: "",
|
||||
heap: effective(128, false),
|
||||
},
|
||||
{
|
||||
name: "leaves a config-file percentage source unestablished",
|
||||
execArgv: ["--experimental-config-file=node.config.json"],
|
||||
nodeOptions: undefined,
|
||||
heap: { kind: "unknown" } as const,
|
||||
},
|
||||
{
|
||||
name: "leaves a default config-file launch unestablished",
|
||||
execArgv: ["--experimental-default-config-file"],
|
||||
nodeOptions: undefined,
|
||||
heap: { kind: "unknown" } as const,
|
||||
},
|
||||
{
|
||||
name: "recognizes a config-file flag split from its path",
|
||||
execArgv: ["--experimental-config-file", "node.config.json"],
|
||||
nodeOptions: undefined,
|
||||
heap: { kind: "unknown" } as const,
|
||||
},
|
||||
{
|
||||
name: "does not let an absolute flag hide a config-file heap option",
|
||||
execArgv: [
|
||||
"--experimental-config-file=node.config.json",
|
||||
"--max-old-space-size=64",
|
||||
],
|
||||
nodeOptions: undefined,
|
||||
heap: { kind: "unknown" } as const,
|
||||
},
|
||||
{
|
||||
name: "does not let a recovered NODE_OPTIONS hide a config-file heap option",
|
||||
execArgv: ["--experimental-default-config-file"],
|
||||
@@ -245,30 +176,12 @@ it.each([
|
||||
nodeOptions: "--experimental-config-file=node.config.json",
|
||||
heap: { kind: "unknown" } as const,
|
||||
},
|
||||
{
|
||||
name: "leaves a positive --max-heap-size unestablished instead of the 128 MiB request",
|
||||
execArgv: ["--max-heap-size=64"],
|
||||
nodeOptions: undefined,
|
||||
heap: { kind: "unknown" } as const,
|
||||
},
|
||||
{
|
||||
name: "recognizes an underscore --max-heap-size",
|
||||
execArgv: ["--max_heap_size=+064"],
|
||||
nodeOptions: undefined,
|
||||
heap: { kind: "unknown" } as const,
|
||||
},
|
||||
{
|
||||
name: "leaves a NODE_OPTIONS --max-heap-size unestablished",
|
||||
execArgv: [],
|
||||
nodeOptions: "--max-heap-size=64",
|
||||
heap: { kind: "unknown" } as const,
|
||||
},
|
||||
{
|
||||
name: "does not let an old-space flag hide a positive --max-heap-size",
|
||||
execArgv: ["--max-heap-size=64", "--max-old-space-size=256"],
|
||||
nodeOptions: undefined,
|
||||
heap: { kind: "unknown" } as const,
|
||||
},
|
||||
{
|
||||
name: "does not let a later --max-heap-size hide an old-space flag",
|
||||
execArgv: ["--max-old-space-size=64"],
|
||||
|
||||
@@ -173,7 +173,7 @@ it.skipIf(process.platform === "win32")(
|
||||
expect(
|
||||
snapshot.files.find(({ path }) => path === "root_0:readable.txt")
|
||||
?.sha256,
|
||||
).not.toBeNull();
|
||||
).toEqual(expect.any(String));
|
||||
expect(
|
||||
snapshot.files.find(({ path }) => path === "root_0:unreadable.txt"),
|
||||
).toMatchObject({ type: "file", size: 17, sha256: null });
|
||||
|
||||
@@ -529,15 +529,16 @@ it("cancels the bounded-wait timer once deliveries settle after an exit", async
|
||||
|
||||
it("keeps a non-empty reason when a signal delivery throws without a message", async () => {
|
||||
const fake = fakeTerminal();
|
||||
let signalStarted = false;
|
||||
const throwing = {
|
||||
...fake,
|
||||
signalTarget: async (): Promise<never> => {
|
||||
signalStarted = true;
|
||||
throw new Error();
|
||||
},
|
||||
};
|
||||
const pending = start(throwing, { timeout_ms: 100, finalization_ms: 5_000 });
|
||||
await waitFor(() => throwing.signals.length >= 0);
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
await waitFor(() => signalStarted);
|
||||
throwing.deliverExit({ exitCode: 0, signal: 0 });
|
||||
|
||||
const exit = observed(await pending);
|
||||
|
||||
@@ -1095,68 +1095,6 @@ it("rejects code keys and members whose consts are unhashable", () => {
|
||||
expect(hashable.coverage.status).toBe("complete");
|
||||
});
|
||||
|
||||
it("uses the code equality fields for each CPython layout", () => {
|
||||
const codeCollections = (
|
||||
layout: "py38" | "py311",
|
||||
different: boolean,
|
||||
): [Buffer, Buffer] => {
|
||||
const lineA = marshalCode(layout, {
|
||||
name: "f",
|
||||
filename: "fixture.py",
|
||||
firstlineno: 1,
|
||||
linetable: Buffer.from([2, 1]),
|
||||
});
|
||||
const lineB = marshalCode(layout, {
|
||||
name: "f",
|
||||
filename: "fixture.py",
|
||||
firstlineno: 1,
|
||||
linetable: Buffer.from([2, different ? 2 : 1]),
|
||||
});
|
||||
const frozenSet = Buffer.concat([
|
||||
Buffer.from([0x3e]),
|
||||
u32(2),
|
||||
lineA,
|
||||
lineB,
|
||||
]);
|
||||
const dict = Buffer.concat([
|
||||
Buffer.from([0x7b]),
|
||||
lineA,
|
||||
Buffer.from([0x4e]),
|
||||
lineB,
|
||||
Buffer.from([0x54, 0x30]),
|
||||
]);
|
||||
return [frozenSet, dict];
|
||||
};
|
||||
for (const [magic, layout] of [
|
||||
[3413, "py38"],
|
||||
[3439, "py38"],
|
||||
] as const) {
|
||||
const [frozenSet, dict] = codeCollections(layout, true);
|
||||
const report = parse(
|
||||
Buffer.concat([
|
||||
pythonPycHeader(magic),
|
||||
marshalCode(layout, { consts: [frozenSet, dict] }),
|
||||
]),
|
||||
);
|
||||
expect(root(report).consts).toEqual([
|
||||
{ kind: "frozenset", length: 1 },
|
||||
{ kind: "dict", length: 1 },
|
||||
]);
|
||||
}
|
||||
|
||||
const [frozenSet, dict] = codeCollections("py311", true);
|
||||
const py312 = parse(
|
||||
Buffer.concat([
|
||||
pythonPycHeader(3531),
|
||||
marshalCode("py311", { consts: [frozenSet, dict] }),
|
||||
]),
|
||||
);
|
||||
expect(root(py312).consts).toEqual([
|
||||
{ kind: "frozenset", length: 2 },
|
||||
{ kind: "dict", length: 2 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the first dictionary key when a later key compares equal", () => {
|
||||
const key = (filename: string): Buffer =>
|
||||
marshalCode("py311", {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import type { Client } from "@modelcontextprotocol/client";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import type { AnySchema } from "ajv";
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { expect, it } from "vitest";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -19,9 +18,10 @@ import { SessionProviderRouter } from "../application/binary/SessionProviderRout
|
||||
import { toolContract } from "../contracts/toolContracts.js";
|
||||
import { createEvidence } from "../domain/evidence.js";
|
||||
import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
import { createServer } from "./createServer.js";
|
||||
import { createArtifactBinarySession } from "../composition/binary.js";
|
||||
import { build } from "plist";
|
||||
import { createApplicationMcpHarness } from "../../tests/fixtures/applicationMcpHarness.js";
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
|
||||
const identity: ProviderIdentity = {
|
||||
id: "bundle-resource-mcp-fixture",
|
||||
@@ -29,14 +29,8 @@ const identity: ProviderIdentity = {
|
||||
version: "1",
|
||||
};
|
||||
|
||||
const resources: Array<{ close(): Promise<unknown> }> = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(resources.splice(0).map((resource) => resource.close()));
|
||||
});
|
||||
|
||||
it("routes bundle resource MCP calls with null executable identity and retained Evidence", async () => {
|
||||
const parent = await mkdtemp(join(tmpdir(), "rea-bundle-resource-mcp-"));
|
||||
const parent = await createTestTempDirectory("rea-bundle-resource-mcp-");
|
||||
const bundle = join(parent, "Fixture.app");
|
||||
await mkdir(bundle);
|
||||
const plistPath = join(bundle, "Info.plist");
|
||||
@@ -82,22 +76,11 @@ it("routes bundle resource MCP calls with null executable identity and retained
|
||||
new InvestigationRecords(),
|
||||
{ createClient: () => ({ client: resourceClient, capabilities: [] }) },
|
||||
);
|
||||
const server = createServer({ kind: "session", session });
|
||||
const client = new Client({ name: "bundle-resource-test", version: "1" });
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
resources.push({
|
||||
close: async () => {
|
||||
await client.close();
|
||||
await server.close();
|
||||
await session.close();
|
||||
await rm(parent, { recursive: true, force: true });
|
||||
},
|
||||
const { client } = await createApplicationMcpHarness({
|
||||
session,
|
||||
clientIdentity: { name: "bundle-resource-test", version: "1" },
|
||||
});
|
||||
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
await expectResourceSchemaBranches(client);
|
||||
|
||||
const unbound = await client.callTool({
|
||||
@@ -192,8 +175,8 @@ it("routes bundle resource MCP calls with null executable identity and retained
|
||||
});
|
||||
|
||||
it("exposes the production resource session through the MCP SDK for a keyless bundle", async () => {
|
||||
const parent = await mkdtemp(
|
||||
join(tmpdir(), "rea-bundle-resource-production-mcp-"),
|
||||
const parent = await createTestTempDirectory(
|
||||
"rea-bundle-resource-production-mcp-",
|
||||
);
|
||||
const bundle = join(parent, "Fixture.app");
|
||||
const resourceDirectory = join(bundle, "Resources");
|
||||
@@ -210,23 +193,10 @@ it("exposes the production resource session through the MCP SDK for a keyless bu
|
||||
);
|
||||
|
||||
const session = await createArtifactBinarySession({}, process.platform);
|
||||
const server = createServer({ kind: "session", session });
|
||||
const client = new Client({
|
||||
name: "production-bundle-resource-test",
|
||||
version: "1",
|
||||
const { client } = await createApplicationMcpHarness({
|
||||
session,
|
||||
clientIdentity: { name: "production-bundle-resource-test", version: "1" },
|
||||
});
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
resources.push({
|
||||
close: async () => {
|
||||
await client.close();
|
||||
await server.close();
|
||||
await session.close();
|
||||
await rm(parent, { recursive: true, force: true });
|
||||
},
|
||||
});
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
const opened = await client.callTool({
|
||||
name: "open_bundle_resources",
|
||||
|
||||
@@ -118,9 +118,18 @@ describe("replay value fidelity over MCP", () => {
|
||||
name: "get_users_by_id",
|
||||
arguments: { id: "3" },
|
||||
});
|
||||
expect(JSON.stringify(ownKeys.structuredContent)).toContain(
|
||||
'"__proto__":{"retained":true}',
|
||||
);
|
||||
const content = ownKeys.structuredContent;
|
||||
if (
|
||||
typeof content !== "object" ||
|
||||
content === null ||
|
||||
!("body" in content)
|
||||
)
|
||||
throw new Error("Expected a structured replay body");
|
||||
const body = content.body;
|
||||
expect(body).toEqual(JSON.parse(proto));
|
||||
if (typeof body !== "object" || body === null)
|
||||
throw new Error("Expected an object replay body");
|
||||
expect(Object.hasOwn(body, "__proto__")).toBe(true);
|
||||
} finally {
|
||||
await transport.terminateSession();
|
||||
await client.close();
|
||||
|
||||
@@ -431,9 +431,28 @@ describe("state data plane intake", () => {
|
||||
]);
|
||||
expect(batch.json).toMatchObject([
|
||||
{ id: 1, result: {} },
|
||||
{ id: 2, result: { parts: [{ kind: "data" }] } },
|
||||
{
|
||||
id: 2,
|
||||
result: {
|
||||
parts: [
|
||||
{
|
||||
kind: "data",
|
||||
data: {
|
||||
rea_state: {
|
||||
entities: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: "/api/users",
|
||||
id: "2",
|
||||
fields: expect.objectContaining({ login: "renamed" }),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(JSON.stringify(batch.json)).toContain('"login":"renamed"');
|
||||
const notified = await plane.post({ jsonrpc: "2.0", method: "data/reset" });
|
||||
expect(notified.status).toBe(202);
|
||||
expect(loginOf(plane.store)).toBe("probe");
|
||||
@@ -542,10 +561,29 @@ describe("state data plane timing and order", () => {
|
||||
const replies = (await batch).json;
|
||||
expect(replies).toMatchObject([
|
||||
{ id: 1, result: {} },
|
||||
{ id: 2, result: { parts: [{ kind: "data" }] } },
|
||||
{
|
||||
id: 2,
|
||||
result: {
|
||||
parts: [
|
||||
{
|
||||
kind: "data",
|
||||
data: {
|
||||
rea_state: {
|
||||
entities: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: "/api/users",
|
||||
id: "2",
|
||||
fields: expect.objectContaining({ login: "renamed" }),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
// The get arrived with the add, before the reset, so it reports the loaded state.
|
||||
expect(JSON.stringify(replies)).toContain('"login":"renamed"');
|
||||
await expect(reset).resolves.toMatchObject({ result: {} });
|
||||
expect(loginOf(plane.store)).toBe("probe");
|
||||
});
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
expectForeignHostRefused,
|
||||
rawHttpRequest,
|
||||
} from "../../../tests/support/http/rawRequest.js";
|
||||
import { awaitAcknowledgement } from "../../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import { closeVerifierResources } from "../../../scripts/lib/verifier-cleanup.mjs";
|
||||
import {
|
||||
hostHeaderAllowlist,
|
||||
McpSessionSlot,
|
||||
@@ -1104,21 +1106,25 @@ it.each(["replacement", "DELETE"])(
|
||||
const first = await connect(served.url);
|
||||
let next: Awaited<ReturnType<typeof connect>> | undefined;
|
||||
let replacement: ReturnType<typeof connect> | undefined;
|
||||
let deletion:
|
||||
| ReturnType<typeof first.transport.terminateSession>
|
||||
| undefined;
|
||||
const request = first.client
|
||||
.callTool({ name: "slow", arguments: {} })
|
||||
.catch(() => undefined);
|
||||
try {
|
||||
void first.client
|
||||
.callTool({ name: "slow", arguments: {} })
|
||||
.catch(() => undefined);
|
||||
await started;
|
||||
const deletion =
|
||||
await awaitAcknowledgement(started, request, "tool start");
|
||||
deletion =
|
||||
ending === "DELETE" ? first.transport.terminateSession() : undefined;
|
||||
if (deletion !== undefined) await aborted;
|
||||
if (deletion !== undefined)
|
||||
await awaitAcknowledgement(aborted, deletion, "tool cancellation");
|
||||
let connected = false;
|
||||
replacement = connect(served.url).then((value) => {
|
||||
connected = true;
|
||||
next = value;
|
||||
return value;
|
||||
});
|
||||
await aborted;
|
||||
await awaitAcknowledgement(aborted, replacement, "tool cancellation");
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(connections).toBe(1);
|
||||
expect(connected).toBe(false);
|
||||
@@ -1132,9 +1138,15 @@ it.each(["replacement", "DELETE"])(
|
||||
]);
|
||||
} finally {
|
||||
release();
|
||||
await replacement?.catch(() => undefined);
|
||||
await first.client.close();
|
||||
await next?.client.close();
|
||||
await closeVerifierResources([
|
||||
() => first.client.close(),
|
||||
async () => {
|
||||
await Promise.allSettled([request, deletion, replacement]);
|
||||
},
|
||||
async () => {
|
||||
await next?.client.close();
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1162,7 +1174,11 @@ it.each(["ready", "failure"] as const)(
|
||||
});
|
||||
const settled = starting.catch(() => undefined);
|
||||
try {
|
||||
await factoryStarted.promise;
|
||||
await awaitAcknowledgement(
|
||||
factoryStarted.promise,
|
||||
starting,
|
||||
"factory start",
|
||||
);
|
||||
expect((await fetch(new URL("/pending", url))).status).toBe(503);
|
||||
expect(handled).not.toHaveBeenCalled();
|
||||
factoryGate.release();
|
||||
@@ -1222,14 +1238,19 @@ it.each(["open stream", "late response"])(
|
||||
const response = fetch(new URL("/stream", served.url)).catch(
|
||||
() => undefined,
|
||||
);
|
||||
await entered.promise;
|
||||
if (timing === "open stream") await response;
|
||||
let closed = false;
|
||||
const closing = served.prepareShutdown().then(() => {
|
||||
closed = true;
|
||||
});
|
||||
let closing: Promise<void> | undefined;
|
||||
try {
|
||||
await cancelling.promise;
|
||||
await awaitAcknowledgement(entered.promise, response, "route start");
|
||||
if (timing === "open stream") await response;
|
||||
let closed = false;
|
||||
closing = served.prepareShutdown().then(() => {
|
||||
closed = true;
|
||||
});
|
||||
await awaitAcknowledgement(
|
||||
cancelling.promise,
|
||||
closing,
|
||||
"body cancellation",
|
||||
);
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(closed).toBe(false);
|
||||
expect(shutdownHooks()).toBe(0);
|
||||
@@ -1238,8 +1259,14 @@ it.each(["open stream", "late response"])(
|
||||
expect(shutdownHooks()).toBe(1);
|
||||
} finally {
|
||||
cleanup.release();
|
||||
await closing;
|
||||
await (await response)?.body?.cancel().catch(() => undefined);
|
||||
await closeVerifierResources([
|
||||
async () => {
|
||||
await (closing ?? served.close());
|
||||
},
|
||||
async () => {
|
||||
await (await response)?.body?.cancel().catch(() => undefined);
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1586,13 +1613,14 @@ it.each(["prepareShutdown", "close"] as const)(
|
||||
const response = fetch(new URL("/pending", served.url)).catch(
|
||||
() => undefined,
|
||||
);
|
||||
await started;
|
||||
let closed = false;
|
||||
const closing = served[method]().then(() => {
|
||||
closed = true;
|
||||
});
|
||||
let closing: Promise<void> | undefined;
|
||||
try {
|
||||
await cleanupStarted;
|
||||
await awaitAcknowledgement(started, response, "route start");
|
||||
let closed = false;
|
||||
closing = served[method]().then(() => {
|
||||
closed = true;
|
||||
});
|
||||
await awaitAcknowledgement(cleanupStarted, closing, "route finalizer");
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(closed).toBe(false);
|
||||
expect(shutdownHooks()).toBe(0);
|
||||
@@ -1601,8 +1629,14 @@ it.each(["prepareShutdown", "close"] as const)(
|
||||
expect(shutdownHooks()).toBe(1);
|
||||
} finally {
|
||||
release();
|
||||
await closing;
|
||||
await response;
|
||||
await closeVerifierResources([
|
||||
async () => {
|
||||
await (closing ?? served.close());
|
||||
},
|
||||
async () => {
|
||||
await response;
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1909,7 +1943,7 @@ it.each(["replacement", "DELETE", "shutdown"])(
|
||||
);
|
||||
try {
|
||||
// With no stream prefetch, this proves the SDK is waiting on the old body.
|
||||
await readStarted;
|
||||
await awaitAcknowledgement(readStarted, pending, "request body read");
|
||||
if (ending === "shutdown") await slot.close();
|
||||
else {
|
||||
if (ending === "DELETE") {
|
||||
@@ -2034,12 +2068,14 @@ it.each(["after timeout", "during drain"])(
|
||||
},
|
||||
});
|
||||
const first = await connect(served.url);
|
||||
const request = first.client
|
||||
.callTool({ name: "slow", arguments: {} })
|
||||
.catch(() => undefined);
|
||||
let replacement: Promise<Response | undefined> | undefined;
|
||||
let pendingClose: Promise<void> | undefined;
|
||||
try {
|
||||
void first.client
|
||||
.callTool({ name: "slow", arguments: {} })
|
||||
.catch(() => undefined);
|
||||
await started;
|
||||
const replacement = fetch(served.url, {
|
||||
await awaitAcknowledgement(started, request, "tool start");
|
||||
replacement = fetch(served.url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
@@ -2051,8 +2087,8 @@ it.each(["after timeout", "during drain"])(
|
||||
if (shutdownTiming === "during drain") return undefined;
|
||||
throw cause;
|
||||
});
|
||||
await drainStarted;
|
||||
const pendingClose =
|
||||
await awaitAcknowledgement(drainStarted, replacement, "session drain");
|
||||
pendingClose =
|
||||
shutdownTiming === "during drain" ? served.close() : undefined;
|
||||
const refused = await replacement;
|
||||
if (shutdownTiming === "after timeout") expect(refused?.status).toBe(503);
|
||||
@@ -2068,7 +2104,15 @@ it.each(["after timeout", "during drain"])(
|
||||
expect(shutdowns).toBe(1);
|
||||
} finally {
|
||||
release();
|
||||
await first.client.close();
|
||||
await closeVerifierResources([
|
||||
() => first.client.close(),
|
||||
async () => {
|
||||
await Promise.allSettled([request, replacement]);
|
||||
},
|
||||
async () => {
|
||||
await pendingClose;
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
);
|
||||
Loaded 100 of 342 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user