diff --git a/bridge/hopper_bridge.py b/bridge/hopper_bridge.py index 8973c3942b..d80ef3a2f8 100644 --- a/bridge/hopper_bridge.py +++ b/bridge/hopper_bridge.py @@ -154,13 +154,6 @@ class HopperApiFacade: ) -def _configure_hopper_api(document_provider): - """Inject Hopper's document provider or a standalone test facade.""" - global _hopper_api, _rea_session_document - _hopper_api = HopperApiFacade(document_provider) - _rea_session_document = None - - def _api(): """Resolve Hopper globals lazily so importing this module needs no Hopper.""" global _hopper_api diff --git a/docs/reference/mcp-contracts.md b/docs/reference/mcp-contracts.md index 871672d2c3..221991344b 100644 --- a/docs/reference/mcp-contracts.md +++ b/docs/reference/mcp-contracts.md @@ -222,7 +222,7 @@ cannot answer, such as an address without one exact file offset, omits this hint; follow its `remediation.action`. `recovery.kind=inspect_timeout_state` names the operation and the REA server/provider deadline. Inspect partial Evidence and reported cleanup state, then follow the returned remediation. Change a -request field or REA_* setting only when the returned remediation explicitly +request field or REA\_\* setting only when the returned remediation explicitly recommends that change. Restart REA when that text says the value is read at startup. A mention that a control does not raise the deadline is not a recommendation to change it. When it names no setting, reduce supported work; @@ -1911,13 +1911,10 @@ support for omitted historical values. known and differ. If only one value is known, storage comparison is unknown; when both are omitted, storage is not compared. - - -`compare_artifacts` also compares the declarations in the `format_metadata` -of UnityFS members (`unityfs-entry`), PyInstaller entries +`compare_artifacts` also compares the declarations in the `format_metadata` of UnityFS members (`unityfs-entry`), PyInstaller entries (`pyinstaller-entry`), Node SEA members (`node-sea-entry`), ESP image segments (`esp-image-segment`), DfuSe elements (`dfuse-element`), -ESP flash partitions (`esp-flash-partition`) and FIT images (`fit-image`); +ESP flash partitions (`esp-flash-partition`) and FIT images (`fit-image`); each format's guide names the compared fields. A changed declaration is a metadata change, including when content is unavailable. A declaration that only one side records, or that the other side records as another kind, is @@ -1928,8 +1925,6 @@ container or from caller options that identify the target, or repeat occurrence fields, except Godot PCK source paths, which [Godot PCK archives](../godot-pck.md) explains. - - Every occurrence records `entry_kind`, `executable` and `encrypted`. A different entry kind or `encrypted` value is a `metadata` difference, and a different `executable` value is an `executable` difference. Occurrence and node diff --git a/docs/specs/desktop-app-environments/file-readers/plan.md b/docs/specs/desktop-app-environments/file-readers/plan.md index 2ab4b82e3b..7967c322d3 100644 --- a/docs/specs/desktop-app-environments/file-readers/plan.md +++ b/docs/specs/desktop-app-environments/file-readers/plan.md @@ -10,8 +10,8 @@ 4. Verifier: `appTaskPython.ts` renders `READER_RUNNER`, `READERS`, `_reader_views` and `evaluate_with_readers` only for a task with readers, so a task without renders as before. -5. Application: `readTaskReaderViews` and `readerViewsOfBytes` in - `DesktopTaskReaders.ts`; `runReaderHere` in `src/desktop/TaskReaderProcess.ts`; +5. Application: `readerViewsOfBytes` in `DesktopTaskReaders.ts`; + `runReaderHere` in `src/desktop/TaskReaderProcess.ts`; the witness runs readers through `AgentEnvComputer.exec`; the export runs them on `shippedSeedBytes` and the end's retained content. 6. Docs: the tasks reference, the witness page, the build skill's recipes for diff --git a/scripts/lib/real-ghidra-boundaries.mjs b/scripts/lib/real-ghidra-boundaries.mjs index ebdb475d0a..04263bfae8 100644 --- a/scripts/lib/real-ghidra-boundaries.mjs +++ b/scripts/lib/real-ghidra-boundaries.mjs @@ -10,7 +10,6 @@ import { access, readFile, readdir, stat } from "node:fs/promises"; import { dirname, join } from "node:path"; import { setTimeout as delay } from "node:timers/promises"; import { promisify } from "node:util"; -import { GHIDRA_ANNOTATIONS_RETAINED } from "../../dist/ghidra/GhidraProviderClient.js"; import Ajv from "ajv/dist/2020.js"; import addFormats from "ajv-formats"; import { OFFICIAL_TOOL_CONTRACTS } from "../../dist/contracts/officialToolContracts.js"; @@ -571,7 +570,11 @@ export async function verifyGhidraBoundaries( // The producer reason describes the failure; continuing-session guidance // is projected separately and the following live calls verify it. assert.match(regexError.details.reason, /stack while matching pattern/u); - assert.ok(regexError.message.includes(GHIDRA_ANNOTATIONS_RETAINED)); + assert.ok( + regexError.message.includes( + "The active analysis session and annotations remain available.", + ), + ); rejectedCalls++; const nestedPattern = "(".repeat(12000) + "a" + ")".repeat(12000); const compileFailure = await client.callTool( diff --git a/src/android/AndroidTargetSnapshot.ts b/src/android/AndroidTargetSnapshot.ts index bd0af19aa9..18c883d193 100644 --- a/src/android/AndroidTargetSnapshot.ts +++ b/src/android/AndroidTargetSnapshot.ts @@ -58,7 +58,7 @@ export const androidSnapshotCopy = ( : undefined; /** Adopt a snapshot descriptor whose close rejected, keyed by its exact path. */ -export type AndroidSnapshotCloseRetention = ( +type AndroidSnapshotCloseRetention = ( path: string, owner: OwnedFileHandle, ) => void; @@ -69,8 +69,8 @@ export const snapshotAndroidTarget = async ( sha256: string, root: string, operation: AndroidOperation, - signal?: AbortSignal, - retainFailedClose?: AndroidSnapshotCloseRetention, + signal: AbortSignal | undefined, + retainFailedClose: AndroidSnapshotCloseRetention, ): Promise => { const snapshot = join(root, "target.apk"); try { @@ -105,8 +105,8 @@ export const snapshotAndroidEngine = async ( sha256: string, root: string, operation: AndroidOperation, - signal?: AbortSignal, - retainFailedClose?: AndroidSnapshotCloseRetention, + signal: AbortSignal | undefined, + retainFailedClose: AndroidSnapshotCloseRetention, ): Promise<{ path: string; sha256: string }> => { const path = join(root, "engine.jar"); try { @@ -159,17 +159,14 @@ const copyAndroidSnapshot = async ( snapshotPath: string, expectedSha256: string, signal: AbortSignal | undefined, - retainFailedClose: AndroidSnapshotCloseRetention | undefined, + retainFailedClose: AndroidSnapshotCloseRetention, ): Promise => { assertRegularFileActive(signal); let removeSnapshot = false; - const retain = (path: string) => - retainFailedClose === undefined - ? {} - : { - retainFailedClose: (owner: OwnedFileHandle) => - retainFailedClose(path, owner), - }; + const retain = (path: string) => ({ + retainFailedClose: (owner: OwnedFileHandle) => + retainFailedClose(path, owner), + }); try { // Close each descriptor through its owner. A rejected close keeps the // copy outcome and stays retained until that owner is retried; the @@ -287,7 +284,7 @@ const openSnapshotSource = async ( path: string, signal: AbortSignal | undefined, retention: { - readonly retainFailedClose?: (owner: OwnedFileHandle) => void; + readonly retainFailedClose: (owner: OwnedFileHandle) => void; }, ): Promise => { try { @@ -305,10 +302,9 @@ const openSnapshotSource = async ( }; const retainAdmissionOwner = ( - retain: ((owner: OwnedFileHandle) => void) | undefined, + retain: (owner: OwnedFileHandle) => void, owner: OwnedFileHandle, ): RegularFileCleanupFailure["retention"] => { - if (retain === undefined) return { kind: "not_requested" }; try { retain(owner); return { kind: "callback_returned" }; diff --git a/src/application/AnalysisSettings.test.ts b/src/application/AnalysisSettings.test.ts index c360fd36f7..03eb5ef1f7 100644 --- a/src/application/AnalysisSettings.test.ts +++ b/src/application/AnalysisSettings.test.ts @@ -6,7 +6,6 @@ import { resolveWindowsChangeTimeSettle, } from "../config/windowsChangeTimeSettle.js"; import { pwntoolsInputBytes } from "../native/pwntools/PwntoolsInputLimit.js"; -import { resolveBridgeMaxRequestBytes } from "../config/bridgeMaxRequestBytes.js"; import { resolveCaptureDecoderHeap } from "../config/captureDecoderHeap.js"; import { resolveRegexMatchingDeadline } from "../config/regexMatchingDeadline.js"; import { resolveRegexWorkerHeapRequest } from "../config/regexWorkerHeapRequest.js"; @@ -366,7 +365,6 @@ it("reads an accepted override before an invalid launch seed", () => { { revision: 1, overrides: { - "bridge.max_request_bytes": 8192, "capture.decoder_old_generation_mib": 64, "regex.matching_deadline_ms": 250, "regex.worker_heap_request_mib": 128, @@ -375,9 +373,6 @@ it("reads an accepted override before an invalid launch seed", () => { }, }, () => ({ - bridge: resolveBridgeMaxRequestBytes({ - REA_BRIDGE_MAX_REQUEST_BYTES: "nope", - }), capture: resolveCaptureDecoderHeap({ REA_CAPTURE_DECODER_OLD_GENERATION_MIB: "0", }), @@ -393,10 +388,6 @@ it("reads an accepted override before an invalid launch seed", () => { }), }), ); - expect(resolved.bridge).toMatchObject({ - maximumBytes: 8192, - source: "runtime", - }); expect(resolved.capture).toMatchObject({ oldGenerationMiB: 64, source: "runtime", diff --git a/src/application/AppBundleExecutable.test.ts b/src/application/AppBundleExecutable.test.ts index 59885158bc..2e3a016bf1 100644 --- a/src/application/AppBundleExecutable.test.ts +++ b/src/application/AppBundleExecutable.test.ts @@ -86,17 +86,13 @@ describe("app bundle Info.plist magic", () => { const plist = Buffer.from("bplist00"); plist[0] = (plist[0] ?? 0) | 0x80; await writeFile(plistPath, plist); - let decoded = false; - const result = await resolveAppBundleExecutable(bundle, { - readFile: async () => plist, - realpath: async (path) => path, - decodeBinaryPlist: async () => { - decoded = true; - return "Fixture"; + const result = await resolveAppBundleExecutable(bundle); + expect(result).toMatchObject({ + ok: false, + error: { + cause: { name: "XmlPlistTextDecodeError", reason: "invalid-bytes" }, }, }); - expect(decoded).toBe(false); - expect(result.ok).toBe(false); } finally { await rm(root, { recursive: true, force: true }); } diff --git a/src/application/AppBundleExecutable.ts b/src/application/AppBundleExecutable.ts index e8209cc166..815debc13d 100644 --- a/src/application/AppBundleExecutable.ts +++ b/src/application/AppBundleExecutable.ts @@ -91,7 +91,6 @@ export interface ResolvedAppBundleResourceRoot { export interface AppBundleFileSystem { readonly readFile: (path: string) => Promise; readonly realpath: (path: string) => Promise; - readonly decodeBinaryPlist?: (path: string) => Promise; } const defaultFileSystem: AppBundleFileSystem = { readFile, realpath }; @@ -396,15 +395,6 @@ const codeDescription = (cause: unknown): string => { : ""; }; -const isSystemErrorCode = (cause: unknown): boolean => { - const code = errorCode(cause); - return ( - typeof code === "string" && - /^[A-Z][A-Z0-9_]+$/u.test(code) && - !code.startsWith("ERR_") - ); -}; - const isPermissionDenied = (cause: unknown): boolean => errorCode(cause) === "EACCES" || errorCode(cause) === "EPERM"; @@ -461,20 +451,12 @@ const resolveLayoutExecutable = async ( let name: string; try { - name = - hasBinaryPlistSignature(plist) && - fileSystem.decodeBinaryPlist !== undefined - ? await fileSystem.decodeBinaryPlist(plistPath) - : executableName(decodeInfoPlistValue(plist)); + name = executableName(decodeInfoPlistValue(plist)); } catch (cause: unknown) { return err( new BinaryTargetError( plistPath, - isPermissionDenied(cause) - ? `permission denied decoding ${noun} Info.plist ${plistPath}${codeDescription(cause)}` - : isSystemErrorCode(cause) - ? `could not decode ${noun} Info.plist ${plistPath}${codeDescription(cause)}` - : `${noun} Info.plist is malformed, unsupported, or lacks CFBundleExecutable: ${plistPath}${cause instanceof Error ? ` (${cause.message})` : ""}`, + `${noun} Info.plist is malformed, unsupported, or lacks CFBundleExecutable: ${plistPath}${cause instanceof Error ? ` (${cause.message})` : ""}`, { cause, // A bundle without code, such as a resource bundle, declares no diff --git a/src/application/BinaryTargetResolver.ts b/src/application/BinaryTargetResolver.ts index 5c88ac40d4..3267f9635c 100644 --- a/src/application/BinaryTargetResolver.ts +++ b/src/application/BinaryTargetResolver.ts @@ -747,8 +747,8 @@ const throwIfTargetResolutionCancelled = (signal?: AbortSignal): void => { const detectArtifactFormat = async ( path: string, source: StableRegularFileDescriptor, - signal?: AbortSignal, - sourcePath = path, + signal: AbortSignal | undefined, + sourcePath: string, ): Promise< | Exclude< BinaryTarget["format"], diff --git a/src/application/ClientConfigurationFiles.ts b/src/application/ClientConfigurationFiles.ts index d90351632e..a490aa5472 100644 --- a/src/application/ClientConfigurationFiles.ts +++ b/src/application/ClientConfigurationFiles.ts @@ -23,14 +23,14 @@ export interface ClientConfigurationSource extends ClientConfigurationDocument { const invalidEnvironment = ( path: string, error: ConfigurationError, - overrides: Readonly> = {}, - platform: NodeJS.Platform = process.platform, + overrides: Readonly> | undefined, + platform: NodeJS.Platform, ): ClientConfigurationFileError => { const constraints = error.settings .map(({ setting, constraint }) => `${setting}: ${constraint}`) .join("; "); const overrideNames = new Set( - Object.keys(overrides).map((name) => + Object.keys(overrides ?? {}).map((name) => platform === "win32" ? name.toUpperCase() : name, ), ); diff --git a/src/application/ReferenceSourceImportEntries.ts b/src/application/ReferenceSourceImportEntries.ts index ac05eb7e16..85ee9598b6 100644 --- a/src/application/ReferenceSourceImportEntries.ts +++ b/src/application/ReferenceSourceImportEntries.ts @@ -34,7 +34,7 @@ export interface ParsedReferenceSourceEntries { } /** Preserve the observed reader failure and add entry-specific recovery guidance. */ -export const projectReferenceSourceEntryFailure = ( +const projectReferenceSourceEntryFailure = ( entry: Extract, ): string => `${entry.message} ${entryRecoveryGuidance(entry)}`; diff --git a/src/application/ReferenceSourceImportTypes.test.ts b/src/application/ReferenceSourceImportTypes.test.ts index 5b95d2b73b..cfdb6625c2 100644 --- a/src/application/ReferenceSourceImportTypes.test.ts +++ b/src/application/ReferenceSourceImportTypes.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from "vitest"; import { - projectReferenceSourceImportError, referenceSourceImportAnalysisError, type ReferenceSourceImportError, } from "./ReferenceSourceImportTypes.js"; @@ -20,11 +19,13 @@ describe("reference-source import error projection", () => { parse: "execution_failure", } as const; for (const [code, category] of Object.entries(expectedCategories)) { - const projected = projectReferenceSourceImportError({ - tag: "reference-source-import", - code: code as ReferenceSourceImportError["code"], - message: "EIO while reading /selected/source/src/main.ts", - }); + const projected = projectAnalysisError( + referenceSourceImportAnalysisError({ + tag: "reference-source-import", + code: code as ReferenceSourceImportError["code"], + message: "EIO while reading /selected/source/src/main.ts", + }), + ); expect(analysisErrorProjectionSchema.safeParse(projected).success).toBe( true, ); diff --git a/src/application/ReferenceSourceImportTypes.ts b/src/application/ReferenceSourceImportTypes.ts index 6f8d022d00..91b6d0c7ef 100644 --- a/src/application/ReferenceSourceImportTypes.ts +++ b/src/application/ReferenceSourceImportTypes.ts @@ -5,10 +5,6 @@ import { AnalysisOutputError, } from "../domain/analysisErrorCore.js"; import { analysisErrorWithCancellation } from "../domain/analysisErrorCancellation.js"; -import { - projectAnalysisError, - type AnalysisErrorProjection, -} from "../domain/analysisErrorProjection.js"; import type { AnalysisError, AnalysisErrorOptions, @@ -125,12 +121,6 @@ export const referenceSourceImportAnalysisError = ( ); }; -/** Canonical CLI/MCP projection, retaining source facts under diagnostic details. */ -export const projectReferenceSourceImportError = ( - error: ReferenceSourceImportError, -): AnalysisErrorProjection => - projectAnalysisError(referenceSourceImportAnalysisError(error)); - /** Import a caller-selected local source tree as historical reference. */ export interface ReferenceSourceImportOptions { readonly root: string; diff --git a/src/application/analysisView/WebSourceLocationViews.test.ts b/src/application/analysisView/WebSourceLocationViews.test.ts index 8c0c03d465..9278c8786f 100644 --- a/src/application/analysisView/WebSourceLocationViews.test.ts +++ b/src/application/analysisView/WebSourceLocationViews.test.ts @@ -11,7 +11,7 @@ import { inspectAnalysisView, projectRetainedAnalysis, } from "./AnalysisViewService.js"; -import { analysisViewRequestSchema } from "../../domain/analysisView/analysisView.js"; +import { analysisViewRequestSchema } from "../../domain/analysisView/analysisViewRequest.js"; import type { Evidence } from "../../domain/evidence.js"; const trace = async ( diff --git a/src/application/binary/BinarySessionExecution.ts b/src/application/binary/BinarySessionExecution.ts index 5b7ceaa841..bb8c0726af 100644 --- a/src/application/binary/BinarySessionExecution.ts +++ b/src/application/binary/BinarySessionExecution.ts @@ -58,7 +58,7 @@ interface PrepareSessionExecutionInput { } /** Recovery for an operation absent from the selected binary route. */ -export const undeclaredOperationGuidance = ( +const undeclaredOperationGuidance = ( operation: string, entryCommentAvailable = false, ): string => { diff --git a/src/application/binary/SessionRuntimeAvailability.ts b/src/application/binary/SessionRuntimeAvailability.ts index 4b6089a454..38d4e5b463 100644 --- a/src/application/binary/SessionRuntimeAvailability.ts +++ b/src/application/binary/SessionRuntimeAvailability.ts @@ -31,9 +31,7 @@ type RoutedOperation = Pick< >; /** Reason recorded for an operation's own refusal or provider-wide failure. */ -export const runtimeFailureReason = ( - failure: AnalysisError, -): string | undefined => { +const runtimeFailureReason = (failure: AnalysisError): string | undefined => { if (failure._tag === "AnalysisCapabilityUnavailableError") return failure.userMessage ?? failure.message; return PROVIDER_WIDE_FAILURES.has(failure._tag) @@ -42,7 +40,7 @@ export const runtimeFailureReason = ( }; /** Explain unavailability inferred for an operation no request failed for. */ -export const inferredUnavailabilityReason = ( +const inferredUnavailabilityReason = ( providerId: string, failure: Pick, ): string => diff --git a/src/application/desktop/DesktopApplicationExport.test.ts b/src/application/desktop/DesktopApplicationExport.test.ts index ff6a825795..e4811bebdc 100644 --- a/src/application/desktop/DesktopApplicationExport.test.ts +++ b/src/application/desktop/DesktopApplicationExport.test.ts @@ -29,16 +29,7 @@ import type { } from "../../domain/desktop/desktopCapture.js"; import { renderBuildBundle } from "../../domain/desktop/appBuild.js"; import { readDownloadInstall } from "./DesktopExportInstall.js"; -import { - NO_LAUNCH_DOCUMENT, - launchDocumentNote, - seedWording, -} from "../../domain/desktop/appExport.js"; import { exportDesktopApplication } from "./DesktopApplicationExport.js"; -import { - seedLimitation, - trashedFilesLimitation, -} from "./DesktopExportLimitations.js"; import { EDITED_NOTE, HOME, @@ -529,13 +520,9 @@ describe("exportDesktopApplication and the desktop Trash", () => { "Documents/today.txt", ], }); - const trashPaths = [ - ".local/share/Trash/files/today.txt", - ".local/share/Trash/info/today.txt.trashinfo", - ]; - const notice = trashedFilesLimitation(trashPaths); - expect(result.limitations).toContain(notice); - for (const path of trashPaths) expect(notice).toContain(`~/${path}`); + expect(result.limitations).toContain( + "The desktop Trash held 2 file(s) at the seed checkpoint, which are not seeded: ~/.local/share/Trash/files/today.txt, ~/.local/share/Trash/info/today.txt.trashinfo. Restore one the task needs and capture again.", + ); }); }); @@ -1936,7 +1923,9 @@ const exportSeeded = async ( /** The limitations on what the launch opens, found by the lead their formatter gives them. */ const launchLimitations = (limitations: readonly string[]) => - limitations.filter((line) => line.includes(NO_LAUNCH_DOCUMENT)); + limitations.filter((line) => + line.includes("The app opens no document at launch"), + ); /** * The seed-then-end capture of an app whose titles name no file, given the @@ -2037,9 +2026,7 @@ describe("exportDesktopApplication launch document", () => { `command=["sh", "-c", "cd \\"$HOME\\" 2>/dev/null; exec 'notes' \\"$HOME\\"/'Documents/today.txt'"]`, ); expect(readme).toContain( - launchDocumentNote({ - opens: { kind: "seed", document: "Documents/today.txt" }, - }), + "At launch the app opens `~/Documents/today.txt`, the seed's one document.", ); expect(readme).toContain("`~/Documents/today.txt`"); expect(launchLimitations(result.limitations)).toEqual([]); @@ -2151,8 +2138,13 @@ describe("exportDesktopApplication launch document", () => { const made = await exportSeeded("notes %U", { syntheticSeed: true }); const captured = await exportSeeded("notes %U"); const notice = (syntheticSeed: boolean) => - seedWording({ name: "notes", seedCheckpoint: "seed", syntheticSeed }) - .notice; + syntheticSeed + ? "The seed was made for this task, not taken from a user's own data; read `envs/notes/seed/` before publishing the bundle." + : "The seed is the user's real data. Keep this bundle local; do not publish it."; + const limitation = (syntheticSeed: boolean) => + syntheticSeed + ? "The seed was made for the task, not taken from a user's own data; read it before publishing the bundle." + : "The seed is the user's real state at the seed checkpoint; keep the bundle local."; expect(made.result.parameters).toMatchObject({ synthetic_seed: true }); expect(captured.result.parameters).toMatchObject({ synthetic_seed: false }); @@ -2160,10 +2152,8 @@ describe("exportDesktopApplication launch document", () => { [made, true], [captured, false], ] as const) { - expect(exported.result.limitations).toContain(seedLimitation(synthetic)); - expect(exported.result.limitations).not.toContain( - seedLimitation(!synthetic), - ); + expect(exported.result.limitations).toContain(limitation(synthetic)); + expect(exported.result.limitations).not.toContain(limitation(!synthetic)); expect(exported.readme).toContain(notice(synthetic)); expect(exported.readme).not.toContain(notice(!synthetic)); } diff --git a/src/application/desktop/DesktopBuildComputers.ts b/src/application/desktop/DesktopBuildComputers.ts index a5c391e5b7..d13df91851 100644 --- a/src/application/desktop/DesktopBuildComputers.ts +++ b/src/application/desktop/DesktopBuildComputers.ts @@ -328,7 +328,7 @@ const liveComputer = ( * message. */ /** The start failure's notice that the bundle seed still holds lock files. */ -export const seededLockStartNotice = (locks: readonly string[]): string => +const seededLockStartNotice = (locks: readonly string[]): string => `The bundle's seed holds lock files, which can keep the app from starting when it finds its profile or document in use: ${counted(locks)}. Delete them from the bundle, or export it again, which leaves them out, and start again.`; const withSeededLocks = async ( diff --git a/src/application/desktop/DesktopExportLimitations.ts b/src/application/desktop/DesktopExportLimitations.ts index 80668ab105..b99ba9b811 100644 --- a/src/application/desktop/DesktopExportLimitations.ts +++ b/src/application/desktop/DesktopExportLimitations.ts @@ -15,7 +15,7 @@ import type { HostIdentity } from "../../domain/desktop/hostIdentity.js"; import { recoveryListLimitation } from "../../domain/desktop/seedRecoveryList.js"; /** The limitation naming files the desktop Trash held at the seed; none when it held none. */ -export const trashedFilesLimitation = ( +const trashedFilesLimitation = ( files: readonly string[], ): string | undefined => files.length === 0 @@ -23,7 +23,7 @@ export const trashedFilesLimitation = ( : `The desktop Trash held ${String(files.length)} file(s) at the seed checkpoint, which are not seeded: ${files.map((home) => `~/${home}`).join(", ")}. Restore one the task needs and capture again.`; /** The limitation on where the seed came from: made for the task, or the user's real state, kept local. */ -export const seedLimitation = (syntheticSeed: boolean): string => +const seedLimitation = (syntheticSeed: boolean): string => syntheticSeed ? "The seed was made for the task, not taken from a user's own data; read it before publishing the bundle." : "The seed is the user's real state at the seed checkpoint; keep the bundle local."; diff --git a/src/application/desktop/DesktopTaskReaders.ts b/src/application/desktop/DesktopTaskReaders.ts index 35f1b81c73..d2ca3616c4 100644 --- a/src/application/desktop/DesktopTaskReaders.ts +++ b/src/application/desktop/DesktopTaskReaders.ts @@ -30,33 +30,6 @@ export type ReaderFileBytes = | { readonly kind: "error"; readonly error: string } | { readonly kind: "bytes"; readonly bytes: Buffer }; -/** - * Each reader's view of its file by reader id, read by REA's reader runner; - * an absent file has none. `pathOf` names bytes to read in place of the - * home's file. A runner that does not answer throws. - */ -export const readTaskReaderViews = async ( - readers: readonly DesktopTaskReader[], - run: ReaderCommandRun, - pathOf: (reader: DesktopTaskReader) => string | undefined = () => undefined, - work?: string, -): Promise> => { - const views = new Map(); - for (const reader of readers) { - const result = await run( - readerCommand(reader, pathOf(reader), work), - readerTimeout(reader) + READER_RUNNER_SLACK_S, - ); - if (result.exit_code !== 0 || result.timed_out) - throw new Error( - `REA's reader runner for ${reader.id} ${result.timed_out ? "timed out" : `exited ${String(result.exit_code)}`}: ${result.stderr.slice(-1024) || result.stdout.slice(-1024)}`, - ); - const view = readerAnswerView(result.stdout); - if (view !== undefined) views.set(reader.id, view); - } - return views; -}; - /** Recover only a complete, valid answer emitted before later finalization failed. */ const completedReaderAnswer = ( cause: unknown, @@ -112,9 +85,17 @@ export const readerViewsOfBytes = async ( const path = join(folder, reader.file.split("/").at(-1) ?? "file"); await writeFile(path, file.bytes); // The runner works inside this folder, so removing it cleans up after a runner that was killed. - const read = await readTaskReaderViews([reader], run, () => path, folder); - for (const [id, view] of read) views.set(id, view); - if (!read.has(reader.id)) absent.push(reader.id); + const result = await run( + readerCommand(reader, path, folder), + readerTimeout(reader) + READER_RUNNER_SLACK_S, + ); + if (result.exit_code !== 0 || result.timed_out) + throw new Error( + `REA's reader runner for ${reader.id} ${result.timed_out ? "timed out" : `exited ${String(result.exit_code)}`}: ${result.stderr.slice(-1024) || result.stdout.slice(-1024)}`, + ); + const view = readerAnswerView(result.stdout); + if (view === undefined) absent.push(reader.id); + else views.set(reader.id, view); } catch (cause: unknown) { failure = { cause }; const completed = completedReaderAnswer(cause); diff --git a/src/application/environment/environmentBuild.test.ts b/src/application/environment/environmentBuild.test.ts index ea35d943b9..6229505826 100644 --- a/src/application/environment/environmentBuild.test.ts +++ b/src/application/environment/environmentBuild.test.ts @@ -26,7 +26,7 @@ import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirect import { ArtifactFileCleanupFailure } from "../../artifacts/ArtifactFileOperation.js"; import { writeJsonOutput } from "../../cli/jsonOutput.js"; import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; -import { analysisViewRequestSchema } from "../../domain/analysisView/analysisView.js"; +import { analysisViewRequestSchema } from "../../domain/analysisView/analysisViewRequest.js"; import { summarizeEnvironmentCoverage } from "../../domain/analysisView/environmentCoverageView.js"; import type { Evidence } from "../../domain/evidence.js"; import { inspectAnalysisViewValidated } from "../analysisView/AnalysisViewService.js"; diff --git a/src/application/environment/environmentTools.test.ts b/src/application/environment/environmentTools.test.ts index 25dd8d4572..c1662f9521 100644 --- a/src/application/environment/environmentTools.test.ts +++ b/src/application/environment/environmentTools.test.ts @@ -26,11 +26,14 @@ const withTools = (tools: readonly EnvironmentManifestTool[]) => ({ manifest: { ...source.manifest, tools: [...tools] }, }); const environmentToolInputSchema = (tool: EnvironmentManifestTool) => { - const [definition] = environmentToolDefinitions(withTools([tool])); + const [definition] = environmentToolDefinitions( + withTools([tool]), + source.manifest.name, + ); if (definition === undefined) throw new Error("Missing tool definition"); return definition.inputSchema; }; -const definitions = environmentToolDefinitions(source); +const definitions = environmentToolDefinitions(source, source.manifest.name); const tool = (name: string): EnvironmentManifestTool => { const found = source.manifest.tools.find((entry) => entry.name === name); @@ -47,9 +50,9 @@ describe("environmentToolDefinitions", () => { expect(definition.description).toContain( "state.source, not replay.exact_match, identifies state vs recording", ); - expect(environmentToolDefinitions(source)[0]?.description).not.toContain( - "state.source", - ); + expect( + environmentToolDefinitions(source, source.manifest.name)[0]?.description, + ).not.toContain("state.source"); }); it("maps every manifest tool in order with a replay description naming its records and evidence", () => { @@ -154,10 +157,13 @@ const usersAnswering = ( record_ordinals: [...exchanges.keys()], statuses: [...statuses], }; - const [definition] = environmentToolDefinitions({ - ...withTools([users]), - exchanges, - }); + const [definition] = environmentToolDefinitions( + { + ...withTools([users]), + exchanges, + }, + source.manifest.name, + ); return definition?.description; }; const returnsOf = (bodyText: string): string | undefined => @@ -314,28 +320,31 @@ describe("environment tool response summaries", () => { const users = tool("get_users_by_id"); const ordinal = users.record_ordinals[0] ?? 0; const catalog = (contentType: string) => - prepareDefinitions({ - ...withTools( - Array.from({ length: 200 }, (_, index) => ({ - ...users, - name: `get_users_${String(index)}`, - record_ordinals: [ordinal], - })), - ), - exchanges: new Map([ - [ - ordinal, - { + prepareDefinitions( + { + ...withTools( + Array.from({ length: 200 }, (_, index) => ({ + ...users, + name: `get_users_${String(index)}`, + record_ordinals: [ordinal], + })), + ), + exchanges: new Map([ + [ ordinal, - path: "/api/users/2", - query: null, - status: 200, - contentType, - bodyText: JSON.stringify(fields), - }, - ], - ]), - }); + { + ordinal, + path: "/api/users/2", + query: null, + status: 200, + contentType, + bodyText: JSON.stringify(fields), + }, + ], + ]), + }, + source.manifest.name, + ); expect(catalog("text/html").ok).toBe(true); const bounded = catalog("application/json"); expect(bounded).toMatchObject({ @@ -346,18 +355,21 @@ describe("environment tool response summaries", () => { reportedLimits: { max_catalog_bytes: ENVIRONMENT_CATALOG_LIMIT_BYTES }, }, }); - const [single] = environmentToolDefinitions({ - ...withTools([users]), - exchanges: new Map([ - [ - ordinal, - { - ...source.exchanges.get(ordinal)!, - bodyText: JSON.stringify(fields), - }, - ], - ]), - }); + const [single] = environmentToolDefinitions( + { + ...withTools([users]), + exchanges: new Map([ + [ + ordinal, + { + ...source.exchanges.get(ordinal)!, + bodyText: JSON.stringify(fields), + }, + ], + ]), + }, + source.manifest.name, + ); expect(Buffer.byteLength(single?.description ?? "")).toBeLessThan(1700); expect(single?.description).toContain( "/300 shown; full list in capture Evidence)", @@ -374,7 +386,10 @@ describe("environment catalog description budgets", () => { path_template: `/${"😀".repeat(1000)}`, query_parameter_names: [query], }; - const [definition] = environmentToolDefinitions(withTools([expanded])); + const [definition] = environmentToolDefinitions( + withTools([expanded]), + source.manifest.name, + ); expect(definition?.tool).toBe(expanded); expect(Buffer.byteLength(definition?.description ?? "")).toBeLessThan(3500); expect(definition?.description).toContain( @@ -406,7 +421,10 @@ describe("environment catalog description budgets", () => { })), record_ordinals: ordinals, }; - const [definition] = environmentToolDefinitions(withTools([expanded])); + const [definition] = environmentToolDefinitions( + withTools([expanded]), + source.manifest.name, + ); expect(definition?.tool).toBe(expanded); expect(expanded.params[0]?.observed_values).toBe(observations); expect(expanded.record_ordinals).toBe(ordinals); @@ -431,7 +449,10 @@ describe("environment catalog description budgets", () => { ...users, path_template: `/api/${"segment/".repeat(200_000)}{id}`, }; - const [definition] = environmentToolDefinitions(withTools([long])); + const [definition] = environmentToolDefinitions( + withTools([long]), + source.manifest.name, + ); expect(Buffer.byteLength(definition?.description ?? "")).toBeLessThan(2048); expect(definition?.description).toContain( "path display truncated; full value in manifest Evidence", @@ -595,6 +616,7 @@ describe("environment tool annotations", () => { method, })), ), + source.manifest.name, ).map((definition) => definition.annotations); expect(hints).toEqual([ { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, diff --git a/src/application/environment/environmentTools.ts b/src/application/environment/environmentTools.ts index 5d31ee50ba..f0446bdf4f 100644 --- a/src/application/environment/environmentTools.ts +++ b/src/application/environment/environmentTools.ts @@ -432,7 +432,7 @@ const catalogBody = ( /** Admit complete discovery before binding; six bytes per UTF-16 unit covers JSON escaping and UTF-8 expansion. */ export const environmentToolDefinitions = ( source: EnvironmentSource, - name: string = source.manifest.name, + name: string, { operation = ENVIRONMENT_SERVE_OPERATION, dataPlane = "none", diff --git a/src/application/javascript/JavaScriptArtifactAnalysis.order.test.ts b/src/application/javascript/JavaScriptArtifactAnalysis.order.test.ts index 4692fcf0b7..3cea7a14ca 100644 --- a/src/application/javascript/JavaScriptArtifactAnalysis.order.test.ts +++ b/src/application/javascript/JavaScriptArtifactAnalysis.order.test.ts @@ -5,7 +5,7 @@ import { } from "./JavaScriptArtifactAnalysis.js"; import { createJavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js"; import { AnalysisResourceConstraintError } from "../../domain/analysisErrorCore.js"; -import { analyzeJavaScriptStaticSource } from "../../domain/javascript/javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { err, ok } from "../../domain/result.js"; diff --git a/src/application/javascript/JavaScriptArtifactAnalysis.packageType.test.ts b/src/application/javascript/JavaScriptArtifactAnalysis.packageType.test.ts index c743c7420f..3fb8d69c54 100644 --- a/src/application/javascript/JavaScriptArtifactAnalysis.packageType.test.ts +++ b/src/application/javascript/JavaScriptArtifactAnalysis.packageType.test.ts @@ -1,7 +1,7 @@ import { expect, it } from "vitest"; import { analyzeAndAdoptJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js"; import { createJavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js"; -import { analyzeJavaScriptStaticSource } from "../../domain/javascript/javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import type { JavaScriptArtifactFile, JavaScriptUnanalyzedArtifactFile, diff --git a/src/application/javascript/JavaScriptArtifactAnalysis.revocation.test.ts b/src/application/javascript/JavaScriptArtifactAnalysis.revocation.test.ts index 0c84bf2ad2..079a89f84e 100644 --- a/src/application/javascript/JavaScriptArtifactAnalysis.revocation.test.ts +++ b/src/application/javascript/JavaScriptArtifactAnalysis.revocation.test.ts @@ -5,7 +5,7 @@ import { AnalysisCancelledError, AnalysisResourceConstraintError, } from "../../domain/analysisErrorCore.js"; -import { analyzeJavaScriptStaticSource } from "../../domain/javascript/javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { err, ok } from "../../domain/result.js"; diff --git a/src/application/javascript/JavaScriptArtifactGraphStructure.test.ts b/src/application/javascript/JavaScriptArtifactGraphStructure.test.ts index e802b09016..03dc849c6b 100644 --- a/src/application/javascript/JavaScriptArtifactGraphStructure.test.ts +++ b/src/application/javascript/JavaScriptArtifactGraphStructure.test.ts @@ -8,7 +8,7 @@ import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptA import { analyzeJavaScriptStaticSource, analyzeParsedJavaScriptStaticSource, -} from "../../domain/javascript/javascriptStaticAnalysis.js"; +} from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { parseJavaScriptSource } from "../../domain/javascript/javascriptSourceParser.js"; import type { AnalyzedJavaScriptArtifactFile, diff --git a/src/application/javascript/JavaScriptArtifactPathResolution.test.ts b/src/application/javascript/JavaScriptArtifactPathResolution.test.ts index 6b6cd8e945..7b79b14183 100644 --- a/src/application/javascript/JavaScriptArtifactPathResolution.test.ts +++ b/src/application/javascript/JavaScriptArtifactPathResolution.test.ts @@ -8,7 +8,7 @@ import { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js"; import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { artifactLocalIdentity } from "./JavaScriptArtifactGraphContext.js"; import { applicationNodeIdentitySchema } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js"; -import { analyzeJavaScriptStaticSource } from "../../domain/javascript/javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const completeInventoryCoverage = { scope: "container-entries", diff --git a/src/application/javascript/JavaScriptSemanticGraphElectronProjection.test.ts b/src/application/javascript/JavaScriptSemanticGraphElectronProjection.test.ts index 94ac750692..b3c6d40eaf 100644 --- a/src/application/javascript/JavaScriptSemanticGraphElectronProjection.test.ts +++ b/src/application/javascript/JavaScriptSemanticGraphElectronProjection.test.ts @@ -1,5 +1,5 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "../../domain/javascript/javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { analyzeJavaScriptSemantics } from "../../domain/javascript/javascriptSemanticAnalysis.js"; import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { projectJavaScriptSemanticFileSteps } from "./JavaScriptSemanticGraphBuilder.js"; diff --git a/src/application/native/NativeValueTrace.fixture.ts b/src/application/native/NativeValueTrace.fixture.ts index 1fb41ec9b9..e68bd7342d 100644 --- a/src/application/native/NativeValueTrace.fixture.ts +++ b/src/application/native/NativeValueTrace.fixture.ts @@ -8,6 +8,12 @@ import { jsonValueSchema } from "../../domain/jsonValue.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { err, ok } from "../../domain/result.js"; +/** Expected user-facing unknown reasons, independent of the trace producer. */ +export const EXPECTED_CLEANUP_STOPPED_TRACE = + "Trace stopped because the provider did not confirm cleanup of resources it owns"; +export const EXPECTED_UNRECORDED_VALUE_FLOW = + "Native def-use was not recorded for this procedure."; + /** Simulated provider seam for value traces; not a real decompiler observation. */ export const valueTraceSubject = { path: "/fixture", diff --git a/src/application/native/NativeValueTrace.test.ts b/src/application/native/NativeValueTrace.test.ts index 5374de96f2..272f0c2bea 100644 --- a/src/application/native/NativeValueTrace.test.ts +++ b/src/application/native/NativeValueTrace.test.ts @@ -1,9 +1,6 @@ import { createAnalysisExecutionEvidence } from "../../domain/analysisObservation.js"; import { describe, expect, it } from "vitest"; -import { - traceNativeValues as traceNativeValuesCore, - UNRECORDED_NATIVE_VALUE_FLOW, -} from "./NativeValueTrace.js"; +import { traceNativeValues as traceNativeValuesCore } from "./NativeValueTrace.js"; import { executeComposedWorkflow } from "../WorkflowEvidence.js"; import { createAnalysisExecution, @@ -22,6 +19,7 @@ import { import { NoBinaryOpenError } from "../../domain/configurationErrors.js"; import { ok, err } from "../../domain/result.js"; import { + EXPECTED_UNRECORDED_VALUE_FLOW, nativeFlowDossier, pcodeOperation as node, pcodeScalar as scalar, @@ -196,7 +194,7 @@ describe("native value trace seed admission", () => { { procedure: base.procedure.address, address: null, - reason: UNRECORDED_NATIVE_VALUE_FLOW, + reason: EXPECTED_UNRECORDED_VALUE_FLOW, }, ], }); diff --git a/src/application/native/NativeValueTrace.ts b/src/application/native/NativeValueTrace.ts index c32c565fc9..a489e28e7e 100644 --- a/src/application/native/NativeValueTrace.ts +++ b/src/application/native/NativeValueTrace.ts @@ -29,11 +29,11 @@ import { import { createValueTraceBudget } from "./NativeValueTraceBudget.js"; /** The located unknown where unconfirmed provider cleanup stopped a trace. */ -export const CLEANUP_STOPPED_VALUE_TRACE = +const CLEANUP_STOPPED_VALUE_TRACE = "Trace stopped because the provider did not confirm cleanup of resources it owns"; /** Fallback when a dossier has no native value-flow facet for this procedure. */ -export const UNRECORDED_NATIVE_VALUE_FLOW = +const UNRECORDED_NATIVE_VALUE_FLOW = "Native def-use was not recorded for this procedure."; /** Compose bounded high-p-code def-use graphs across exactly referenced call destinations. */ diff --git a/src/application/native/NativeValueTraceStops.test.ts b/src/application/native/NativeValueTraceStops.test.ts index 40a1568d63..9408f93431 100644 --- a/src/application/native/NativeValueTraceStops.test.ts +++ b/src/application/native/NativeValueTraceStops.test.ts @@ -6,11 +6,9 @@ import { AnalysisCapabilityUnavailableError, } from "../../domain/analysisErrorCore.js"; import { nativeValueTraceSchema } from "../../domain/native/nativeValueTrace.js"; +import { traceNativeValues } from "./NativeValueTrace.js"; import { - CLEANUP_STOPPED_VALUE_TRACE, - traceNativeValues, -} from "./NativeValueTrace.js"; -import { + EXPECTED_CLEANUP_STOPPED_TRACE, failingValueTraceReads, valueTraceDiamond, valueTraceProvider as provider, @@ -149,7 +147,7 @@ describe("native value trace child stops", () => { { procedure: "0x1000", address: "0x1008", - reason: CLEANUP_STOPPED_VALUE_TRACE, + reason: EXPECTED_CLEANUP_STOPPED_TRACE, }, ], ); diff --git a/src/application/process/ProcessComparisonService.ts b/src/application/process/ProcessComparisonService.ts index f2fc3a13ba..3f7a6f458a 100644 --- a/src/application/process/ProcessComparisonService.ts +++ b/src/application/process/ProcessComparisonService.ts @@ -34,7 +34,6 @@ export const compareProcessEvidence = ( lookup?: EvidenceLookup, options: { readonly signal?: AbortSignal; - readonly now?: () => number; } = {}, ): Result => { const selected = resolveEvidenceRequest( @@ -57,7 +56,6 @@ export const compareProcessEvidence = ( ...(request.trace_spec === undefined ? {} : { traceSpecification: request.trace_spec }), - ...(options.now === undefined ? {} : { now: options.now }), }); }; diff --git a/src/artifacts/NodeSeaArtifactReader.test.ts b/src/artifacts/NodeSeaArtifactReader.test.ts index b3f525db68..8a8910c1f9 100644 --- a/src/artifacts/NodeSeaArtifactReader.test.ts +++ b/src/artifacts/NodeSeaArtifactReader.test.ts @@ -22,9 +22,9 @@ import type { ArtifactEntry } from "./ArtifactReader.js"; import { artifactFormatDescriptor } from "./artifactFormats.js"; import { ArtifactResourceScope } from "./ArtifactResourceScope.js"; import { extractArtifact } from "./extraction/ArtifactExtraction.js"; -import { NODE_SEA_MAGIC } from "./nodeSea/NodeSeaBlob.js"; import { NODE_SEA_FUSE_HASH, + NODE_SEA_FIXTURE_MAGIC, buildNodeSeaBlob, nodeSeaElf, nodeSeaMachO, @@ -206,7 +206,7 @@ it.each([ expect(root).toMatchObject({ artifact_kind: "container" }); expect(root?.format_metadata).toMatchObject({ kind: "node-sea", - magic: NODE_SEA_MAGIC, + magic: NODE_SEA_FIXTURE_MAGIC, carrier, code_path: "app/main.js", disable_experimental_sea_warning: true, @@ -503,7 +503,7 @@ it("fails closed when the located blob is truncated, has a bad magic, or unknown message: expect.stringMatching(/magic.*not.*0x143da20/iu), }); const unknown = Buffer.alloc(9); - unknown.writeUInt32LE(NODE_SEA_MAGIC, 0); + unknown.writeUInt32LE(NODE_SEA_FIXTURE_MAGIC, 0); unknown.writeUInt32LE(1 << 5, 4); unknown[8] = 1; await expect( diff --git a/src/artifacts/ReleaseExposureReader.ts b/src/artifacts/ReleaseExposureReader.ts index 88ce71ece7..a3d4054db1 100644 --- a/src/artifacts/ReleaseExposureReader.ts +++ b/src/artifacts/ReleaseExposureReader.ts @@ -2551,7 +2551,7 @@ const embeddedPackageFormat = async ( nativeFormat: NativeFormat, state: ScanState, context: ScanContext, - location = filePath, + location: string, expectedSha256?: string, ): Promise => { try { diff --git a/src/artifacts/apple/InterfaceBuilderAnalysis.ts b/src/artifacts/apple/InterfaceBuilderAnalysis.ts index edf282f181..67ddf7ae0a 100644 --- a/src/artifacts/apple/InterfaceBuilderAnalysis.ts +++ b/src/artifacts/apple/InterfaceBuilderAnalysis.ts @@ -1014,8 +1014,8 @@ const isInterfaceBuilderArchive = (path: string): boolean => { const readEntry = async ( reader: DirectoryArtifactReader, entry: ArtifactEntry, - signal?: AbortSignal, - maxInputBytes = MAX_AGGREGATE_INPUT_BYTES, + signal: AbortSignal | undefined, + maxInputBytes: number, ): Promise => { if (entry.declaredSize !== null && entry.declaredSize > maxInputBytes) throw new InterfaceBuilderDecodeBudgetExceeded( diff --git a/src/artifacts/apple/NibViewHierarchy.test.ts b/src/artifacts/apple/NibViewHierarchy.test.ts index 4aedb7f11f..6d7ad06e9f 100644 --- a/src/artifacts/apple/NibViewHierarchy.test.ts +++ b/src/artifacts/apple/NibViewHierarchy.test.ts @@ -1,6 +1,34 @@ import { describe, expect, it } from "vitest"; import { projectNibViewHierarchy } from "./NibViewHierarchy.js"; +import { InterfaceBuilderDecodeBudget } from "./InterfaceBuilderDecodeBudget.js"; + +// Use the same budget and diagnostic accounting as Interface Builder's NIB owner. +const projectFixture = ( + objects: Parameters[0], + retainedIds: ReadonlySet, + expectedDiagnostics: readonly string[] = [], + maximumBytes = 4_000_000, +) => { + const budget = new InterfaceBuilderDecodeBudget(maximumBytes); + const diagnostics: string[] = []; + const projection = projectNibViewHierarchy( + objects, + retainedIds, + () => budget.tryReserve(128), + 20_000, + (description) => { + budget.reserve( + 256, + "NIB registry hierarchy diagnostics exceed the aggregate Interface Builder decode budget", + ); + budget.reserveJsonStringOccurrence(description); + diagnostics.push(description); + }, + ); + expect(diagnostics).toEqual(expectedDiagnostics); + return projection; +}; const object = (id: number, parent?: number) => ({ id, @@ -11,7 +39,7 @@ const object = (id: number, parent?: number) => ({ describe("serialized NIB view hierarchy bounds", () => { it("does not invent hierarchy for objects without serialized relationships", () => { - expect(projectNibViewHierarchy([object(0)], new Set([0]))).toEqual({ + expect(projectFixture([object(0)], new Set([0]))).toEqual({ hierarchy: null, omitted: 0, }); @@ -19,7 +47,7 @@ describe("serialized NIB view hierarchy bounds", () => { it("uses the serialized parent direction and retains all sibling links", () => { expect( - projectNibViewHierarchy( + projectFixture( [object(0), object(1, 0), object(2, 0)], new Set([0, 1, 2]), ), @@ -38,7 +66,7 @@ describe("serialized NIB view hierarchy bounds", () => { }); it("reports missing parents without inventing parentage", () => { - expect(projectNibViewHierarchy([object(0, 99)], new Set([0]))).toEqual({ + expect(projectFixture([object(0, 99)], new Set([0]))).toEqual({ hierarchy: [], omitted: 1, }); @@ -46,7 +74,7 @@ describe("serialized NIB view hierarchy bounds", () => { it("reports a missing encoded window content view as incomplete", () => { expect( - projectNibViewHierarchy( + projectFixture( [ { id: 0, @@ -61,7 +89,7 @@ describe("serialized NIB view hierarchy bounds", () => { it("marks unsupported serialized parent values as incomplete", () => { expect( - projectNibViewHierarchy( + projectFixture( [{ id: 0, class_name: "NSView", values: { NSSuperview: "unknown" } }], new Set([0]), ), @@ -70,7 +98,7 @@ describe("serialized NIB view hierarchy bounds", () => { it("reports cyclic and self-referential parentage as incomplete", () => { expect( - projectNibViewHierarchy( + projectFixture( [object(0, 1), object(1, 0), object(2, 2)], new Set([0, 1, 2]), ), @@ -81,9 +109,7 @@ describe("serialized NIB view hierarchy bounds", () => { }); it("does not project children whose parent was not retained", () => { - expect( - projectNibViewHierarchy([object(0), object(1, 0)], new Set([1])), - ).toEqual({ + expect(projectFixture([object(0), object(1, 0)], new Set([1]))).toEqual({ hierarchy: [], omitted: 1, }); @@ -94,8 +120,7 @@ describe("serialized NIB view hierarchy bounds", () => { object(id, id === 0 ? undefined : id - 1), ); expect( - projectNibViewHierarchy(objects, new Set(objects.map(({ id }) => id))) - .omitted, + projectFixture(objects, new Set(objects.map(({ id }) => id))).omitted, ).toBe(3); }); @@ -104,14 +129,21 @@ describe("serialized NIB view hierarchy bounds", () => { object(id, id === 0 ? undefined : 0), ); expect( - projectNibViewHierarchy(objects, new Set(objects.map(({ id }) => id))) - .omitted, + projectFixture(objects, new Set(objects.map(({ id }) => id))).omitted, ).toBe(2); }); + + it("reports nodes the shared representation budget cannot reserve", () => { + const objects = [object(0), object(1, 0)]; + expect(projectFixture(objects, new Set([0, 1]), [], 128)).toEqual({ + hierarchy: [{ objectID: "0", children: [] }], + omitted: 1, + }); + }); }); it("does not treat a dangling registry owner as top-level authority", () => { - const result = projectNibViewHierarchy( + const result = projectFixture( [ { id: 0, @@ -135,6 +167,9 @@ it("does not treat a dangling registry owner as top-level authority", () => { }, ], new Set([1]), + [ + "NSIBObjectData object 0: NSObjectsKeys/NSObjectsValues pair child 1 with registry value 99; NSRoot is 99 (unresolved). This decoded registry association does not establish visual parentage.", + ], ); expect(result.hierarchy).toEqual([]); expect(result.omitted).toBeGreaterThan(0); diff --git a/src/artifacts/apple/NibViewHierarchy.ts b/src/artifacts/apple/NibViewHierarchy.ts index 8a45359fb5..68874d6d97 100644 --- a/src/artifacts/apple/NibViewHierarchy.ts +++ b/src/artifacts/apple/NibViewHierarchy.ts @@ -56,9 +56,9 @@ const isNonViewCell = ( export const projectNibViewHierarchy = ( objects: NibArchiveDocument["objects"], retainedIds: ReadonlySet, - reserveNode: () => boolean = () => true, - maxNodes = 20_000, - reportUnknownRegistry: (description: string) => void = () => {}, + reserveNode: () => boolean, + maxNodes: number, + reportUnknownRegistry: (description: string) => void, ) => { const byId = new Map(objects.map((object) => [object.id, object])); const parents = new Map(); diff --git a/src/artifacts/apple/PropertyListDecodeBudget.ts b/src/artifacts/apple/PropertyListDecodeBudget.ts index fc5b201b64..3696c09eec 100644 --- a/src/artifacts/apple/PropertyListDecodeBudget.ts +++ b/src/artifacts/apple/PropertyListDecodeBudget.ts @@ -74,13 +74,10 @@ const XML_REFERENCE_BYTES = 64; const estimateXmlPlistExpansion = ( bytes: Buffer, maxBytes: number, - xmlText?: string, - numberProjection = false, - budget = "the property list decode budget", - fail: ( - kind: BinaryPlistExpansionFailure, - message: string, - ) => Error = propertyListDecodeBudgetFailure, + xmlText: string | undefined, + numberProjection: boolean, + budget: string, + fail: (kind: BinaryPlistExpansionFailure, message: string) => Error, ): number => { const xml = xmlText ?? decodeXmlPlistText(bytes); // The decoded text and the DOM's copies of its character data. diff --git a/src/artifacts/artifactFormats.test.ts b/src/artifacts/artifactFormats.test.ts index 3dc0afa291..7fef477b0a 100644 --- a/src/artifacts/artifactFormats.test.ts +++ b/src/artifacts/artifactFormats.test.ts @@ -5,10 +5,7 @@ import { ZIP_PACKAGE_FORMATS, type ZipPackageFormat, } from "../domain/zipPackageFormat.js"; -import { - ARTIFACT_FORMATS, - ZIP_FAMILY_CAPABILITIES, -} from "./artifactFormats.js"; +import { ARTIFACT_FORMATS } from "./artifactFormats.js"; import { createArtifactReader, type ArtifactReaderFormat, @@ -76,7 +73,15 @@ describe("artifact format descriptors", () => { it("shares one capability row across zip package families", () => { for (const id of ZIP_PACKAGE_FORMATS) - expect(ARTIFACT_FORMATS[id]).toMatchObject(ZIP_FAMILY_CAPABILITIES); + expect(ARTIFACT_FORMATS[id]).toMatchObject({ + zipFamily: true, + occurrenceContainer: true, + roleContainer: false, + retainClassifiedSource: true, + extractable: true, + requiresDarwin: false, + absentInventory: "unavailable", + }); const zipIds = new Set(ZIP_PACKAGE_FORMATS); for (const id of ARTIFACT_FORMAT_IDS) if (!zipIds.has(id as ZipPackageFormat)) diff --git a/src/artifacts/artifactFormats.ts b/src/artifacts/artifactFormats.ts index d9b3fdcc2a..44465e4f39 100644 --- a/src/artifacts/artifactFormats.ts +++ b/src/artifacts/artifactFormats.ts @@ -134,7 +134,7 @@ function admittedAs( } /** One capability row for every ZIP-backed package family. */ -export const ZIP_FAMILY_CAPABILITIES = { +const ZIP_FAMILY_CAPABILITIES = { zipFamily: true, occurrenceContainer: true, roleContainer: false, diff --git a/src/artifacts/bun/BunModuleGraph.test.ts b/src/artifacts/bun/BunModuleGraph.test.ts index 03fb1b9fde..104b7cfe0b 100644 --- a/src/artifacts/bun/BunModuleGraph.test.ts +++ b/src/artifacts/bun/BunModuleGraph.test.ts @@ -6,13 +6,11 @@ import { open, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it, vi } from "vitest"; import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js"; -import { - probeBunStandaloneGraph, - BUN_GRAPH_SUFFIX_BYTES, -} from "./BunModuleGraphLayout.js"; +import { probeBunStandaloneGraph } from "./BunModuleGraphLayout.js"; import { decodeBunModuleGraph } from "./BunModuleGraphMembers.js"; import { locateBunGraphFixture, + BUN_FIXTURE_GRAPH_SUFFIX_BYTES, bunModuleGraphPayload, bunPrefixedPayload, machoBunSection, @@ -167,7 +165,11 @@ it.each(["trailer", "byte_count"])( modules: [{ name: "/$bunfs/root/main.js" }], }); if (field === "trailer") payload[payload.length - 1] = 0; - else payload.writeBigUInt64LE(0n, payload.length - BUN_GRAPH_SUFFIX_BYTES); + else + payload.writeBigUInt64LE( + 0n, + payload.length - BUN_FIXTURE_GRAPH_SUFFIX_BYTES, + ); const prefix = Buffer.alloc(4); prefix.writeUInt32LE(payload.length); expect( diff --git a/src/artifacts/bun/BunModuleGraphLayout.ts b/src/artifacts/bun/BunModuleGraphLayout.ts index dec848b418..b95d1b65bf 100644 --- a/src/artifacts/bun/BunModuleGraphLayout.ts +++ b/src/artifacts/bun/BunModuleGraphLayout.ts @@ -17,10 +17,10 @@ import { abortIfNeeded } from "../ArtifactHash.js"; import { ArtifactReaderFailure } from "../ArtifactReader.js"; /** Exact trailer written by Bun's standalone module graph serializer. */ -export const BUN_TRAILER = Buffer.from("\n---- Bun! ----\n"); -export const BUN_TRAILER_BYTES = BUN_TRAILER.length; -export const BUN_OFFSETS_BYTES = 32; -export const BUN_GRAPH_SUFFIX_BYTES = BUN_OFFSETS_BYTES + BUN_TRAILER_BYTES; +const BUN_TRAILER = Buffer.from("\n---- Bun! ----\n"); +const BUN_TRAILER_BYTES = BUN_TRAILER.length; +const BUN_OFFSETS_BYTES = 32; +const BUN_GRAPH_SUFFIX_BYTES = BUN_OFFSETS_BYTES + BUN_TRAILER_BYTES; /** Current compilers prefix a section with this little-endian payload length. */ export const BUN_LENGTH_PREFIX_BYTES = 8; /** Bun 1.3.3 through 1.3.8 module records. */ diff --git a/src/artifacts/bun/BunStandalone.fixture.ts b/src/artifacts/bun/BunStandalone.fixture.ts index 553d8bb775..ce433fbc22 100644 --- a/src/artifacts/bun/BunStandalone.fixture.ts +++ b/src/artifacts/bun/BunStandalone.fixture.ts @@ -1,9 +1,5 @@ import type { FileHandle } from "node:fs/promises"; import { - BUN_OFFSETS_BYTES, - BUN_RECORD_BYTES_1_3_3, - BUN_RECORD_BYTES_1_3_9, - BUN_TRAILER, loadBunStandaloneGraph, probeBunStandaloneGraph, type BunLayoutRevision, @@ -11,6 +7,15 @@ import { type BunStringPointer, } from "./BunModuleGraphLayout.js"; +// https://github.com/oven-sh/bun/blob/bun-v1.3.9/src/StandaloneModuleGraph.zig +// and the 1.3.3 serializer define the two supported layouts. +const OFFSETS_BYTES = 32; +const RECORD_BYTES_1_3_3 = 36; +const RECORD_BYTES_1_3_9 = 52; +export const BUN_FIXTURE_TRAILER = Buffer.from("\n---- Bun! ----\n"); +export const BUN_FIXTURE_GRAPH_SUFFIX_BYTES = + OFFSETS_BYTES + BUN_FIXTURE_TRAILER.length; + /** Exercise the same two-stage probe and payload read as the artifact reader. */ export const locateBunGraphFixture = async ( handle: FileHandle, @@ -46,7 +51,7 @@ export const bunModuleGraphPayload = (input: { }): Buffer => { const revision = input.revision ?? "bun-1.3.9"; const recordBytes = - revision === "bun-1.3.3" ? BUN_RECORD_BYTES_1_3_3 : BUN_RECORD_BYTES_1_3_9; + revision === "bun-1.3.3" ? RECORD_BYTES_1_3_3 : RECORD_BYTES_1_3_9; const modules = input.modules ?? []; const chunks: Buffer[] = []; let cursor = 0; @@ -87,8 +92,8 @@ export const bunModuleGraphPayload = (input: { writePointer(record, 8, contents); writePointer(record, 16, row.sourcemap); writePointer(record, 24, row.bytecode); - const metadataAt = recordBytes === BUN_RECORD_BYTES_1_3_9 ? 48 : 32; - if (recordBytes === BUN_RECORD_BYTES_1_3_9) { + const metadataAt = recordBytes === RECORD_BYTES_1_3_9 ? 48 : 32; + if (recordBytes === RECORD_BYTES_1_3_9) { writePointer(record, 32, row.moduleInfo); writePointer(record, 40, row.origin); } @@ -100,13 +105,13 @@ export const bunModuleGraphPayload = (input: { chunks.push(records.subarray(0, tableLength)); cursor += tableLength; const data = Buffer.concat(chunks); - const offsets = Buffer.alloc(BUN_OFFSETS_BYTES); + const offsets = Buffer.alloc(OFFSETS_BYTES); offsets.writeBigUInt64LE(BigInt(data.length), 0); writePointer(offsets, 8, { offset: tableOffset, length: tableLength }); offsets.writeUInt32LE(input.entryPointId ?? 0, 16); writePointer(offsets, 20, argv); offsets.writeUInt32LE(input.flags ?? 0, 28); - return Buffer.concat([data, offsets, BUN_TRAILER]); + return Buffer.concat([data, offsets, BUN_FIXTURE_TRAILER]); }; /** Length prefix used by current Mach-O, PE, and ELF section writers. */ diff --git a/src/artifacts/deno/DenoSectionLayout.test.ts b/src/artifacts/deno/DenoSectionLayout.test.ts index f568477dd9..8c7d4614b8 100644 --- a/src/artifacts/deno/DenoSectionLayout.test.ts +++ b/src/artifacts/deno/DenoSectionLayout.test.ts @@ -1,18 +1,26 @@ import { expect, it } from "vitest"; import { - DENO_SECTION_MAGIC, MAX_DENO_METADATA_JSON_BYTES, MAX_DENO_TABLE_COUNT, - parseDenoSectionBytes, + parseDenoSection, } from "./DenoSectionLayout.js"; import { buildDenoSection, defaultDenoMetadata, + DENO_FIXTURE_SECTION_MAGIC, } from "./DenoStandalone.fixture.js"; const secret = "super-secret-value"; +const parseDenoSectionBytes = (bytes: Buffer, signal?: AbortSignal) => + parseDenoSection( + bytes.length, + (offset, length) => + Promise.resolve(bytes.subarray(offset, offset + length)), + signal, + ); + it("parses metadata, tables, remote regions, and embedded configuration", async () => { const original = Buffer.from("export const value = 1;\n"); const transpiled = Buffer.from("transpiled\n"); @@ -123,9 +131,9 @@ it("treats a missing opening marker as an unsupported layout", async () => { it("refuses metadata JSON above the named allowance before reading it as JSON", async () => { const claimed = MAX_DENO_METADATA_JSON_BYTES + 1; - const bytes = Buffer.alloc(DENO_SECTION_MAGIC.length + 8 + claimed); - DENO_SECTION_MAGIC.copy(bytes, 0); - bytes.writeBigUInt64LE(BigInt(claimed), DENO_SECTION_MAGIC.length); + const bytes = Buffer.alloc(DENO_FIXTURE_SECTION_MAGIC.length + 8 + claimed); + DENO_FIXTURE_SECTION_MAGIC.copy(bytes, 0); + bytes.writeBigUInt64LE(BigInt(claimed), DENO_FIXTURE_SECTION_MAGIC.length); await expect(parseDenoSectionBytes(bytes)).rejects.toMatchObject({ reason: "limit", message: expect.stringContaining("metadata JSON"), diff --git a/src/artifacts/deno/DenoSectionLayout.ts b/src/artifacts/deno/DenoSectionLayout.ts index 5b23e4df08..93d2a7b89c 100644 --- a/src/artifacts/deno/DenoSectionLayout.ts +++ b/src/artifacts/deno/DenoSectionLayout.ts @@ -366,18 +366,6 @@ export const parseDenoSection = async ( }; }; -/** Parse a section that is already in memory. Tests and small fixtures use this. */ -export const parseDenoSectionBytes = ( - bytes: Buffer, - signal?: AbortSignal, -): Promise => - parseDenoSection( - bytes.length, - (offset, length) => - Promise.resolve(bytes.subarray(offset, offset + length)), - signal, - ); - const addRemoteBody = (total: bigint, length: number): bigint => { const next = total + BigInt(length); if (next > BigInt(MAX_DENO_REMOTE_BODY_BYTES)) diff --git a/src/artifacts/deno/DenoStandalone.fixture.ts b/src/artifacts/deno/DenoStandalone.fixture.ts index 480434a9dc..d8717903cc 100644 --- a/src/artifacts/deno/DenoStandalone.fixture.ts +++ b/src/artifacts/deno/DenoStandalone.fixture.ts @@ -1,7 +1,9 @@ import { elfNote, elfNoteFixture } from "../../native/elf/ElfNotes.fixture.js"; import { peResourceFixture } from "../../native/pe/PeResources.fixture.js"; -import { DENO_SECTION_MAGIC } from "./DenoSectionLayout.js"; +// https://github.com/denoland/deno/blob/v2.9.8/cli/lib/standalone/binary.rs +// declares this section marker. +export const DENO_FIXTURE_SECTION_MAGIC = Buffer.from("d3n0l4nd", "latin1"); export interface DenoSpecifierFixture { readonly text?: string; @@ -93,7 +95,7 @@ export const buildDenoSection = (options: DenoSectionFixture = {}): Buffer => { const vfsBytes = Buffer.from(JSON.stringify(vfs), "utf8"); const parts: Buffer[] = []; if (options.openingMagic !== null) - parts.push(options.openingMagic ?? DENO_SECTION_MAGIC); + parts.push(options.openingMagic ?? DENO_FIXTURE_SECTION_MAGIC); parts.push( u64(options.metadataLength ?? metadataBytes.length), metadataBytes, @@ -138,7 +140,7 @@ export const buildDenoSection = (options: DenoSectionFixture = {}): Buffer => { } parts.push(u64(vfsBytes.length), vfsBytes, u64(contents.length), contents); if (options.closingMagic !== null) - parts.push(options.closingMagic ?? DENO_SECTION_MAGIC); + parts.push(options.closingMagic ?? DENO_FIXTURE_SECTION_MAGIC); if (options.trailing !== undefined) parts.push(options.trailing); return Buffer.concat(parts); }; diff --git a/src/artifacts/deno/DenoStandaloneArtifactReader.test.ts b/src/artifacts/deno/DenoStandaloneArtifactReader.test.ts index d125d1c349..b363fac09d 100644 --- a/src/artifacts/deno/DenoStandaloneArtifactReader.test.ts +++ b/src/artifacts/deno/DenoStandaloneArtifactReader.test.ts @@ -19,13 +19,13 @@ import { artifactFormatDescriptor } from "../artifactFormats.js"; import { ArtifactPathRegistry } from "../ArtifactPaths.js"; import { DenoStandaloneArtifactReader } from "../DenoStandaloneArtifactReader.js"; import { cArchiveExecutable } from "../pyinstaller/CArchive.fixture.js"; -import { DENO_SECTION_MAGIC } from "./DenoSectionLayout.js"; import { locateDenoStandalone, MAX_DENO_INTEL_TRAILER_SENTINELS, } from "./DenoStandaloneLocation.js"; import { buildDenoSection, + DENO_FIXTURE_SECTION_MAGIC, defaultDenoMetadata, denoElf, denoIntelTrailer, @@ -249,7 +249,7 @@ it("fails closed when a named section is truncated or lacks the opening marker", await expect( inventoryOf( "truncated", - denoMachO(Buffer.concat([DENO_SECTION_MAGIC, Buffer.alloc(4)])), + denoMachO(Buffer.concat([DENO_FIXTURE_SECTION_MAGIC, Buffer.alloc(4)])), ), ).rejects.toMatchObject({ reason: "format" }); await expect( diff --git a/src/artifacts/inventory/ArtifactGraphConstruction.ts b/src/artifacts/inventory/ArtifactGraphConstruction.ts index 5d6569eacd..2b1228e431 100644 --- a/src/artifacts/inventory/ArtifactGraphConstruction.ts +++ b/src/artifacts/inventory/ArtifactGraphConstruction.ts @@ -560,7 +560,7 @@ export const classifyArtifactPath = ( export const classifyArtifactContent = ( path: string, evidence: ArtifactClassificationEvidence, - fileSize = evidence.prefix.length, + fileSize: number, ): { readonly kind: ArtifactOccurrence["artifact_kind"]; readonly format: ArtifactOccurrence["artifact_format"]; @@ -620,7 +620,7 @@ const artifactRoleForFormat = ( /** Classify storage bytes independently of path names, roles, and permissions. */ export const classifyArtifactBytes = ( evidence: ArtifactClassificationEvidence, - fileSize = evidence.prefix.length, + fileSize: number, ): ArtifactNode["format"] => { const { prefix, suffix, mzWindowsHeaderSignature } = evidence; // A header-only region has the signature but not the declared cartridge diff --git a/src/artifacts/nodeSea/NodeSea.fixture.ts b/src/artifacts/nodeSea/NodeSea.fixture.ts index 0cbf8636a7..d21990d99b 100644 --- a/src/artifacts/nodeSea/NodeSea.fixture.ts +++ b/src/artifacts/nodeSea/NodeSea.fixture.ts @@ -1,7 +1,8 @@ import { elfNote, elfNoteFixture } from "../../native/elf/ElfNotes.fixture.js"; import { peResourceFixture } from "../../native/pe/PeResources.fixture.js"; -import { NODE_SEA_MAGIC } from "./NodeSeaBlob.js"; +/** Node 24 SEA blob magic: https://github.com/nodejs/node/blob/v24.12.0/src/node_sea.h */ +export const NODE_SEA_FIXTURE_MAGIC = 0x143da20; /** Official Node `NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2` hash. */ export const NODE_SEA_FUSE_HASH = "fce680ab2cc467b6e072b8b5df1996b2"; @@ -62,7 +63,7 @@ export const buildNodeSeaBlob = ( if (assets.length > 0) flags |= 1 << 3; if (execArgv.length > 0) flags |= 1 << 4; const header = Buffer.alloc(9); - header.writeUInt32LE(NODE_SEA_MAGIC, 0); + header.writeUInt32LE(NODE_SEA_FIXTURE_MAGIC, 0); header.writeUInt32LE(flags, 4); header[8] = options.execArgvExtension ?? 1; const parts: Buffer[] = [header, sized(codePath), sized(body)]; diff --git a/src/artifacts/nodeSea/NodeSeaBlob.test.ts b/src/artifacts/nodeSea/NodeSeaBlob.test.ts index 3e76582e20..ed076fa39d 100644 --- a/src/artifacts/nodeSea/NodeSeaBlob.test.ts +++ b/src/artifacts/nodeSea/NodeSeaBlob.test.ts @@ -3,16 +3,12 @@ import { setImmediate } from "node:timers/promises"; import { expect, it } from "vitest"; import { DEFAULT_NODE_SEA_READER_LIMITS } from "../../domain/artifactReaderLimits.js"; -import { - NODE_SEA_MAGIC, - decodeNodeSeaBlob, - observeNodeSeaFuseMatches, -} from "./NodeSeaBlob.js"; -import { buildNodeSeaBlob } from "./NodeSea.fixture.js"; +import { decodeNodeSeaBlob, observeNodeSeaFuseMatches } from "./NodeSeaBlob.js"; +import { buildNodeSeaBlob, NODE_SEA_FIXTURE_MAGIC } from "./NodeSea.fixture.js"; const header = (flags: number, extension = 1): Buffer => { const bytes = Buffer.alloc(9); - bytes.writeUInt32LE(NODE_SEA_MAGIC, 0); + bytes.writeUInt32LE(NODE_SEA_FIXTURE_MAGIC, 0); bytes.writeUInt32LE(flags, 4); bytes[8] = extension; return bytes; diff --git a/src/artifacts/nodeSea/NodeSeaBlob.ts b/src/artifacts/nodeSea/NodeSeaBlob.ts index 7a9c92eba2..27127fe105 100644 --- a/src/artifacts/nodeSea/NodeSeaBlob.ts +++ b/src/artifacts/nodeSea/NodeSeaBlob.ts @@ -9,7 +9,7 @@ import { import { artifactReaderLimitFailure } from "../ArtifactReaderLimits.js"; /** Node `SeaBlob::kMagic` from `src/node_sea.h` (v24). */ -export const NODE_SEA_MAGIC = 0x143da20; +const NODE_SEA_MAGIC = 0x143da20; const FLAG_DISABLE_EXPERIMENTAL_SEA_WARNING = 1 << 0; const FLAG_USE_SNAPSHOT = 1 << 1; diff --git a/src/artifacts/pyinstaller/CArchive.fixture.ts b/src/artifacts/pyinstaller/CArchive.fixture.ts index 481eb82cf1..7f566316a4 100644 --- a/src/artifacts/pyinstaller/CArchive.fixture.ts +++ b/src/artifacts/pyinstaller/CArchive.fixture.ts @@ -6,7 +6,7 @@ import { } from "../../python/PythonPyc.fixture.js"; // PyInstaller CArchiveWriter's on-disk cookie signature, independent of REA's reader. -const CARCHIVE_COOKIE_MAGIC = Buffer.from("4d45490c0b0a0b0e", "hex"); +export const CARCHIVE_COOKIE_MAGIC = Buffer.from("4d45490c0b0a0b0e", "hex"); /** Final CPython 3.9 bytecode magic, little-endian with the CR LF trailer. */ export const PYTHON_39_MAGIC = Buffer.from("610d0d0a", "hex"); diff --git a/src/artifacts/pyinstaller/CArchiveLayout.ts b/src/artifacts/pyinstaller/CArchiveLayout.ts index eb77c4b08a..0ae82cbc22 100644 --- a/src/artifacts/pyinstaller/CArchiveLayout.ts +++ b/src/artifacts/pyinstaller/CArchiveLayout.ts @@ -8,7 +8,7 @@ import { } from "./PyInstallerFailure.js"; /** Magic at the start of a CArchive cookie. */ -export const PYINSTALLER_COOKIE_MAGIC = Buffer.from("MEI\x0c\x0b\x0a\x0b\x0e"); +const PYINSTALLER_COOKIE_MAGIC = Buffer.from("MEI\x0c\x0b\x0a\x0b\x0e"); /** PyInstaller 2.1+ cookie size, including the 64-byte library name. */ export const PYINSTALLER_COOKIE_V21_BYTES = 88; diff --git a/src/artifacts/pyinstaller/PyInstallerAdmission.test.ts b/src/artifacts/pyinstaller/PyInstallerAdmission.test.ts index 2971fa992f..75f2161c1d 100644 --- a/src/artifacts/pyinstaller/PyInstallerAdmission.test.ts +++ b/src/artifacts/pyinstaller/PyInstallerAdmission.test.ts @@ -9,7 +9,10 @@ import { ArtifactResourceScope } from "../ArtifactResourceScope.js"; import { inventoryArtifact } from "../inventory/ArtifactInventory.js"; import { classifyAndHashRootForInventory } from "../inventory/classify.js"; import { PyInstallerArtifactReader } from "../PyInstallerArtifactReader.js"; -import { cArchiveExecutable } from "./CArchive.fixture.js"; +import { + CARCHIVE_COOKIE_MAGIC, + cArchiveExecutable, +} from "./CArchive.fixture.js"; import * as layout from "./CArchiveLayout.js"; const fixture = () => @@ -248,7 +251,7 @@ it("retains rejected magic candidates without classifying their native carrier a path, Buffer.concat([ fixture().subarray(0, 64), - layout.PYINSTALLER_COOKIE_MAGIC, + CARCHIVE_COOKIE_MAGIC, Buffer.alloc(88), ]), ); @@ -288,11 +291,7 @@ it("continues past an invalid trailing magic copy to inventory the valid cookie" const path = join(root, "app"); await writeFile( path, - Buffer.concat([ - fixture(), - layout.PYINSTALLER_COOKIE_MAGIC, - Buffer.alloc(88), - ]), + Buffer.concat([fixture(), CARCHIVE_COOKIE_MAGIC, Buffer.alloc(88)]), ); const resourceScope = new ArtifactResourceScope(); try { diff --git a/src/browser/CdpConnection.ts b/src/browser/CdpConnection.ts index 8ab66d367e..4ad285f2ea 100644 --- a/src/browser/CdpConnection.ts +++ b/src/browser/CdpConnection.ts @@ -614,8 +614,8 @@ const isRecord = (value: unknown): value is Record => const waitForOpen = async ( socket: WebSocket, operation: BrowserObservationOperation, - signal?: AbortSignal, - commandTimeoutMs: number = DEFAULT_CDP_COMMAND_TIMEOUT_MS, + signal: AbortSignal | undefined, + handshakeTimeoutMs: number, ): Promise => await new Promise((resolve, reject) => { const onOpen = (): void => { @@ -651,7 +651,7 @@ const waitForOpen = async ( }; const boundedTimeoutMs = Math.min( MAX_BROWSER_DEADLINE_MS, - commandTimeoutMs, + handshakeTimeoutMs, ); const timer = setTimeout(() => { terminateSocket(socket); diff --git a/src/browser/CdpNetworkEventFacts.ts b/src/browser/CdpNetworkEventFacts.ts index ae7fa548d3..266301ad45 100644 --- a/src/browser/CdpNetworkEventFacts.ts +++ b/src/browser/CdpNetworkEventFacts.ts @@ -43,7 +43,7 @@ export interface CdpResponseFacts { readonly encodedDataLength: number | undefined; readonly headers: unknown; } -export const readCdpResponseFacts = ( +const readCdpResponseFacts = ( reported: unknown, ): CdpResponseFacts | undefined => { const source = recordValue(reported); diff --git a/src/browser/CdpSafeMetadata.test.ts b/src/browser/CdpSafeMetadata.test.ts index b08df963e3..e9fb87b5f7 100644 --- a/src/browser/CdpSafeMetadata.test.ts +++ b/src/browser/CdpSafeMetadata.test.ts @@ -2,7 +2,7 @@ import { MIMEType } from "node:util"; import { describe, expect, it } from "vitest"; import { safeResponseMetadata as projectResponseMetadata } from "./CdpSafeMetadata.js"; -import { readCdpResponseFacts } from "./CdpNetworkEventFacts.js"; +import { readCdpResponseReceived } from "./CdpNetworkEventFacts.js"; import type { UnknownRecord } from "./CdpCaptureValues.js"; /** Admit each raw response fixture through the production field owner. */ @@ -12,7 +12,7 @@ const safeResponseMetadata = ( response: UnknownRecord, allowedOrigins: ReadonlySet, ) => { - const facts = readCdpResponseFacts(response); + const facts = readCdpResponseReceived({ requestId, response }).response; if (facts === undefined) throw new Error("Expected a response fixture record"); return projectResponseMetadata(requestId, url, facts, allowedOrigins); diff --git a/src/browser/PlaywrightElectronActiveFuses.ts b/src/browser/PlaywrightElectronActiveFuses.ts index 3bf67773e5..2e656856b6 100644 --- a/src/browser/PlaywrightElectronActiveFuses.ts +++ b/src/browser/PlaywrightElectronActiveFuses.ts @@ -15,7 +15,6 @@ export interface InspectArgumentsPreflightOptions { readonly signal?: AbortSignal; readonly resources?: ArtifactResourceScope; readonly readFuseImage?: typeof readElectronFuseImage; - readonly resolveFuseTarget?: typeof resolveElectronFuseTarget; } const appBundleDirectoryOf = (executablePath: string): string | null => { @@ -47,13 +46,15 @@ export const assertNodeCliInspectArgumentsSupported = async ( ): Promise => { assertArtifactReadActive(options?.signal); const reader = options?.readFuseImage ?? readElectronFuseImage; - const resolver = options?.resolveFuseTarget ?? resolveElectronFuseTarget; try { let targetPath = executablePath; const bundleDir = appBundleDirectoryOf(executablePath); if (bundleDir !== null) { try { - const resolved = await resolver(bundleDir, options?.signal); + const resolved = await resolveElectronFuseTarget( + bundleDir, + options?.signal, + ); targetPath = resolved.file.path; } catch (cause: unknown) { if (options?.signal?.aborted) throw cause; diff --git a/src/browser/WebSourceMapFetcher.test.ts b/src/browser/WebSourceMapFetcher.test.ts index a426809f31..5a92142691 100644 --- a/src/browser/WebSourceMapFetcher.test.ts +++ b/src/browser/WebSourceMapFetcher.test.ts @@ -191,9 +191,13 @@ it("retains a fetch host that ignores an item deadline until it settles", async }); const pending = fetchWebSourceMaps( [request], - { ...input, allowed_origins: [new URL(request.fetchUrl).origin] }, + { + ...input, + allowed_origins: [new URL(request.fetchUrl).origin], + source_map_fetch_timeout_ms: 50, + }, undefined, - { fetch: async () => fetching, timeoutMs: 50 }, + { fetch: async () => fetching }, ); const result = await pending; if (result.ok) throw new Error("Expected pending-fetch cleanup"); @@ -365,9 +369,10 @@ it.each([ { ...input, allowed_origins: [new URL(request.fetchUrl).origin], + source_map_limits: { fetch_bytes: 1 }, }, controller.signal, - { fetch: async () => new Response(body), maxResponseBytes: 1 }, + { fetch: async () => new Response(body) }, ); try { await reading; @@ -534,7 +539,11 @@ it.each([null, false])( try { const pending = fetchWebSourceMaps( [{ ...request, declaredUrl: url, fetchUrl: url }], - { ...input, allowed_origins: [new URL(url).origin] }, + { + ...input, + allowed_origins: [new URL(url).origin], + source_map_fetch_timeout_ms: 10_000, + }, controller.signal, { fetch: async (...args) => { @@ -542,7 +551,6 @@ it.each([null, false])( delivered(); return response; }, - timeoutMs: 10_000, }, ); await responseReceived; @@ -604,9 +612,13 @@ it("keeps an actual stalled HTTP header deadline local to its map", async () => try { const result = await fetchWebSourceMaps( [{ ...request, declaredUrl: url, fetchUrl: url }], - { ...input, allowed_origins: [new URL(url).origin] }, + { + ...input, + allowed_origins: [new URL(url).origin], + source_map_fetch_timeout_ms: 1000, + }, undefined, - { fetch, timeoutMs: 1000 }, + { fetch }, ); expect(result).toMatchObject({ ok: true, @@ -641,9 +653,13 @@ it("keeps an actual stalled HTTP response deadline local to its map", async () = try { const result = await fetchWebSourceMaps( [{ ...request, declaredUrl: url, fetchUrl: url }], - { ...input, allowed_origins: [new URL(url).origin] }, + { + ...input, + allowed_origins: [new URL(url).origin], + source_map_fetch_timeout_ms: 1000, + }, undefined, - { fetch, timeoutMs: 1000 }, + { fetch }, ); expect(result).toMatchObject({ ok: true, @@ -674,7 +690,11 @@ it.each([302, 503])( }).tee(); const pending = fetchWebSourceMaps( [request], - { ...input, allowed_origins: [new URL(request.fetchUrl).origin] }, + { + ...input, + allowed_origins: [new URL(request.fetchUrl).origin], + source_map_fetch_timeout_ms: 100, + }, undefined, { fetch: async () => @@ -682,7 +702,6 @@ it.each([302, 503])( status, ...(status === 302 ? { headers: { location: "/next" } } : {}), }), - timeoutMs: 100, }, ); let timer: ReturnType | undefined; @@ -719,7 +738,11 @@ it("retains an admitted response when the deadline expires before body reading", }).tee(); const pending = fetchWebSourceMaps( [request], - { ...input, allowed_origins: [new URL(request.fetchUrl).origin] }, + { + ...input, + allowed_origins: [new URL(request.fetchUrl).origin], + source_map_fetch_timeout_ms: 50, + }, undefined, { fetch: async () => { @@ -727,7 +750,6 @@ it("retains an admitted response when the deadline expires before body reading", Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 150); return new Response(body); }, - timeoutMs: 50, }, ); try { @@ -757,6 +779,7 @@ it("retains an oversized redirect identity's pending response cleanup", async () ...input, allowed_origins: [new URL(request.fetchUrl).origin], source_map_limits: { output_bytes: 4000 }, + source_map_fetch_timeout_ms: 100, }, undefined, { @@ -764,7 +787,6 @@ it("retains an oversized redirect identity's pending response cleanup", async () String(url) === request.fetchUrl ? new Response(null, { status: 302, headers: { location: finalUrl } }) : new Response(body), - timeoutMs: 100, }, ); try { diff --git a/src/browser/WebSourceMapFetcher.ts b/src/browser/WebSourceMapFetcher.ts index 6e42f7e2a4..c4e855dadb 100644 --- a/src/browser/WebSourceMapFetcher.ts +++ b/src/browser/WebSourceMapFetcher.ts @@ -60,10 +60,6 @@ export interface WebSourceMapRequest { interface SourceMapFetchHost { readonly fetch: typeof fetch; - /** Fetch-operation deadline; injectable so boundary regressions stay fast. */ - readonly timeoutMs?: number; - /** Maximum response bytes retained across this operation. */ - readonly maxResponseBytes?: number; } // Source maps are normally fetched as a small part of a larger inspection. @@ -125,12 +121,10 @@ export const fetchWebSourceMaps = async ( const limits = selectedSourceMapLimits(input.source_map_limits); const items: WebSourceMapItem[] = []; const operationController = new AbortController(); - const fetchTimeoutMs = - host.timeoutMs ?? - selectedBrowserDeadlineMs( - input.source_map_fetch_timeout_ms, - SOURCE_MAP_FETCH_TIMEOUT_MS, - ); + const fetchTimeoutMs = selectedBrowserDeadlineMs( + input.source_map_fetch_timeout_ms, + SOURCE_MAP_FETCH_TIMEOUT_MS, + ); const fetchDeadlineLimitation = sourceMapFetchDeadlineLimitation(fetchTimeoutMs); const timeout = setTimeout( @@ -695,7 +689,7 @@ const fetchOne = async ( const body = await readBoundedText( response, budget, - host.maxResponseBytes ?? budget.decodedRecords.limits.fetchBytes, + budget.decodedRecords.limits.fetchBytes, signal, fetchedUrl, ); diff --git a/src/browser/network/PlaywrightScenarioNetwork.test.ts b/src/browser/network/PlaywrightScenarioNetwork.test.ts index 83fae0005f..d570e43fe9 100644 --- a/src/browser/network/PlaywrightScenarioNetwork.test.ts +++ b/src/browser/network/PlaywrightScenarioNetwork.test.ts @@ -125,7 +125,6 @@ const harness = ( selection: selected, limits: scenario.capture.limits, secrets, - readTimeoutMs: 50, ...(signal === undefined ? {} : { signal }), emit: (event) => { const sequence = events.length + 1; @@ -733,7 +732,7 @@ it("starts each queued body's timeout when its read begins", async () => { const response = new ResponseFixture(request, "unused"); response.read = () => new Promise((resolve) => { - setTimeout(() => resolve(Buffer.from("captured")), 40); + setTimeout(() => resolve(Buffer.from("captured")), 4_000); }); collector.request(request); collector.response(response); @@ -741,8 +740,8 @@ it("starts each queued body's timeout when its read begins", async () => { } const finishing = collector.finish(); - await vi.advanceTimersByTimeAsync(40); - await vi.advanceTimersByTimeAsync(40); + await vi.advanceTimersByTimeAsync(4_000); + await vi.advanceTimersByTimeAsync(4_000); await finishing; const content = events.filter( @@ -817,7 +816,7 @@ it("bounds hung completed-body reads and rejects late mutation after finalizatio collector.response(response); collector.finished(request); const finishing = collector.finish(); - await vi.advanceTimersByTimeAsync(51); + await vi.advanceTimersByTimeAsync(5_001); await finishing; const frozen = JSON.stringify(events); expect(events).toContainEqual( diff --git a/src/browser/network/PlaywrightScenarioNetwork.ts b/src/browser/network/PlaywrightScenarioNetwork.ts index 431b6d9d2a..4c39ac47dd 100644 --- a/src/browser/network/PlaywrightScenarioNetwork.ts +++ b/src/browser/network/PlaywrightScenarioNetwork.ts @@ -125,8 +125,6 @@ interface NetworkCaptureOptions { reason: BrowserScenarioEventBudgetReason, ) => void; readonly signal?: AbortSignal; - /** Production uses 5s; a fixture can supply its own bounded-read clock. */ - readonly readTimeoutMs?: number; } /** Captures correlated network phases and drains selected content before browser cleanup. */ @@ -156,7 +154,7 @@ export class PlaywrightScenarioNetwork { constructor(private readonly options: NetworkCaptureOptions) { this.limits = options.limits; this.readOptions = { - timeoutMs: options.readTimeoutMs ?? PLAYWRIGHT_NETWORK_READ_TIMEOUT_MS, + timeoutMs: PLAYWRIGHT_NETWORK_READ_TIMEOUT_MS, maxBodyBytes: this.limits.max_network_body_bytes, maxBodyAllocationBytes: this.limits.max_network_body_allocation_bytes, maxHeaderAllocationBytes: this.limits.max_network_header_allocation_bytes, diff --git a/src/config/analysisSettingAdmission.test.ts b/src/config/analysisSettingAdmission.test.ts new file mode 100644 index 0000000000..36a538727a --- /dev/null +++ b/src/config/analysisSettingAdmission.test.ts @@ -0,0 +1,15 @@ +import { expect, it } from "vitest"; + +import { ANALYSIS_SETTINGS } from "../domain/analysisSettingDefinitions.js"; +import { parseConfig } from "./parseConfig.js"; +import { parseEnvironment } from "./environment.js"; + +it("admits invalid runtime-owned launch seeds at startup", () => { + for (const definition of Object.values(ANALYSIS_SETTINGS)) { + if (definition.updateMode !== "runtime" || !("environment" in definition)) + continue; + const environment = { [definition.environment]: "not-a-number" }; + expect(parseEnvironment(environment).ok, definition.environment).toBe(true); + expect(parseConfig(environment).ok, definition.environment).toBe(true); + } +}); diff --git a/src/config/bridgeMaxRequestBytes.ts b/src/config/bridgeMaxRequestBytes.ts index 4116a1276a..91c089eeb8 100644 --- a/src/config/bridgeMaxRequestBytes.ts +++ b/src/config/bridgeMaxRequestBytes.ts @@ -1,7 +1,6 @@ import { positiveDecimalIntegerSchema } from "./positiveDecimalInteger.js"; import { analysisSettingLimit } from "../domain/analysisSettingDefinitions.js"; -import { currentAnalysisSettingOverride } from "../domain/analysisOperationSnapshot.js"; import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js"; import { ConfigurationError } from "../domain/configurationErrors.js"; import { err, ok, type Result } from "../domain/result.js"; @@ -95,24 +94,6 @@ export const parseBridgeMaxRequestBytes = ( ); }; -/** - * Resolve the ceiling from a composition root's selected environment. An - * invalid setting is refused, not ignored. Callers that already resolved it - * at startup pass that value through instead of reading the environment again. - */ -export const resolveBridgeMaxRequestBytes = ( - environment: Readonly>, -): BridgeMaxRequestBytes => { - const override = currentAnalysisSettingOverride("bridge.max_request_bytes"); - if (override !== undefined) - return { maximumBytes: override, source: "runtime" }; - const parsed = parseBridgeMaxRequestBytes( - environment[BRIDGE_MAX_REQUEST_BYTES_SETTING], - ); - if (!parsed.ok) throw parsed.error; - return parsed.value; -}; - /** How to read and change the effective bridge request ceiling. */ export const bridgeRequestLimit = (limit: BridgeMaxRequestBytes) => analysisSettingLimit( diff --git a/src/config/debuggerSessionLimits.test.ts b/src/config/debuggerSessionLimits.test.ts index 747e22e37c..7cbdd2a06b 100644 --- a/src/config/debuggerSessionLimits.test.ts +++ b/src/config/debuggerSessionLimits.test.ts @@ -23,7 +23,6 @@ import { DEBUGGER_MAX_PENDING_COMMANDS_SETTING, DEBUGGER_MAX_SESSIONS_SETTING, parseDebuggerSessionLimits, - resolveDebuggerSessionLimits, } from "./debuggerSessionLimits.js"; it("uses the shared defaults when the settings are unset", () => { @@ -302,9 +301,12 @@ it("defers an invalid GDB deadline until a command reads it", () => { code: "configuration_invalid", details: { settings: parsed.error.settings }, }); - expect( - resolveDebuggerSessionLimits({ - [DEBUGGER_COMMAND_DEADLINE_SETTING]: "1500", - }).commandDeadline, - ).toMatchObject({ value: 1500, source: "setting" }); + const valid = parseDebuggerSessionLimits({ + [DEBUGGER_COMMAND_DEADLINE_SETTING]: "1500", + }); + if (!valid.ok) throw valid.error; + expect(valid.value.commandDeadline).toMatchObject({ + value: 1500, + source: "setting", + }); }); diff --git a/src/config/debuggerSessionLimits.ts b/src/config/debuggerSessionLimits.ts index 1c8272716f..b844eef219 100644 --- a/src/config/debuggerSessionLimits.ts +++ b/src/config/debuggerSessionLimits.ts @@ -270,15 +270,6 @@ export const debuggerConstructionLimits = ( return again.value; }; -/** Refuse an invalid launch cap instead of falling back to the default. */ -export const resolveDebuggerSessionLimits = ( - environment: Readonly>, -): DebuggerSessionLimits => { - const parsed = parseDebuggerSessionLimits(environment); - if (!parsed.ok) throw parsed.error; - return parsed.value; -}; - /** * Fixed budget for Rizin cleanup readback and GDB startup or detach MI. * This is not `REA_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES`. A caller cap below diff --git a/src/config/hopperCallerScanBudget.test.ts b/src/config/hopperCallerScanBudget.test.ts index b1823c0db5..338d5c3561 100644 --- a/src/config/hopperCallerScanBudget.test.ts +++ b/src/config/hopperCallerScanBudget.test.ts @@ -16,7 +16,6 @@ import { ConfigurationError } from "../domain/configurationErrors.js"; import { runWithAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js"; import { HOPPER_CALLER_SCAN_BUDGET_SETTING, - hopperCallerScanLimit, hopperCallerScanRecovery, } from "./hopperCallerScanBudget.js"; @@ -79,15 +78,19 @@ it("explains the time budget and that fixed byte allowances have no override", ( const parsed = parseConfig({ [HOPPER_CALLER_SCAN_BUDGET_SETTING]: "250" }); if (!parsed.ok) throw parsed.error; const budget = parsed.value.hopperCallerScanBudget; - expect(hopperCallerScanLimit(budget)).toMatchObject({ - setting: "hopper.caller_scan_budget_ms", - effective_value: 250, - source: "setting", - minimum: MINIMUM_HOPPER_CALLER_SCAN_BUDGET_MS, - maximum: MAX_NODE_TIMER_DELAY_MS, - update_mode: "runtime", - pending_restart: false, - }); + const deadline = hopperCallerScanRecovery( + facts({ stop: { code: "work_budget", budget_ms: 250 } }), + budget, + ); + expect(deadline).toContain( + "hopper.caller_scan_budget_ms is 250 ms (source setting", + ); + expect(deadline).toContain( + `floor ${String(MINIMUM_HOPPER_CALLER_SCAN_BUDGET_MS)} ms`, + ); + expect(deadline).toContain(`maximum ${String(MAX_NODE_TIMER_DELAY_MS)} ms`); + expect(deadline).toContain("update_analysis_settings"); + expect(deadline).toContain(HOPPER_CALLER_SCAN_BUDGET_SETTING); expect(DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS).toBe(2000); const page = hopperCallerScanRecovery( facts({ diff --git a/src/config/hopperCallerScanBudget.ts b/src/config/hopperCallerScanBudget.ts index 2f35abeebf..115db0301b 100644 --- a/src/config/hopperCallerScanBudget.ts +++ b/src/config/hopperCallerScanBudget.ts @@ -56,7 +56,7 @@ export const hopperCallerScanBudgetForMilliseconds = ( const fixedAllowanceRecovery = `This release has no runtime override for the ${String(HOPPER_CALLER_PAGE_ALLOWANCE_BYTES)} byte instruction, native-caller, and page allowances or the ${String(HOPPER_CALLER_SCAN_RETENTION_BYTES)} byte retention budget. hopper.caller_scan_budget_ms does not raise them.`; /** Structured facts for the scan time budget. The sentence is display text. */ -export const hopperCallerScanLimit = (budget: HopperCallerScanBudget) => +const hopperCallerScanLimit = (budget: HopperCallerScanBudget) => analysisSettingLimit( "hopper.caller_scan_budget_ms", budget.milliseconds, diff --git a/src/contracts/analysisDetail.test.ts b/src/contracts/analysisDetail.test.ts index 8f12296993..fdca656e78 100644 --- a/src/contracts/analysisDetail.test.ts +++ b/src/contracts/analysisDetail.test.ts @@ -6,8 +6,8 @@ import { binaryLayoutSummaryViewResultSchema, javascriptApplicationSummaryViewResultSchema, projectAnalysisView, - analysisViewRequestSchema, } from "../domain/analysisView/analysisView.js"; +import { analysisViewRequestSchema } from "../domain/analysisView/analysisViewRequest.js"; import { analysisViewCaptureEvidence, analysisViewJavaScriptEvidence, diff --git a/src/contracts/compactInputSchema.test.ts b/src/contracts/compactInputSchema.test.ts index 8c389aa078..c03e2507c3 100644 --- a/src/contracts/compactInputSchema.test.ts +++ b/src/contracts/compactInputSchema.test.ts @@ -754,6 +754,19 @@ it("includes restored root identity in the compact byte budget", () => { expect(result.schema.properties).toEqual({ value: { type: "number" } }); }); +it("bounds an oversized root description while retaining an object input", () => { + const budget = 13 * 1024; + const result = compactAdvertisedInputSchema( + { type: "object", description: "x".repeat(budget + 1), properties: {} }, + budget, + ); + expect(result.presentation).toBe("reduced"); + expect(serializedBytes(result.schema)).toBeLessThanOrEqual(budget); + const validate = new Ajv2020({ strict: false }).compile(result.schema); + expect(validate({ value: "allowed" })).toBe(true); + expect(validate("not an object")).toBe(false); +}); + // An untyped primitive enum is gateway-incompatible, so these schemas take // the reduced projection independently of the budget. const reducedPresentation = ( diff --git a/src/contracts/compactInputSchema.ts b/src/contracts/compactInputSchema.ts index d45329674f..d9bdafa8ad 100644 --- a/src/contracts/compactInputSchema.ts +++ b/src/contracts/compactInputSchema.ts @@ -87,13 +87,7 @@ export const compactAdvertisedInputSchema = ( // an honest, budget-fitting shape instead of an oversized one. return { schema: reduceToShallowProperties( - { - type: "object", - description: - (typeof schema.description === "string" - ? `${schema.description} ` - : "") + INPUT_REDUCED_ANNOTATION, - }, + { type: "object" }, INPUT_REDUCED_ANNOTATION, ), presentation: "reduced", diff --git a/src/contracts/toolSchemaMetadata.ts b/src/contracts/toolSchemaMetadata.ts index da21833dfb..021bc9c4a8 100644 --- a/src/contracts/toolSchemaMetadata.ts +++ b/src/contracts/toolSchemaMetadata.ts @@ -256,9 +256,7 @@ const hasRelativeSchemaReference = ( const withAdvertisedJsonSchema = ( canonical: Schema, io: "input" | "output", - advertise: (project: JsonSchemaProjection) => JsonSchemaProjection = ( - project, - ) => project, + advertise: (project: JsonSchemaProjection) => JsonSchemaProjection, ): Schema => { // Zod's input projection drops root metadata when a descendant transforms. // Preserve the parser and let the SDK own conversion of everything else. diff --git a/src/domain/analysisSettingAdmission.test.ts b/src/domain/analysisSettingAdmission.test.ts index 24d21779ab..679164f81e 100644 --- a/src/domain/analysisSettingAdmission.test.ts +++ b/src/domain/analysisSettingAdmission.test.ts @@ -1,24 +1,5 @@ -import { readFileSync } from "node:fs"; -import { join } from "node:path"; import { expect, it } from "vitest"; -import { - analysisSettingDefinition, - runtimeAnalysisEnvironmentNames, -} from "./analysisSettingDefinitions.js"; - -const source = (path: string) => - readFileSync(join(import.meta.dirname, path), "utf8"); - -it("does not validate runtime-owned launch seeds at startup", () => { - const startup = [ - source("../config/environment.ts"), - source("../config/debuggerSessionLimits.ts"), - ].join("\n"); - const mentioned = [...runtimeAnalysisEnvironmentNames()].filter( - (name) => startup.includes(`${name}:`) || startup.includes(`[${name}]`), - ); - expect(mentioned).toEqual([]); -}); +import { analysisSettingDefinition } from "./analysisSettingDefinitions.js"; it("advertises the operations that consume each migrated setting", () => { const operations = (id: Parameters[0]) => diff --git a/src/domain/analysisSettingDefinitions.ts b/src/domain/analysisSettingDefinitions.ts index 0bef39f77d..dac1d29d0d 100644 --- a/src/domain/analysisSettingDefinitions.ts +++ b/src/domain/analysisSettingDefinitions.ts @@ -559,16 +559,6 @@ export const ANALYSIS_SETTINGS = { operations: windowsChangeTimeOperations, }, } as const satisfies Record; -/** Launch seeds `update_analysis_settings` can replace. Startup must not reject them. */ -export const runtimeAnalysisEnvironmentNames = (): ReadonlySet => - new Set( - Object.values(ANALYSIS_SETTINGS).flatMap((definition) => - definition.updateMode === "runtime" && "environment" in definition - ? [definition.environment] - : [], - ), - ); - export const analysisSettingIds = Object.keys( ANALYSIS_SETTINGS, ) as AnalysisSettingId[]; diff --git a/src/domain/analysisView/analysisView.test.ts b/src/domain/analysisView/analysisView.test.ts index 8182a9330d..9a87346563 100644 --- a/src/domain/analysisView/analysisView.test.ts +++ b/src/domain/analysisView/analysisView.test.ts @@ -27,7 +27,6 @@ import { } from "../javascript/javascriptSemanticGraph.js"; import { sourceMapReference } from "../javascript/javascriptApplicationGraph.fixture.js"; import { createArtifactInspection } from "../artifactInspection.js"; -import { javascriptModulePaths } from "./javascriptApplicationView.js"; import { analysisViewResultSchema, inspectAnalysisViewInputSchema, @@ -793,9 +792,6 @@ it("projects JavaScript summary and preserves selected module observations", () selector: { path: "renderer.js" }, }); if (!item.ok) throw item.error; - const moduleNode = analysis.graph.nodes[0]; - if (moduleNode === undefined) throw new Error("missing module"); - expect(javascriptModulePaths(moduleNode)).toEqual(["renderer.js"]); if (item.value.kind !== "item") throw new Error("expected item view"); expect(item.value.item).toMatchObject({ path: "renderer.js", diff --git a/src/domain/analysisView/analysisView.ts b/src/domain/analysisView/analysisView.ts index 71f64171cb..0e8b37a8fe 100644 --- a/src/domain/analysisView/analysisView.ts +++ b/src/domain/analysisView/analysisView.ts @@ -101,7 +101,6 @@ import { projectIl2cppMetadataView } from "./il2cppMetadataView.js"; import type { AnalysisViewParent } from "./analysisViewParent.js"; import type { GroupingViewKind } from "./analysisViewShape.js"; import { - analysisViewRequestSchema, inspectAnalysisViewInputSchema, type AnalysisViewRequest, type InspectAnalysisViewInput, @@ -180,7 +179,6 @@ import { type UnsignedAnalysisView, } from "./analysisViewResult.js"; export { - analysisViewRequestSchema, inspectAnalysisViewInputSchema, type AnalysisViewRequest, type InspectAnalysisViewInput, diff --git a/src/domain/analysisView/androidPackageView.test.ts b/src/domain/analysisView/androidPackageView.test.ts index bbb7cac2cf..a82e5c65e3 100644 --- a/src/domain/analysisView/androidPackageView.test.ts +++ b/src/domain/analysisView/androidPackageView.test.ts @@ -3,12 +3,12 @@ import { createEvidence } from "../evidence.js"; import { projectAnalysisError } from "../analysisErrorProjection.js"; import type { JsonValue } from "../jsonValue.js"; import { - analysisViewRequestSchema, androidComponentsViewResultSchema, androidComponentSummaryViewResultSchema, projectAnalysisView, type AnalysisViewParent, } from "./analysisView.js"; +import { analysisViewRequestSchema } from "./analysisViewRequest.js"; const component = (name: string, extra: Record = {}) => ({ kind: "activity", diff --git a/src/domain/analysisView/batchDecompileView.ts b/src/domain/analysisView/batchDecompileView.ts index c4de4f8bc0..29ee452596 100644 --- a/src/domain/analysisView/batchDecompileView.ts +++ b/src/domain/analysisView/batchDecompileView.ts @@ -243,9 +243,7 @@ const summarizeBatch = (items: readonly BatchDecompileItem[]) => { }; }; -export const batchUnknowns = ( - items: readonly BatchDecompileItem[], -): string[] => { +const batchUnknowns = (items: readonly BatchDecompileItem[]): string[] => { const unknownIdentity = items.filter( ({ procedure }) => procedure.status === "unknown", ).length; diff --git a/src/domain/analysisView/environmentCoverageView.test.ts b/src/domain/analysisView/environmentCoverageView.test.ts index 99223be011..54f68dd646 100644 --- a/src/domain/analysisView/environmentCoverageView.test.ts +++ b/src/domain/analysisView/environmentCoverageView.test.ts @@ -11,11 +11,11 @@ import { } from "../../../tests/fixtures/browserScenarioCapture.js"; import type { Evidence } from "../evidence.js"; import { - analysisViewRequestSchema, projectAnalysisView, type AnalysisViewParent, type AnalysisViewResult, } from "./analysisView.js"; +import { analysisViewRequestSchema } from "./analysisViewRequest.js"; import { WITHHELD_WRITE_HINTS, summarizeEnvironmentCoverage, diff --git a/src/domain/analysisView/environmentManifestView.test.ts b/src/domain/analysisView/environmentManifestView.test.ts index 35931bc676..37b7767683 100644 --- a/src/domain/analysisView/environmentManifestView.test.ts +++ b/src/domain/analysisView/environmentManifestView.test.ts @@ -5,10 +5,10 @@ import { environmentManifestCaptureFixture, } from "../../../tests/fixtures/analysisView.js"; import { - analysisViewRequestSchema, projectAnalysisView, type AnalysisViewRequest, } from "./analysisView.js"; +import { analysisViewRequestSchema } from "./analysisViewRequest.js"; type ManifestView = Extract< AnalysisViewRequest, diff --git a/src/domain/analysisView/javascriptApplicationView.ts b/src/domain/analysisView/javascriptApplicationView.ts index a186bc6e79..c81904c833 100644 --- a/src/domain/analysisView/javascriptApplicationView.ts +++ b/src/domain/analysisView/javascriptApplicationView.ts @@ -54,9 +54,7 @@ const jsIncompatible = (detail: string): AnalysisError => ]); /** Every exact module path or original source-map reference retained by a node. */ -export const javascriptModulePaths = ( - node: ApplicationNode, -): readonly string[] => { +const javascriptModulePaths = (node: ApplicationNode): readonly string[] => { if (node.identity.strategy === "canonical-path") return [node.identity.path]; if (node.identity.strategy === "source-map-original") return [node.identity.original_source]; diff --git a/src/domain/analysisView/javascriptRoutesView.test.ts b/src/domain/analysisView/javascriptRoutesView.test.ts index fd3b5532bd..d972591d35 100644 --- a/src/domain/analysisView/javascriptRoutesView.test.ts +++ b/src/domain/analysisView/javascriptRoutesView.test.ts @@ -1,14 +1,14 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "../javascript/javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { completeSemanticSteps } from "../javascript/javascriptSemanticTraversal.js"; import { buildJavaScriptRouteInventorySteps } from "../javascript/javascriptRouteInventory.js"; import { freezeJsonSnapshot } from "../immutableJson.js"; import { projectAnalysisError } from "../analysisErrorProjection.js"; import { projectAnalysisView, - analysisViewRequestSchema, type AnalysisViewParent, } from "./analysisView.js"; +import { analysisViewRequestSchema } from "./analysisViewRequest.js"; const inventory = ( text = `createBrowserRouter([{path:'/reports',children:[{path:':id'},{path:'settings'}]}]);router.push('/login');`, diff --git a/src/domain/analysisView/nativeInventoryView.test.ts b/src/domain/analysisView/nativeInventoryView.test.ts index cf184fa8b9..ae07597581 100644 --- a/src/domain/analysisView/nativeInventoryView.test.ts +++ b/src/domain/analysisView/nativeInventoryView.test.ts @@ -2,10 +2,10 @@ import { afterEach, expect, it, vi } from "vitest"; import { freezeJsonSnapshot } from "../immutableJson.js"; import { nativeInventoryRowsSchemas } from "../native/nativeInventory.js"; import { - analysisViewRequestSchema, projectAnalysisView, type AnalysisViewParent, } from "./analysisView.js"; +import { analysisViewRequestSchema } from "./analysisViewRequest.js"; afterEach(() => vi.restoreAllMocks()); diff --git a/src/domain/analysisView/recordedArrayView.test.ts b/src/domain/analysisView/recordedArrayView.test.ts index e94b817549..5e49460d34 100644 --- a/src/domain/analysisView/recordedArrayView.test.ts +++ b/src/domain/analysisView/recordedArrayView.test.ts @@ -3,10 +3,8 @@ import { createEvidence } from "../evidence.js"; import { MAX_JSON_DEPTH, type JsonValue } from "../jsonValue.js"; import { projectAnalysisError } from "../analysisErrorProjection.js"; import type { AnalysisViewParent } from "./analysisViewParent.js"; -import { - analysisViewRequestSchema, - projectAnalysisView, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import { analysisViewRequestSchema } from "./analysisViewRequest.js"; const nested = (levels: number): JsonValue => { let value: JsonValue = "leaf"; diff --git a/src/domain/analysisView/webBundleView.test.ts b/src/domain/analysisView/webBundleView.test.ts index 782c657e1e..3c6ed6133e 100644 --- a/src/domain/analysisView/webBundleView.test.ts +++ b/src/domain/analysisView/webBundleView.test.ts @@ -3,10 +3,10 @@ import type { WebBundleAnalysis } from "../webBundleAnalysis.js"; import { AnalysisInputError } from "../analysisErrorCore.js"; import { projectAnalysisError } from "../analysisErrorProjection.js"; import { - analysisViewRequestSchema, projectAnalysisView, type AnalysisViewParent, } from "./analysisView.js"; +import { analysisViewRequestSchema } from "./analysisViewRequest.js"; const sampleAnalysis = (): WebBundleAnalysis => ({ capture: { diff --git a/src/domain/analysisView/webNetworkApiSurfaceView.test.ts b/src/domain/analysisView/webNetworkApiSurfaceView.test.ts index 74215d1e7f..45e7d3d071 100644 --- a/src/domain/analysisView/webNetworkApiSurfaceView.test.ts +++ b/src/domain/analysisView/webNetworkApiSurfaceView.test.ts @@ -3,10 +3,8 @@ import { analysisViewCaptureFixture, analysisViewCaptureEvidence, } from "../../../tests/fixtures/analysisView.js"; -import { - analysisViewRequestSchema, - projectAnalysisView, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import { analysisViewRequestSchema } from "./analysisViewRequest.js"; import { projectAnalysisError } from "../analysisErrorProjection.js"; const project = ( diff --git a/src/domain/analysisView/webNetworkCaptureView.test.ts b/src/domain/analysisView/webNetworkCaptureView.test.ts index f19fd0e09f..336a57d46b 100644 --- a/src/domain/analysisView/webNetworkCaptureView.test.ts +++ b/src/domain/analysisView/webNetworkCaptureView.test.ts @@ -3,10 +3,8 @@ import { createHash } from "node:crypto"; import { createEvidence } from "../evidence.js"; import { freezeJsonSnapshot } from "../immutableJson.js"; import { webNetworkCaptureSchema } from "../webNetworkCapture.js"; -import { - projectAnalysisView, - analysisViewRequestSchema, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import { analysisViewRequestSchema } from "./analysisViewRequest.js"; import { timedCaptureEvidence } from "../../../tests/fixtures/analysisView.js"; const capture = () => { diff --git a/src/domain/apktool/apktoolResourceAnalysis.test.ts b/src/domain/apktool/apktoolResourceAnalysis.test.ts index 7cad51191f..2e8c666197 100644 --- a/src/domain/apktool/apktoolResourceAnalysis.test.ts +++ b/src/domain/apktool/apktoolResourceAnalysis.test.ts @@ -1,7 +1,6 @@ import { expect, it } from "vitest"; import { androidLocaleDirectorySuffix, - androidLocaleQualifierSchema, apktoolInputSchemas, classifyAndroidStringsTable, DEFAULT_APKTOOL_DECODE_TIMEOUT_MS, @@ -77,7 +76,12 @@ it.each([ ])( "refuses suffixes outside the bounded locale selection contract: %s", (suffix) => { - expect(androidLocaleQualifierSchema.safeParse(suffix).success).toBe(false); + expect( + apktoolInputSchemas.decode_android_resources.safeParse({ + path: "/targets/app.apk", + locale: suffix, + }).success, + ).toBe(false); expect(classifyAndroidStringsTable(suffix)).toEqual({ kind: "other", suffix, diff --git a/src/domain/apktool/apktoolResourceAnalysis.ts b/src/domain/apktool/apktoolResourceAnalysis.ts index 36883574de..71f8a30b99 100644 --- a/src/domain/apktool/apktoolResourceAnalysis.ts +++ b/src/domain/apktool/apktoolResourceAnalysis.ts @@ -28,7 +28,7 @@ const ANDROID_NON_LOCALE_QUALIFIERS = new Set([ const MAX_ANDROID_LOCALE_SUFFIX = 2 + 3 + (1 + 4) + (1 + 3) + (1 + 8); /** Locale language tags admitted by Android resource table selection. */ -export const androidLocaleQualifierSchema = z +const androidLocaleQualifierSchema = z .string() .min(2) .max(MAX_ANDROID_LOCALE_SUFFIX) diff --git a/src/domain/browserScenarioDiff.ts b/src/domain/browserScenarioDiff.ts index 68462734c3..86c1811be4 100644 --- a/src/domain/browserScenarioDiff.ts +++ b/src/domain/browserScenarioDiff.ts @@ -521,7 +521,6 @@ interface CompareCapturedValuesInput { readonly afterStep: BrowserScenarioStep; readonly rules: readonly BrowserScenarioNormalizationRule[]; readonly project?: (value: Value) => unknown; - readonly order?: (normalized: unknown) => unknown; readonly beforeState?: BrowserScenarioEvidenceState; readonly afterState?: BrowserScenarioEvidenceState; readonly incomparableReason?: string | undefined; @@ -538,7 +537,6 @@ const compareCapturedValues = ( afterStep, rules, project = (value) => value, - order, } = input; const beforeState = input.beforeState ?? artifactEvidenceState(artifact, before, beforeStep); @@ -548,21 +546,11 @@ const compareCapturedValues = ( return { artifact, status: "not_compared", diff: null }; const beforeSha256 = beforeState === "captured" && before.state === "captured" - ? digestNormalizedScenarioValue( - project(before.value), - artifact, - rules, - order, - ) + ? digestNormalizedScenarioValue(project(before.value), artifact, rules) : null; const afterSha256 = afterState === "captured" && after.state === "captured" - ? digestNormalizedScenarioValue( - project(after.value), - artifact, - rules, - order, - ) + ? digestNormalizedScenarioValue(project(after.value), artifact, rules) : null; if ( beforeState !== "captured" || diff --git a/src/domain/browserScenarioNormalization.ts b/src/domain/browserScenarioNormalization.ts index ec26953947..5aa3f070b3 100644 --- a/src/domain/browserScenarioNormalization.ts +++ b/src/domain/browserScenarioNormalization.ts @@ -31,15 +31,14 @@ export const commitBrowserScenarioNormalization = ( }; }; -/** Digest one artifact after applying committed exact-literal rules, then any artifact ordering. */ +/** Digest one artifact after applying committed exact-literal rules. */ export const digestNormalizedScenarioValue = ( value: unknown, artifact: BrowserScenarioArtifactKind, rules: readonly BrowserScenarioNormalizationRule[], - order: (normalized: unknown) => unknown = (normalized) => normalized, ): string => digestCanonicalValue( - order(normalizeScenarioValue(value, artifact, rules)), + normalizeScenarioValue(value, artifact, rules), "Browser scenario", ); diff --git a/src/domain/desktop/appExport.test.ts b/src/domain/desktop/appExport.test.ts index 97c01c39e8..f7026d20c4 100644 --- a/src/domain/desktop/appExport.test.ts +++ b/src/domain/desktop/appExport.test.ts @@ -11,7 +11,6 @@ import { environmentName, launchCommand, launchDocumentLimitation, - launchDocumentNote, ownedPaths, renderAppClass, renderEnvToml, @@ -378,16 +377,19 @@ describe("README and class wording", () => { expect(command.join(" ")).toContain(`"$HOME"/'Documents/todo.txt'`); expect(bare).toContain(`\`${command.join(" ")}\``); - for (const opens of [seed, none]) { - const note = launchDocumentNote({ opens }); - expect(note).not.toBe(""); - expect(readme(opens).replace(note, "")).toBe(bare); - } + const seedNote = + "\n- At launch the app opens `~/Documents/todo.txt`, the seed's one document."; + const noneNote = + "\n- The app opens no document at launch, since the test gives this reason."; + expect(readme(seed)).toContain(seedNote); + expect(readme(none)).toContain(noneNote); + expect(readme(seed).replace(seedNote, "")).toBe(bare); + expect(readme(none).replace(noneNote, "")).toBe(bare); expect(readme(seed)).toContain("`~/Documents/todo.txt`"); expect(bare).not.toContain("~/Documents/todo.txt"); const limitation = launchDocumentLimitation(none); expect(limitation).toContain(none.reason); - expect(launchDocumentNote({ opens: none })).toContain(limitation); + expect(readme(none)).toContain(limitation); expect(readme({ kind: "captured" })).toBe(bare); for (const opens of [ seed, diff --git a/src/domain/desktop/appExport.ts b/src/domain/desktop/appExport.ts index 94df518516..e2b1505425 100644 --- a/src/domain/desktop/appExport.ts +++ b/src/domain/desktop/appExport.ts @@ -607,7 +607,7 @@ export const renderInstallScript = (file: ExportPackageFile): string => ].join("\n"); /** How the class and README say where the seed came from: made for the task, or the user's own data, kept local. */ -export const seedWording = ({ +const seedWording = ({ name, seedCheckpoint, syntheticSeed, @@ -678,7 +678,7 @@ window and dialogs; the result is read from those files. }`; /** The start of the limitation of a launch that opens no document; its reason follows. */ -export const NO_LAUNCH_DOCUMENT = "The app opens no document at launch"; +const NO_LAUNCH_DOCUMENT = "The app opens no document at launch"; /** The limitation of a launch that opens no document; none when it opens one. */ export const launchDocumentLimitation = ( @@ -689,7 +689,7 @@ export const launchDocumentLimitation = ( : undefined; /** The README line on what the launch opens; none when the captured command names its own file. */ -export const launchDocumentNote = ({ +const launchDocumentNote = ({ opens, }: Pick): string => opens.kind === "seed" diff --git a/src/domain/desktop/computerActions.test.ts b/src/domain/desktop/computerActions.test.ts index 208105867f..9d47f779de 100644 --- a/src/domain/desktop/computerActions.test.ts +++ b/src/domain/desktop/computerActions.test.ts @@ -1,31 +1,50 @@ import { describe, expect, it } from "vitest"; -import { - computerActionInputSchema, - driverCall, - w3cKeys, -} from "./computerActions.js"; +import { computerActionInputSchema, driverCall } from "./computerActions.js"; describe("computer actions", () => { it("translates model key spellings to W3C names and keeps W3C names and characters", () => { - expect(w3cKeys(["ctrl+s"])).toEqual(["Control", "s"]); - expect(w3cKeys(["cmd+Shift+z"])).toEqual(["Meta", "Shift", "z"]); - expect(w3cKeys(["Return", "esc", "pgdn", "f5", "space"])).toEqual([ - "Enter", - "Escape", - "PageDown", - "F5", - " ", - ]); - expect(w3cKeys(["Control", "ArrowUp", "+", "ctrl++"])).toEqual([ + const keys = [ + "ctrl+s", + "cmd+Shift+z", + "Return", + "esc", + "pgdn", + "f5", + "space", "Control", "ArrowUp", "+", - "Control", - "+", - ]); - // An unknown name passes through for the driver to refuse with its hint. - expect(w3cKeys(["Hyper"])).toEqual(["Hyper"]); + "ctrl++", + // An unknown name passes through for the driver to refuse with its hint. + "Hyper", + ]; + expect(driverCall("key", { keys })).toEqual({ + ok: true, + value: { + tool: "computer_key", + arguments: { + keys: [ + "Control", + "s", + "Meta", + "Shift", + "z", + "Enter", + "Escape", + "PageDown", + "F5", + " ", + "Control", + "ArrowUp", + "+", + "Control", + "+", + "Hyper", + ], + }, + }, + }); }); it("passes the driver's arguments through and translates key fields", () => { diff --git a/src/domain/desktop/computerActions.ts b/src/domain/desktop/computerActions.ts index d8042806cb..1d4cc04fb5 100644 --- a/src/domain/desktop/computerActions.ts +++ b/src/domain/desktop/computerActions.ts @@ -187,7 +187,7 @@ const w3cKey = (spelling: string): string => { }; /** W3C key names from W3C names or model spellings such as `ctrl+s`, which name a chord in order. */ -export const w3cKeys = (keys: readonly string[]): string[] => +const w3cKeys = (keys: readonly string[]): string[] => keys.flatMap((key) => ONE_CHARACTER.test(key) ? [key] : key.split(/\+(?!$)/u).map(w3cKey), ); diff --git a/src/domain/desktop/stateFormats.test.ts b/src/domain/desktop/stateFormats.test.ts index ab93c6c333..07c02f474e 100644 --- a/src/domain/desktop/stateFormats.test.ts +++ b/src/domain/desktop/stateFormats.test.ts @@ -7,10 +7,10 @@ import { describe, expect, it } from "vitest"; import { detectStateFormat, - jsonView, keyfileView, textSyntaxFormat, textView, + textualView, xmlView, } from "./stateFormats.js"; @@ -93,17 +93,23 @@ describe("detectStateFormat", () => { describe("jsonView", () => { it("refuses values a JavaScript number would round, or that nest too deeply", () => { - expect(jsonView('{"id":9007199254740993}')).toEqual({ + expect(textualView("json", bytes('{"id":9007199254740993}'))).toEqual({ ok: false, error: "a number in it cannot be read exactly, such as an integer above 2^53", }); - expect(jsonView(`${"[".repeat(502)}${"]".repeat(502)}`)).toEqual({ + expect( + textualView("json", bytes(`${"[".repeat(502)}${"]".repeat(502)}`)), + ).toEqual({ ok: false, error: "nested deeper than the 500 levels a JSON view keeps", }); - expect(jsonView(`${"[".repeat(501)}${"]".repeat(501)}`).ok).toBe(true); - expect(jsonView('{"id":9007199254740991,"x":0.5}')).toEqual({ + expect( + textualView("json", bytes(`${"[".repeat(501)}${"]".repeat(501)}`)).ok, + ).toBe(true); + expect( + textualView("json", bytes('{"id":9007199254740991,"x":0.5}')), + ).toEqual({ ok: true, value: { id: 9007199254740991, x: 0.5 }, }); @@ -332,7 +338,11 @@ it("keeps maximum-depth views encodable inside captured ZIP-member Evidence", () `${''.repeat(495)}${"".repeat(495)}`, policy, ); - const json = jsonView(`${"[".repeat(991)}0${"]".repeat(991)}`, policy); + const json = textualView( + "json", + bytes(`${"[".repeat(991)}0${"]".repeat(991)}`), + policy, + ); if (!json.ok) throw new Error(json.error); expect(xml).toMatchObject({ truncated: false }); for (const view of [xml, json.value]) { diff --git a/src/domain/desktop/stateFormats.ts b/src/domain/desktop/stateFormats.ts index e3d16b2ef2..f32ff051eb 100644 --- a/src/domain/desktop/stateFormats.ts +++ b/src/domain/desktop/stateFormats.ts @@ -196,7 +196,7 @@ const jsonScalar = (scan: JsonScan, string: boolean): boolean => { * a time without building the value: it holds a byte per open container, so * text too large to parse into a view is still told apart from other text. */ -export const isJsonText = (text: string): boolean => { +const isJsonText = (text: string): boolean => { const scan: JsonScan = { expect: "value", empty: false, @@ -382,9 +382,9 @@ export const deeperThan = (root: JsonValue, limit: number): boolean => { }; /** A JSON file's value, refused when a number would be rounded or it nests too deeply. */ -export const jsonView = ( +const jsonView = ( text: string, - policy: DesktopViewPolicy = DESKTOP_VIEW_DEFAULTS, + policy: DesktopViewPolicy, ): Result => { let rounded = false; const parsed = safeParseJson(text, (_key, value, context) => { diff --git a/src/domain/desktop/stateRoles.test.ts b/src/domain/desktop/stateRoles.test.ts index bfe4c55938..87efc24945 100644 --- a/src/domain/desktop/stateRoles.test.ts +++ b/src/domain/desktop/stateRoles.test.ts @@ -5,7 +5,6 @@ import { isApplicationDconfGroup, namedInTitles, roleFacts, - sqliteSidecarRoots, stateReasons, stateRole, type DesktopRoleFacts, @@ -241,10 +240,15 @@ describe("sqliteSidecarRoots", () => { const database = "/home/ada/Documents/books.db"; const wal = `${database}-wal`; const journal = `${database}-journal`; - expect(sqliteSidecarRoots([database, wal, journal])).toEqual( - new Set([wal, journal]), - ); - expect(sqliteSidecarRoots([wal])).toEqual(new Set()); + const sidecars = (roots: string[]) => + roleFacts({ + host: { home: "/home/ada" }, + application: { state_directories: [] }, + state: { content_roots: roots, selected_paths: [] }, + session: { windows: [], opened_files: [] }, + } as unknown as DesktopApplicationCapture).sqliteSidecars; + expect(sidecars([database, wal, journal])).toEqual(new Set([wal, journal])); + expect(sidecars([wal])).toEqual(new Set()); }); it("keeps a separately selected journal-named document alongside automatic sidecars", () => { diff --git a/src/domain/desktop/stateRoles.ts b/src/domain/desktop/stateRoles.ts index 7c0f370b38..ebd95b9c36 100644 --- a/src/domain/desktop/stateRoles.ts +++ b/src/domain/desktop/stateRoles.ts @@ -139,9 +139,9 @@ export interface DesktopRoleFacts { * selections of their own. An explicitly selected path is not one of those * logs, even when its name ends in `-wal` or `-journal`. */ -export const sqliteSidecarRoots = ( +const sqliteSidecarRoots = ( roots: readonly string[], - selected: ReadonlySet = new Set(), + selected: ReadonlySet, ): ReadonlySet => { const present = new Set(roots); return new Set( diff --git a/src/domain/desktop/taskDifficulty.test.ts b/src/domain/desktop/taskDifficulty.test.ts index f2c2121f4e..81dc822085 100644 --- a/src/domain/desktop/taskDifficulty.test.ts +++ b/src/domain/desktop/taskDifficulty.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from "vitest"; import { - bandOf, difficultyRecord, difficultySection, nextSolve, @@ -42,7 +41,17 @@ describe("the adaptive band", () => { [pass, unfinished, pass, miss], ], ] as const)("places %s as %s", (_label, band, solves) => { - expect(bandOf(solves)).toBe(band); + const record = difficultyRecord({ + model: "fixture", + agent: "fixture", + solves, + budgetUsd: 10, + stopped: "budget", + taskSha256: "ab".repeat(32), + versions: { rea: "fixture", agentenv: null }, + measuredAt: "2026-11-07T01:00:00.000Z", + }); + expect(record.band).toBe(band); }); it("asks for 3 solves, stops at 0 or 3 of 3, asks for 2 more otherwise and stops at 5", () => { diff --git a/src/domain/desktop/taskDifficulty.ts b/src/domain/desktop/taskDifficulty.ts index 309a1ef4e6..24eb296b91 100644 --- a/src/domain/desktop/taskDifficulty.ts +++ b/src/domain/desktop/taskDifficulty.ts @@ -54,7 +54,7 @@ const counts = (solves: readonly SolveRecord[]) => { }; /** The band the finished solves place the task in: 0 or 3 of 3 decide it, else 1 to 3 of 5 keep it. */ -export const bandOf = (solves: readonly SolveRecord[]): Band => { +const bandOf = (solves: readonly SolveRecord[]): Band => { const { done, passes } = counts(solves); if (done >= ALL_SOLVES) return passes === 0 diff --git a/src/domain/evidenceBundle.test.ts b/src/domain/evidenceBundle.test.ts index ddd2d71f69..2b809fc64b 100644 --- a/src/domain/evidenceBundle.test.ts +++ b/src/domain/evidenceBundle.test.ts @@ -155,7 +155,15 @@ it("retains only linked cross-target source closure in a target snapshot", () => createEvidenceBundle([trace, source, leaf, unrelated]), targetDigest, ); - expect(selected).toEqual(createEvidenceBundle([trace, source, leaf])); + expect(selected.records).toHaveLength(3); + expect(selected.records).toEqual( + expect.arrayContaining([trace, source, leaf]), + ); + expect(selected.records).not.toContainEqual(unrelated); + expect(selected.records.map(({ evidence_id }) => evidence_id)).toEqual( + [trace.evidence_id, source.evidence_id, leaf.evidence_id].sort(), + ); + expect(selected.unknowns).toEqual([]); expect(evidenceBundleForTarget(selected, targetDigest)).toEqual(selected); expect(parseEvidenceBundle(selected)).toEqual(selected); }); diff --git a/src/domain/javascript/electronBundledRequireIdentity.test.ts b/src/domain/javascript/electronBundledRequireIdentity.test.ts index 169cd36ea7..c7f6222d28 100644 --- a/src/domain/javascript/electronBundledRequireIdentity.test.ts +++ b/src/domain/javascript/electronBundledRequireIdentity.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { runInNewContext } from "node:vm"; -import { analyzeJavaScriptStaticSource as analyzeUnknownHostSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource as analyzeUnknownHostSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { analyzeCommonJsStaticFixture as analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js"; // Webpack 5 standalone bootstrap with an Electron external, as emitted for diff --git a/src/domain/javascript/electronModuleInterop.test.ts b/src/domain/javascript/electronModuleInterop.test.ts index eda151f4f1..a9a04f03f6 100644 --- a/src/domain/javascript/electronModuleInterop.test.ts +++ b/src/domain/javascript/electronModuleInterop.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource as analyzeUnknownHostSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource as analyzeUnknownHostSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { analyzeCommonJsStaticFixture as analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js"; import { ROLLDOWN_MODULE_INTEROP } from "./javascriptModuleInterop.fixture.js"; diff --git a/src/domain/javascript/electronNativeLoaderPaths.test.ts b/src/domain/javascript/electronNativeLoaderPaths.test.ts index c56a066c16..d8a19a6ecd 100644 --- a/src/domain/javascript/electronNativeLoaderPaths.test.ts +++ b/src/domain/javascript/electronNativeLoaderPaths.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; describe("native addon type-only TypeScript imports", () => { it("ignores declaration-wide import type and export type from .node modules", () => { diff --git a/src/domain/javascript/electronOpaqueBindingIdentity.test.ts b/src/domain/javascript/electronOpaqueBindingIdentity.test.ts index e3464933fd..4725add32f 100644 --- a/src/domain/javascript/electronOpaqueBindingIdentity.test.ts +++ b/src/domain/javascript/electronOpaqueBindingIdentity.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; -import { analyzeParsedJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeParsedJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; const window = "new s.BrowserWindow({webPreferences:{nodeIntegration:true}})"; diff --git a/src/domain/javascript/electronStaticAnalysisAppPathSelection.test.ts b/src/domain/javascript/electronStaticAnalysisAppPathSelection.test.ts index 697d545316..f513565f69 100644 --- a/src/domain/javascript/electronStaticAnalysisAppPathSelection.test.ts +++ b/src/domain/javascript/electronStaticAnalysisAppPathSelection.test.ts @@ -1,7 +1,7 @@ import { runInNewContext } from "node:vm"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const prefix = "const {app,BrowserWindow}=require('electron');"; const cases = [ diff --git a/src/domain/javascript/electronStaticAnalysisCandidate.test.ts b/src/domain/javascript/electronStaticAnalysisCandidate.test.ts index ba364d0222..0578d68a6c 100644 --- a/src/domain/javascript/electronStaticAnalysisCandidate.test.ts +++ b/src/domain/javascript/electronStaticAnalysisCandidate.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; -import { analyzeParsedJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeParsedJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const analyze = (source: string, path: string) => { const parsed = parseJavaScriptSource(source, path); diff --git a/src/domain/javascript/electronStaticAnalysisComputedPreferences.test.ts b/src/domain/javascript/electronStaticAnalysisComputedPreferences.test.ts index 5fa11139ed..605034ec71 100644 --- a/src/domain/javascript/electronStaticAnalysisComputedPreferences.test.ts +++ b/src/domain/javascript/electronStaticAnalysisComputedPreferences.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; describe("computed Electron web preference keys", () => { it("retains exact string, numeric, and empty literal keys", () => { diff --git a/src/domain/javascript/electronStaticAnalysisDynamicCode.test.ts b/src/domain/javascript/electronStaticAnalysisDynamicCode.test.ts index c2b0912b2c..faea375cd2 100644 --- a/src/domain/javascript/electronStaticAnalysisDynamicCode.test.ts +++ b/src/domain/javascript/electronStaticAnalysisDynamicCode.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js"; diff --git a/src/domain/javascript/electronStaticAnalysisIdentity.test.ts b/src/domain/javascript/electronStaticAnalysisIdentity.test.ts index 1e8899aa11..99f49c0298 100644 --- a/src/domain/javascript/electronStaticAnalysisIdentity.test.ts +++ b/src/domain/javascript/electronStaticAnalysisIdentity.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js"; it.each([ diff --git a/src/domain/javascript/electronStaticAnalysisOptions.test.ts b/src/domain/javascript/electronStaticAnalysisOptions.test.ts index 7c911a6b76..adb38b459a 100644 --- a/src/domain/javascript/electronStaticAnalysisOptions.test.ts +++ b/src/domain/javascript/electronStaticAnalysisOptions.test.ts @@ -8,7 +8,7 @@ import { semanticPropertyReadSources } from "./javascriptSemanticIterationSource import { analyzeParsedJavaScriptStaticSource, analyzeJavaScriptStaticSource, -} from "./javascriptStaticAnalysis.js"; +} from "../../../tests/fixtures/javascriptStaticAnalysis.js"; describe("Electron constant option evidence", () => { it("keeps mixed constant, inline and unresolved windows in construction order", () => { diff --git a/src/domain/javascript/electronStaticAnalysisProtocol.test.ts b/src/domain/javascript/electronStaticAnalysisProtocol.test.ts index a5aa5f2b2a..8536a9b0e5 100644 --- a/src/domain/javascript/electronStaticAnalysisProtocol.test.ts +++ b/src/domain/javascript/electronStaticAnalysisProtocol.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const protocolsOf = (source: string) => analyzeJavaScriptStaticSource(source).electron.protocols; diff --git a/src/domain/javascript/electronStaticAnalysisPublishedApp.test.ts b/src/domain/javascript/electronStaticAnalysisPublishedApp.test.ts index 1145736c12..6e6e48a59a 100644 --- a/src/domain/javascript/electronStaticAnalysisPublishedApp.test.ts +++ b/src/domain/javascript/electronStaticAnalysisPublishedApp.test.ts @@ -1,6 +1,6 @@ import { runInNewContext } from "node:vm"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; it.each([ diff --git a/src/domain/javascript/electronStaticAnalysisReturnedApp.test.ts b/src/domain/javascript/electronStaticAnalysisReturnedApp.test.ts index 2169bf18ba..f35996deb4 100644 --- a/src/domain/javascript/electronStaticAnalysisReturnedApp.test.ts +++ b/src/domain/javascript/electronStaticAnalysisReturnedApp.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; it.each([ [ diff --git a/src/domain/javascript/electronStaticAnalysisScopedIpc.test.ts b/src/domain/javascript/electronStaticAnalysisScopedIpc.test.ts index 956a82f901..c8ca27ca13 100644 --- a/src/domain/javascript/electronStaticAnalysisScopedIpc.test.ts +++ b/src/domain/javascript/electronStaticAnalysisScopedIpc.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource as analyzeUnknownHostSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource as analyzeUnknownHostSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { analyzeCommonJsStaticFixture as analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js"; it("retains an unknown-host Electron request as a candidate without claiming IPC identity", () => { diff --git a/src/domain/javascript/electronWrappedCallPaths.test.ts b/src/domain/javascript/electronWrappedCallPaths.test.ts index deffeb4731..66cab74c32 100644 --- a/src/domain/javascript/electronWrappedCallPaths.test.ts +++ b/src/domain/javascript/electronWrappedCallPaths.test.ts @@ -1,6 +1,6 @@ import { Script } from "node:vm"; import { describe, expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; it("retains switch selection on a direct bridge exposure", () => { const result = analyzeJavaScriptStaticSource(` diff --git a/src/domain/javascript/javascriptAstFingerprint.test.ts b/src/domain/javascript/javascriptAstFingerprint.test.ts index 64fbac2a46..6d2f9a963c 100644 --- a/src/domain/javascript/javascriptAstFingerprint.test.ts +++ b/src/domain/javascript/javascriptAstFingerprint.test.ts @@ -2,7 +2,7 @@ import { parse } from "@babel/parser"; import { expect, it } from "vitest"; import { fingerprintJavaScriptAst } from "./javascriptAstFingerprint.js"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; it.each([ ["bigint values", "const value = 1n;", "const value = 2n;"], diff --git a/src/domain/javascript/javascriptBundlerRegistrationIdentity.test.ts b/src/domain/javascript/javascriptBundlerRegistrationIdentity.test.ts index 1b251e3132..a5bebde931 100644 --- a/src/domain/javascript/javascriptBundlerRegistrationIdentity.test.ts +++ b/src/domain/javascript/javascriptBundlerRegistrationIdentity.test.ts @@ -1,5 +1,5 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const registration = (runtime: string, callback: string, chunk = 1) => "(globalThis." + diff --git a/src/domain/javascript/javascriptCommonJsAssignAppPath.test.ts b/src/domain/javascript/javascriptCommonJsAssignAppPath.test.ts index 82366cb920..6abae292be 100644 --- a/src/domain/javascript/javascriptCommonJsAssignAppPath.test.ts +++ b/src/domain/javascript/javascriptCommonJsAssignAppPath.test.ts @@ -2,7 +2,7 @@ import { runInNewContext } from "node:vm"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { analyzeCommonJsStaticFixture, analyzeCommonJsSemanticFixture, diff --git a/src/domain/javascript/javascriptFactoryExportRoles.test.ts b/src/domain/javascript/javascriptFactoryExportRoles.test.ts index 045c2d715f..844b54e472 100644 --- a/src/domain/javascript/javascriptFactoryExportRoles.test.ts +++ b/src/domain/javascript/javascriptFactoryExportRoles.test.ts @@ -3,7 +3,7 @@ import { runInNewContext } from "node:vm"; import { transformSync } from "esbuild"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; it.each([false, true])( "retains generated esbuild exports with module use %s", diff --git a/src/domain/javascript/javascriptIntrinsicInvocation.test.ts b/src/domain/javascript/javascriptIntrinsicInvocation.test.ts index b3858d8837..af70a49878 100644 --- a/src/domain/javascript/javascriptIntrinsicInvocation.test.ts +++ b/src/domain/javascript/javascriptIntrinsicInvocation.test.ts @@ -8,7 +8,7 @@ import { resolveJavaScriptGenericInvocation, } from "./javascriptIntrinsicInvocation.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; -import { indexJavaScriptBindingsSteps } from "./javascriptSemanticAnalysis.js"; +import { indexJavaScriptBindingsSteps } from "../../../tests/fixtures/javascriptSemanticIndex.js"; import { completeSemanticSteps, traverseJavaScriptAst, diff --git a/src/domain/javascript/javascriptMemberMutationCoverage.test.ts b/src/domain/javascript/javascriptMemberMutationCoverage.test.ts index 23399c7dce..320b977940 100644 --- a/src/domain/javascript/javascriptMemberMutationCoverage.test.ts +++ b/src/domain/javascript/javascriptMemberMutationCoverage.test.ts @@ -6,7 +6,7 @@ import { } from "./javascriptSemanticAnalysis.js"; import { onlyCallable } from "./javascriptSemanticAnalysis.fixture.js"; import { semanticResourceLimitReason } from "./javascriptSemanticResourceLimits.js"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; import { completeSemanticSteps, diff --git a/src/domain/javascript/javascriptReturnedReferenceCoverage.test.ts b/src/domain/javascript/javascriptReturnedReferenceCoverage.test.ts index 64f9394bf7..fc82b874a0 100644 --- a/src/domain/javascript/javascriptReturnedReferenceCoverage.test.ts +++ b/src/domain/javascript/javascriptReturnedReferenceCoverage.test.ts @@ -1,7 +1,7 @@ import { expect, it } from "vitest"; import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { RETURNED_REFERENCE_WIDENING_LIMITATION } from "./javascriptSemanticCoverage.js"; it("preserves possible ambient effects and discloses projection widening in source coverage", () => { diff --git a/src/domain/javascript/javascriptRouteInventory.test.ts b/src/domain/javascript/javascriptRouteInventory.test.ts index 543a614897..5db3f77b77 100644 --- a/src/domain/javascript/javascriptRouteInventory.test.ts +++ b/src/domain/javascript/javascriptRouteInventory.test.ts @@ -1,5 +1,5 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { completeSemanticSteps } from "./javascriptSemanticTraversal.js"; import { buildJavaScriptRouteInventorySteps, diff --git a/src/domain/javascript/javascriptRouteResultBudget.test.ts b/src/domain/javascript/javascriptRouteResultBudget.test.ts index 49dcb666f8..3c3daa998f 100644 --- a/src/domain/javascript/javascriptRouteResultBudget.test.ts +++ b/src/domain/javascript/javascriptRouteResultBudget.test.ts @@ -1,5 +1,5 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { javaScriptRouteSourceFactsSchema } from "./javascriptRouteFacts.js"; import { fitJavaScriptRouteSourceFacts } from "./javascriptRouteResultBudget.js"; diff --git a/src/domain/javascript/javascriptRpcEndpoints.test.ts b/src/domain/javascript/javascriptRpcEndpoints.test.ts index 77d2427aa5..815fbc128e 100644 --- a/src/domain/javascript/javascriptRpcEndpoints.test.ts +++ b/src/domain/javascript/javascriptRpcEndpoints.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { runInNewContext } from "node:vm"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const endpoints = (source: string) => analyzeJavaScriptStaticSource(source).endpoints.map( diff --git a/src/domain/javascript/javascriptSemanticAmbientWrites.test.ts b/src/domain/javascript/javascriptSemanticAmbientWrites.test.ts index 5044197dcc..54c73730ba 100644 --- a/src/domain/javascript/javascriptSemanticAmbientWrites.test.ts +++ b/src/domain/javascript/javascriptSemanticAmbientWrites.test.ts @@ -1,11 +1,9 @@ import * as t from "@babel/types"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; -import { - indexJavaScriptBindingsSteps, - indexJavaScriptBindingsWithReceiversSteps, -} from "./javascriptSemanticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; +import { indexJavaScriptBindingsWithReceiversSteps } from "./javascriptSemanticAnalysis.js"; +import { indexJavaScriptBindingsSteps } from "../../../tests/fixtures/javascriptSemanticIndex.js"; import { completeSemanticSteps, traverseJavaScriptAst, diff --git a/src/domain/javascript/javascriptSemanticAnalysis.member_depth.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.member_depth.test.ts index dde90fd528..54bcde762e 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.member_depth.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.member_depth.test.ts @@ -8,7 +8,7 @@ import { import { onlyBinding, origin } from "./javascriptSemanticAnalysis.fixture.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; import { calleeName } from "./javascriptStaticAnalysisHelpers.js"; -import { analyzeParsedJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeParsedJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const memberChain = ".next".repeat(12_000); const ordinaryMemberChain = ".next".repeat(64); diff --git a/src/domain/javascript/javascriptSemanticAnalysis.rejection.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.rejection.test.ts index d60b62d226..8e56a7b0b4 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.rejection.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.rejection.test.ts @@ -7,7 +7,7 @@ import { analyzeParsedJavaScriptSemantics, } from "./javascriptSemanticAnalysis.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; -import { analyzeParsedJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeParsedJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { collectJavaScriptExports, fingerprintJavaScriptAst, diff --git a/src/domain/javascript/javascriptSemanticAnalysis.ts b/src/domain/javascript/javascriptSemanticAnalysis.ts index f9abd62983..9c09d52412 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.ts @@ -408,17 +408,6 @@ const semanticPathsOverlap = ( return true; }; -/** Share the lexical index with bounded, demand-driven boundary analyses. */ -export function* indexJavaScriptBindingsSteps( - program: t.Program, - precision?: JavaScriptSemanticPrecisionInput, -): Generator { - const state = createState(program, "import", precision); - yield* collectDefinitionsSteps(program, state); - yield* indexSemanticAmbientWritesSteps(state); - return state; -} - /** Recover semantics from an already parsed JavaScript artifact. */ export const analyzeParsedJavaScriptSemantics = ( file: ParsedJavaScriptSource, diff --git a/src/domain/javascript/javascriptSemanticCallResolution.test.ts b/src/domain/javascript/javascriptSemanticCallResolution.test.ts index fd42045f8f..31977aa646 100644 --- a/src/domain/javascript/javascriptSemanticCallResolution.test.ts +++ b/src/domain/javascript/javascriptSemanticCallResolution.test.ts @@ -3,9 +3,9 @@ import { expect, it, vi } from "vitest"; import { analyzeJavaScriptSemantics, - indexJavaScriptBindingsSteps, indexJavaScriptBindingsWithReceiversSteps, } from "./javascriptSemanticAnalysis.js"; +import { indexJavaScriptBindingsSteps } from "../../../tests/fixtures/javascriptSemanticIndex.js"; import { resolveLocalCallables, type LocalCallableResolution, diff --git a/src/domain/javascript/javascriptSemanticExpressionShapes.test.ts b/src/domain/javascript/javascriptSemanticExpressionShapes.test.ts index e8e37d3079..b060286a71 100644 --- a/src/domain/javascript/javascriptSemanticExpressionShapes.test.ts +++ b/src/domain/javascript/javascriptSemanticExpressionShapes.test.ts @@ -1,10 +1,8 @@ import * as t from "@babel/types"; import { expect, it } from "vitest"; -import { - analyzeParsedJavaScriptSemantics, - indexJavaScriptBindingsSteps, -} from "./javascriptSemanticAnalysis.js"; +import { analyzeParsedJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; +import { indexJavaScriptBindingsSteps } from "../../../tests/fixtures/javascriptSemanticIndex.js"; import { createSemanticExpressionShapeResolver } from "./javascriptSemanticExpressionShapes.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; import { diff --git a/src/domain/javascript/javascriptSemanticObjectReferences.test.ts b/src/domain/javascript/javascriptSemanticObjectReferences.test.ts index 148608ca2d..e0baf1d5a9 100644 --- a/src/domain/javascript/javascriptSemanticObjectReferences.test.ts +++ b/src/domain/javascript/javascriptSemanticObjectReferences.test.ts @@ -1,7 +1,7 @@ import * as t from "@babel/types"; import { expect, it } from "vitest"; -import { indexJavaScriptBindingsSteps } from "./javascriptSemanticAnalysis.js"; +import { indexJavaScriptBindingsSteps } from "../../../tests/fixtures/javascriptSemanticIndex.js"; import { semanticClassPropertyReferences, semanticNativePrototypeChain, diff --git a/src/domain/javascript/javascriptSemanticPrecision.test.ts b/src/domain/javascript/javascriptSemanticPrecision.test.ts index 93d5fe0a07..cb02475f98 100644 --- a/src/domain/javascript/javascriptSemanticPrecision.test.ts +++ b/src/domain/javascript/javascriptSemanticPrecision.test.ts @@ -2,7 +2,7 @@ import { expect, it } from "vitest"; import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; import { onlyBinding } from "./javascriptSemanticAnalysis.fixture.js"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { semanticResourceLimitCoverage } from "./javascriptSemanticCoverage.js"; import { javaScriptSemanticPrecisionSchema } from "./javascriptSemanticPrecision.js"; diff --git a/src/domain/javascript/javascriptSemanticProvenanceAssignments.test.ts b/src/domain/javascript/javascriptSemanticProvenanceAssignments.test.ts index eeff6ee595..d40828563a 100644 --- a/src/domain/javascript/javascriptSemanticProvenanceAssignments.test.ts +++ b/src/domain/javascript/javascriptSemanticProvenanceAssignments.test.ts @@ -2,7 +2,7 @@ import { expect, it } from "vitest"; import { analyzeParsedJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const provenance = (source: string, name: string) => { const file = parseJavaScriptSource(source, "fixture.cjs"); diff --git a/src/domain/javascript/javascriptSemanticRequireCaptures.test.ts b/src/domain/javascript/javascriptSemanticRequireCaptures.test.ts index 8fd55286bb..6460680f3e 100644 --- a/src/domain/javascript/javascriptSemanticRequireCaptures.test.ts +++ b/src/domain/javascript/javascriptSemanticRequireCaptures.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; it("keeps a self-defaulted module delegate as a possible load", () => { expect( diff --git a/src/domain/javascript/javascriptSemanticRestAssignmentCaptures.test.ts b/src/domain/javascript/javascriptSemanticRestAssignmentCaptures.test.ts index 9e8cce3189..893803b291 100644 --- a/src/domain/javascript/javascriptSemanticRestAssignmentCaptures.test.ts +++ b/src/domain/javascript/javascriptSemanticRestAssignmentCaptures.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; import { topLevelBinding } from "./javascriptSemanticAnalysis.fixture.js"; diff --git a/src/domain/javascript/javascriptSemanticReturnedReferences.projection.test.ts b/src/domain/javascript/javascriptSemanticReturnedReferences.projection.test.ts index a82a2423a7..dfe89e8715 100644 --- a/src/domain/javascript/javascriptSemanticReturnedReferences.projection.test.ts +++ b/src/domain/javascript/javascriptSemanticReturnedReferences.projection.test.ts @@ -1,7 +1,7 @@ import * as t from "@babel/types"; import { expect, it } from "vitest"; -import { indexJavaScriptBindingsSteps } from "./javascriptSemanticAnalysis.js"; +import { indexJavaScriptBindingsSteps } from "../../../tests/fixtures/javascriptSemanticIndex.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; import { createSemanticReturnedReferences } from "./javascriptSemanticReturnedReferences.js"; import { completeSemanticSteps } from "./javascriptSemanticTraversal.js"; diff --git a/src/domain/javascript/javascriptSemanticReturnedReferencesBounded.test.ts b/src/domain/javascript/javascriptSemanticReturnedReferencesBounded.test.ts index 65ea590887..b619d27590 100644 --- a/src/domain/javascript/javascriptSemanticReturnedReferencesBounded.test.ts +++ b/src/domain/javascript/javascriptSemanticReturnedReferencesBounded.test.ts @@ -1,7 +1,7 @@ import * as t from "@babel/types"; import { expect, it, vi } from "vitest"; -import { indexJavaScriptBindingsSteps } from "./javascriptSemanticAnalysis.js"; +import { indexJavaScriptBindingsSteps } from "../../../tests/fixtures/javascriptSemanticIndex.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; import { createSemanticReturnedReferences } from "./javascriptSemanticReturnedReferences.js"; diff --git a/src/domain/javascript/javascriptSemanticReturnedReferencesCopies.test.ts b/src/domain/javascript/javascriptSemanticReturnedReferencesCopies.test.ts index d209dc1f12..1eaa928626 100644 --- a/src/domain/javascript/javascriptSemanticReturnedReferencesCopies.test.ts +++ b/src/domain/javascript/javascriptSemanticReturnedReferencesCopies.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; it.each([ [ diff --git a/src/domain/javascript/javascriptSemanticReturnedReferencesWrites.test.ts b/src/domain/javascript/javascriptSemanticReturnedReferencesWrites.test.ts index f64cae8454..394fe9688f 100644 --- a/src/domain/javascript/javascriptSemanticReturnedReferencesWrites.test.ts +++ b/src/domain/javascript/javascriptSemanticReturnedReferencesWrites.test.ts @@ -1,7 +1,7 @@ import * as t from "@babel/types"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { indexJavaScriptBindingsWithReceiversSteps } from "./javascriptSemanticAnalysis.js"; import { createSemanticReturnedReferences } from "./javascriptSemanticReturnedReferences.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; diff --git a/src/domain/javascript/javascriptSemanticSharedChildWrites.test.ts b/src/domain/javascript/javascriptSemanticSharedChildWrites.test.ts index 2ecac92443..17d71cf900 100644 --- a/src/domain/javascript/javascriptSemanticSharedChildWrites.test.ts +++ b/src/domain/javascript/javascriptSemanticSharedChildWrites.test.ts @@ -1,7 +1,7 @@ import { createContext, runInContext, runInNewContext } from "node:vm"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import * as t from "@babel/types"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; import { diff --git a/src/domain/javascript/javascriptSemanticValues.memo.test.ts b/src/domain/javascript/javascriptSemanticValues.memo.test.ts index 7911c40953..d2670bd1f8 100644 --- a/src/domain/javascript/javascriptSemanticValues.memo.test.ts +++ b/src/domain/javascript/javascriptSemanticValues.memo.test.ts @@ -1,9 +1,7 @@ import { expect, it, vi } from "vitest"; -import { - analyzeJavaScriptSemantics, - indexJavaScriptBindingsSteps, -} from "./javascriptSemanticAnalysis.js"; +import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; +import { indexJavaScriptBindingsSteps } from "../../../tests/fixtures/javascriptSemanticIndex.js"; import { SemanticDependencyProofs } from "./javascriptSemanticDependencies.js"; import { JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS } from "./javascriptSemanticPrecision.js"; import type { JavaScriptSemanticValue } from "./javascriptSemanticValueTypes.js"; diff --git a/src/domain/javascript/javascriptSemanticWrapperAssignments.test.ts b/src/domain/javascript/javascriptSemanticWrapperAssignments.test.ts index 71d6a7e25a..275a935ea4 100644 --- a/src/domain/javascript/javascriptSemanticWrapperAssignments.test.ts +++ b/src/domain/javascript/javascriptSemanticWrapperAssignments.test.ts @@ -2,7 +2,7 @@ import { expect, it } from "vitest"; import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; import { topLevelBinding } from "./javascriptSemanticAnalysis.fixture.js"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { analyzeCommonJsStaticFixture, analyzeCommonJsSemanticFixture, diff --git a/src/domain/javascript/javascriptSourceParser.test.ts b/src/domain/javascript/javascriptSourceParser.test.ts index f94f7f1f45..8f010e30f2 100644 --- a/src/domain/javascript/javascriptSourceParser.test.ts +++ b/src/domain/javascript/javascriptSourceParser.test.ts @@ -10,7 +10,7 @@ import { javaScriptThisOwner, } from "./javascriptSourceParser.js"; import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; -import { analyzeParsedJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeParsedJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; it.each([ ["class C extends (this.marker, Object) {}", true], diff --git a/src/domain/javascript/javascriptStaticAnalysis.event_source.test.ts b/src/domain/javascript/javascriptStaticAnalysis.event_source.test.ts index 4f6801e784..43fdfa0bc2 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.event_source.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.event_source.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const endpoints = (source: string) => analyzeJavaScriptStaticSource(source).endpoints.map( diff --git a/src/domain/javascript/javascriptStaticAnalysis.mapEndpoints.test.ts b/src/domain/javascript/javascriptStaticAnalysis.mapEndpoints.test.ts index 7239802f80..b8c1436c16 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.mapEndpoints.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.mapEndpoints.test.ts @@ -1,7 +1,7 @@ import { expect, it } from "vitest"; import * as t from "@babel/types"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { inspectCall } from "./javascriptStaticAnalysisCalls.js"; import { createJavaScriptAnalysisAccumulator } from "./javascriptStaticAnalysisState.js"; import { indexJavaScriptBindingsWithReceiversSteps } from "./javascriptSemanticAnalysis.js"; diff --git a/src/domain/javascript/javascriptStaticAnalysis.mapOpaqueEffects.test.ts b/src/domain/javascript/javascriptStaticAnalysis.mapOpaqueEffects.test.ts index a7176d202a..b2c468a185 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.mapOpaqueEffects.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.mapOpaqueEffects.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; import { onlyCallable } from "./javascriptSemanticAnalysis.fixture.js"; diff --git a/src/domain/javascript/javascriptStaticAnalysis.member_path_depth.test.ts b/src/domain/javascript/javascriptStaticAnalysis.member_path_depth.test.ts index 63ffcf9de6..c8c4010b8e 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.member_path_depth.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.member_path_depth.test.ts @@ -1,7 +1,7 @@ import { expect, it } from "vitest"; import { collectJavaScriptExports } from "./javascriptAstFingerprint.js"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; const memberChain = ".next".repeat(12_000); diff --git a/src/domain/javascript/javascriptStaticAnalysis.no_finding_caps.test.ts b/src/domain/javascript/javascriptStaticAnalysis.no_finding_caps.test.ts index a8e44d623b..ee5fbb3424 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.no_finding_caps.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.no_finding_caps.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js"; describe("JavaScript static analysis findings", () => { diff --git a/src/domain/javascript/javascriptStaticAnalysis.partialEndpoints.test.ts b/src/domain/javascript/javascriptStaticAnalysis.partialEndpoints.test.ts index b0b184efa5..d516dd49b0 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.partialEndpoints.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.partialEndpoints.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const endpoints = (source: string) => analyzeJavaScriptStaticSource(source).endpoints.map((endpoint) => ({ diff --git a/src/domain/javascript/javascriptStaticAnalysis.routeProperties.test.ts b/src/domain/javascript/javascriptStaticAnalysis.routeProperties.test.ts index 801591159b..8da02b7d18 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.routeProperties.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.routeProperties.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const routeEndpoints = (source: string) => analyzeJavaScriptStaticSource(source) diff --git a/src/domain/javascript/javascriptStaticAnalysis.send_beacon.test.ts b/src/domain/javascript/javascriptStaticAnalysis.send_beacon.test.ts index b55ad86488..4d2750f17b 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.send_beacon.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.send_beacon.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const endpoints = (source: string) => analyzeJavaScriptStaticSource(source).endpoints.map( diff --git a/src/domain/javascript/javascriptStaticAnalysis.sqliteStorage.test.ts b/src/domain/javascript/javascriptStaticAnalysis.sqliteStorage.test.ts index b1a0262943..9889a5225a 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.sqliteStorage.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.sqliteStorage.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; it.each([ 'class Database { constructor(name) { this.name = name; } } new Database("hello");', diff --git a/src/domain/javascript/javascriptStaticAnalysis.ts b/src/domain/javascript/javascriptStaticAnalysis.ts index 5bdfa47a04..2f5e2b0d7e 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.ts @@ -15,10 +15,7 @@ import { ELECTRON_REFERENCE_KINDS, } from "./javascriptElectronMemberWrites.js"; -import { - completeSemanticSteps, - traverseJavaScriptAstSteps, -} from "./javascriptSemanticTraversal.js"; +import { traverseJavaScriptAstSteps } from "./javascriptSemanticTraversal.js"; import { electronRendererLoadAuthority, @@ -37,7 +34,6 @@ import { createSemanticExpressionShapeResolver } from "./javascriptSemanticExpre import { finishElectronBrowserWindowsSteps } from "./electronStaticAnalysisBrowser.js"; import { detectVendors, - failedJavaScriptStaticAnalysis, registrationKey, stringValue, } from "./javascriptStaticAnalysisHelpers.js"; @@ -71,10 +67,7 @@ import { indexJavaScriptElectronIdentitySteps, indexJavaScriptBindingsWithReceiversSteps, } from "./javascriptSemanticAnalysis.js"; -import { - parseJavaScriptSource, - type ParsedJavaScriptSource, -} from "./javascriptSourceParser.js"; +import type { ParsedJavaScriptSource } from "./javascriptSourceParser.js"; import { createJavaScriptRpcScan } from "./javascriptRpcEndpoints.js"; import { @@ -87,27 +80,6 @@ import { type JavaScriptStaticCoverage, } from "./javascriptStaticCoverage.js"; -/** Parse one JavaScript artifact and recover static structure only. */ -export const analyzeJavaScriptStaticSource = ( - source: string, - precision?: JavaScriptSemanticPrecisionInput, -): JavaScriptStaticAnalysis => { - const file = parseJavaScriptSource(source); - return file === null - ? failedJavaScriptStaticAnalysis() - : analyzeParsedJavaScriptStaticSource(source, file, precision); -}; - -/** Recover static structure from an already parsed JavaScript artifact. */ -export const analyzeParsedJavaScriptStaticSource = ( - source: string, - file: ParsedJavaScriptSource, - precision?: JavaScriptSemanticPrecisionInput, -): JavaScriptStaticAnalysis => - completeSemanticSteps( - analyzeParsedJavaScriptStaticSourceSteps(source, file, precision), - ); - /** Static analysis that yields during its source traversal; results are unchanged. */ export function* analyzeParsedJavaScriptStaticSourceSteps( source: string, diff --git a/src/domain/javascript/javascriptStaticAnalysisAssignedRequire.test.ts b/src/domain/javascript/javascriptStaticAnalysisAssignedRequire.test.ts index 3c680ef4b4..98ba4db6c8 100644 --- a/src/domain/javascript/javascriptStaticAnalysisAssignedRequire.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisAssignedRequire.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; it.each([ [ diff --git a/src/domain/javascript/javascriptStaticAnalysisAssignmentLoaders.test.ts b/src/domain/javascript/javascriptStaticAnalysisAssignmentLoaders.test.ts index 95435f7ac0..8ade10e6d9 100644 --- a/src/domain/javascript/javascriptStaticAnalysisAssignmentLoaders.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisAssignmentLoaders.test.ts @@ -3,7 +3,7 @@ import * as path from "node:path"; import { runInNewContext } from "node:vm"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const cases = [ { diff --git a/src/domain/javascript/javascriptStaticAnalysisCallableReceivers.test.ts b/src/domain/javascript/javascriptStaticAnalysisCallableReceivers.test.ts index 25d8f3a8bc..39a575f8c6 100644 --- a/src/domain/javascript/javascriptStaticAnalysisCallableReceivers.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisCallableReceivers.test.ts @@ -1,7 +1,7 @@ import { runInNewContext } from "node:vm"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const generic = "const get=()=>({});const invoke=get.call;"; const cases = [ diff --git a/src/domain/javascript/javascriptStaticAnalysisCalleeBoundary.test.ts b/src/domain/javascript/javascriptStaticAnalysisCalleeBoundary.test.ts index 74a290f81b..740d660a61 100644 --- a/src/domain/javascript/javascriptStaticAnalysisCalleeBoundary.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisCalleeBoundary.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js"; import { aliasSelectionBudgetSource } from "../../../tests/fixtures/javascriptAliasSelectionBudget.js"; diff --git a/src/domain/javascript/javascriptStaticAnalysisCapturedGeneric.test.ts b/src/domain/javascript/javascriptStaticAnalysisCapturedGeneric.test.ts index b9f67c7c0c..ad85d2fc14 100644 --- a/src/domain/javascript/javascriptStaticAnalysisCapturedGeneric.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisCapturedGeneric.test.ts @@ -3,7 +3,7 @@ import { expect, it } from "vitest"; import * as t from "@babel/types"; import { capturedGenericInvocations } from "../../../tests/fixtures/javascriptCapturedGenericInvocations.js"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; import { indexJavaScriptBindingsWithReceiversSteps } from "./javascriptSemanticAnalysis.js"; import { completeSemanticSteps } from "./javascriptSemanticTraversal.js"; diff --git a/src/domain/javascript/javascriptStaticAnalysisCapturedRequire.test.ts b/src/domain/javascript/javascriptStaticAnalysisCapturedRequire.test.ts index 3065ba811b..79053eb309 100644 --- a/src/domain/javascript/javascriptStaticAnalysisCapturedRequire.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisCapturedRequire.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; it.each([ ['const load = module.require; load.call(module, "../shared.cjs");', true], diff --git a/src/domain/javascript/javascriptStaticAnalysisCopiedRequire.test.ts b/src/domain/javascript/javascriptStaticAnalysisCopiedRequire.test.ts index e879964473..64d043637a 100644 --- a/src/domain/javascript/javascriptStaticAnalysisCopiedRequire.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisCopiedRequire.test.ts @@ -1,7 +1,7 @@ import { Module } from "node:module"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; it("checks inherited and own enumerable require against a real Node Module", () => { const host = new Module("copied-require-oracle.cjs"); diff --git a/src/domain/javascript/javascriptStaticAnalysisDeferredLoader.test.ts b/src/domain/javascript/javascriptStaticAnalysisDeferredLoader.test.ts index 89f3ef69e3..bf45203844 100644 --- a/src/domain/javascript/javascriptStaticAnalysisDeferredLoader.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisDeferredLoader.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; /** * A `createRequire` loader saved under a member literally named `require` is still diff --git a/src/domain/javascript/javascriptStaticAnalysisDelegateEligibility.test.ts b/src/domain/javascript/javascriptStaticAnalysisDelegateEligibility.test.ts index 1ea1db913f..5525d6c4c2 100644 --- a/src/domain/javascript/javascriptStaticAnalysisDelegateEligibility.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisDelegateEligibility.test.ts @@ -3,7 +3,7 @@ import * as path from "node:path"; import { runInNewContext } from "node:vm"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js"; it("keeps repeated Node resolver calls separate from loader delegate eligibility", () => { diff --git a/src/domain/javascript/javascriptStaticAnalysisFactoryAlternatives.test.ts b/src/domain/javascript/javascriptStaticAnalysisFactoryAlternatives.test.ts index dabe4c424b..e22d14016d 100644 --- a/src/domain/javascript/javascriptStaticAnalysisFactoryAlternatives.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisFactoryAlternatives.test.ts @@ -3,7 +3,7 @@ import { dirname, resolve } from "node:path"; import { runInNewContext } from "node:vm"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; it("observes an authored factory returning a loader from another directory", () => { const source = resolve("factory-oracle-source/main.cjs"); diff --git a/src/domain/javascript/javascriptStaticAnalysisInvocationIteration.test.ts b/src/domain/javascript/javascriptStaticAnalysisInvocationIteration.test.ts index 492a4e93fd..8761b202b2 100644 --- a/src/domain/javascript/javascriptStaticAnalysisInvocationIteration.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisInvocationIteration.test.ts @@ -8,7 +8,7 @@ import { expect, it } from "vitest"; import { analyzeJavaScriptStaticSource, analyzeParsedJavaScriptStaticSource, -} from "./javascriptStaticAnalysis.js"; +} from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; const changedIterator = diff --git a/src/domain/javascript/javascriptStaticAnalysisInvocationReadSite.test.ts b/src/domain/javascript/javascriptStaticAnalysisInvocationReadSite.test.ts index cbbf991632..4d2ab15e3f 100644 --- a/src/domain/javascript/javascriptStaticAnalysisInvocationReadSite.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisInvocationReadSite.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; it.each([ "handlers[key]()", diff --git a/src/domain/javascript/javascriptStaticAnalysisLoaderInvocations.test.ts b/src/domain/javascript/javascriptStaticAnalysisLoaderInvocations.test.ts index b0fdc0da3c..b21b8b40a7 100644 --- a/src/domain/javascript/javascriptStaticAnalysisLoaderInvocations.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisLoaderInvocations.test.ts @@ -6,7 +6,7 @@ import { expect, it } from "vitest"; import { analyzeJavaScriptStaticSource, analyzeParsedJavaScriptStaticSource, -} from "./javascriptStaticAnalysis.js"; +} from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; import { referenceSourceRequireCalls } from "../referenceSourceRequireBindings.js"; diff --git a/src/domain/javascript/javascriptStaticAnalysisLoaderMayTargets.test.ts b/src/domain/javascript/javascriptStaticAnalysisLoaderMayTargets.test.ts index aa5c9bc080..2eb7b02b72 100644 --- a/src/domain/javascript/javascriptStaticAnalysisLoaderMayTargets.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisLoaderMayTargets.test.ts @@ -3,7 +3,7 @@ import * as path from "node:path"; import { runInNewContext } from "node:vm"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; it("keeps the actual CommonJS module export separate from an ESM synthetic default", () => { const require = createRequire(import.meta.url); diff --git a/src/domain/javascript/javascriptStaticAnalysisLoaderMutationControls.test.ts b/src/domain/javascript/javascriptStaticAnalysisLoaderMutationControls.test.ts index a14fab5d63..061c9a664a 100644 --- a/src/domain/javascript/javascriptStaticAnalysisLoaderMutationControls.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisLoaderMutationControls.test.ts @@ -3,7 +3,7 @@ import * as path from "node:path"; import { runInNewContext } from "node:vm"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const named = "import {createRequire as make} from 'node:module';"; const namespace = "import * as mod from 'node:module';"; diff --git a/src/domain/javascript/javascriptStaticAnalysisLocations.test.ts b/src/domain/javascript/javascriptStaticAnalysisLocations.test.ts index b4e08c2585..cfcd1400bd 100644 --- a/src/domain/javascript/javascriptStaticAnalysisLocations.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisLocations.test.ts @@ -1,7 +1,7 @@ import { parse } from "@babel/parser"; import { describe, expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const directive = "//# sourceMappingURL=app.js.map"; diff --git a/src/domain/javascript/javascriptStaticAnalysisMainModule.test.ts b/src/domain/javascript/javascriptStaticAnalysisMainModule.test.ts index e43e4de24a..1a32d42dfb 100644 --- a/src/domain/javascript/javascriptStaticAnalysisMainModule.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisMainModule.test.ts @@ -2,7 +2,7 @@ import { Module } from "node:module"; import { runInNewContext } from "node:vm"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const cases = [ ["native main", "require.main.require('node:path');", true, true], diff --git a/src/domain/javascript/javascriptStaticAnalysisModuleIndex.test.ts b/src/domain/javascript/javascriptStaticAnalysisModuleIndex.test.ts index f22aa55f2d..5279138c63 100644 --- a/src/domain/javascript/javascriptStaticAnalysisModuleIndex.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisModuleIndex.test.ts @@ -1,7 +1,7 @@ import { expect, it } from "vitest"; import { JavaScriptModuleRangeIndex } from "./javascriptStaticAnalysisFindings.js"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; it.each(["direct", "call"])( "recovers standalone module factories dispatched with %s", diff --git a/src/domain/javascript/javascriptStaticAnalysisProcessMainModule.test.ts b/src/domain/javascript/javascriptStaticAnalysisProcessMainModule.test.ts index fb31d16c03..96204295e2 100644 --- a/src/domain/javascript/javascriptStaticAnalysisProcessMainModule.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisProcessMainModule.test.ts @@ -3,7 +3,7 @@ import { runInNewContext } from "node:vm"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const cases = [ ["native process", "process.mainModule.require('node:path');", true], diff --git a/src/domain/javascript/javascriptStaticAnalysisProjectedModuleWrites.test.ts b/src/domain/javascript/javascriptStaticAnalysisProjectedModuleWrites.test.ts index e9f8f3e962..6bb2da3e78 100644 --- a/src/domain/javascript/javascriptStaticAnalysisProjectedModuleWrites.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisProjectedModuleWrites.test.ts @@ -3,7 +3,7 @@ import * as path from "node:path"; import { runInNewContext } from "node:vm"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const projectedModuleWrites = [ [ diff --git a/src/domain/javascript/javascriptStaticAnalysisReturnedInvocation.test.ts b/src/domain/javascript/javascriptStaticAnalysisReturnedInvocation.test.ts index eb365bfb12..b73bb63bd3 100644 --- a/src/domain/javascript/javascriptStaticAnalysisReturnedInvocation.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisReturnedInvocation.test.ts @@ -1,7 +1,7 @@ import { expect, it } from "vitest"; import * as t from "@babel/types"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; import { indexJavaScriptBindingsWithReceiversSteps } from "./javascriptSemanticAnalysis.js"; import { completeSemanticSteps } from "./javascriptSemanticTraversal.js"; diff --git a/src/domain/javascript/javascriptStaticAnalysisReturnedLoaders.test.ts b/src/domain/javascript/javascriptStaticAnalysisReturnedLoaders.test.ts index 6d32929474..6ed666b380 100644 --- a/src/domain/javascript/javascriptStaticAnalysisReturnedLoaders.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisReturnedLoaders.test.ts @@ -2,7 +2,7 @@ import { expect, it } from "vitest"; import { parseReferenceSourceImports } from "../referenceSourceImportParsing.js"; -import { analyzeParsedJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeParsedJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { parseRecoveredJavaScript } from "./javascriptSourceParser.js"; const analyze = (source: string, path = "main.cjs") => { diff --git a/src/domain/javascript/javascriptStaticAnalysisReturnedRequire.test.ts b/src/domain/javascript/javascriptStaticAnalysisReturnedRequire.test.ts index eed35ede1a..f4c26cd301 100644 --- a/src/domain/javascript/javascriptStaticAnalysisReturnedRequire.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisReturnedRequire.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; it.each([ ["async function get(){return module;}(await get())", true], diff --git a/src/domain/javascript/javascriptStaticAnalysisServiceWorkers.test.ts b/src/domain/javascript/javascriptStaticAnalysisServiceWorkers.test.ts index 952cb62c5c..9cb9430a0e 100644 --- a/src/domain/javascript/javascriptStaticAnalysisServiceWorkers.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisServiceWorkers.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const serviceWorkers = (source: string) => { const analysis = analyzeJavaScriptStaticSource(source); diff --git a/src/domain/javascript/javascriptStaticAnalysisSharedHelperWrites.test.ts b/src/domain/javascript/javascriptStaticAnalysisSharedHelperWrites.test.ts index 5a07f99fe3..c2245fa2d0 100644 --- a/src/domain/javascript/javascriptStaticAnalysisSharedHelperWrites.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisSharedHelperWrites.test.ts @@ -5,7 +5,7 @@ import changedUrl from "node:url"; import url from "node:url"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; it("observes that separate Node default imports share mutable helper slots", () => { expect(changedPath).toBe(path); diff --git a/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts b/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts index 6779d35bdd..b86b08fde0 100644 --- a/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; const sourceMapUrls = (source: string): readonly string[] => analyzeJavaScriptStaticSource(source).source_map_urls.map( diff --git a/src/domain/javascript/javascriptStaticAnalysisTypedMemberStores.test.ts b/src/domain/javascript/javascriptStaticAnalysisTypedMemberStores.test.ts index 333a852296..0dd2322fd7 100644 --- a/src/domain/javascript/javascriptStaticAnalysisTypedMemberStores.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisTypedMemberStores.test.ts @@ -6,7 +6,7 @@ import { expect, it } from "vitest"; import { semanticWriteTargets } from "./javascriptAstValues.js"; import { semanticIterationSources } from "./javascriptSemanticIterationSources.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; it.each( [false, true].flatMap((typed) => diff --git a/src/domain/javascript/javascriptStaticApiIdentity.test.ts b/src/domain/javascript/javascriptStaticApiIdentity.test.ts index c8d85e3194..99076e0a8d 100644 --- a/src/domain/javascript/javascriptStaticApiIdentity.test.ts +++ b/src/domain/javascript/javascriptStaticApiIdentity.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js"; const windowPrefix = diff --git a/src/domain/javascript/javascriptStaticCoverage.test.ts b/src/domain/javascript/javascriptStaticCoverage.test.ts index 002f6d7c2a..d3e1619459 100644 --- a/src/domain/javascript/javascriptStaticCoverage.test.ts +++ b/src/domain/javascript/javascriptStaticCoverage.test.ts @@ -1,5 +1,5 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js"; import { javaScriptStaticParseStatus, diff --git a/src/domain/javascript/javascriptSymbolicBindings.test.ts b/src/domain/javascript/javascriptSymbolicBindings.test.ts index abdafa1fb9..c45df0622e 100644 --- a/src/domain/javascript/javascriptSymbolicBindings.test.ts +++ b/src/domain/javascript/javascriptSymbolicBindings.test.ts @@ -1,13 +1,13 @@ import { expect, it, vi } from "vitest"; import * as t from "@babel/types"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; -import { indexJavaScriptBindingsSteps } from "./javascriptSemanticAnalysis.js"; +import { indexJavaScriptBindingsSteps } from "../../../tests/fixtures/javascriptSemanticIndex.js"; import { completeSemanticSteps, traverseJavaScriptAst, } from "./javascriptSemanticTraversal.js"; import { JavaScriptSymbolicBindings } from "./javascriptSymbolicBindings.js"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { symbolicTruth, symbolicVariants, diff --git a/src/domain/javascript/javascriptTypeErasure.test.ts b/src/domain/javascript/javascriptTypeErasure.test.ts index 7adadd7f57..2c8d6320aa 100644 --- a/src/domain/javascript/javascriptTypeErasure.test.ts +++ b/src/domain/javascript/javascriptTypeErasure.test.ts @@ -1,7 +1,7 @@ import { expect, it } from "vitest"; import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js"; it("keeps declared exports as source type links without runtime values", () => { const ir = analyzeJavaScriptSemantics(` diff --git a/src/domain/javascript/javascriptVitePreloadDependencies.test.ts b/src/domain/javascript/javascriptVitePreloadDependencies.test.ts index 3188c1dd96..acca98fd75 100644 --- a/src/domain/javascript/javascriptVitePreloadDependencies.test.ts +++ b/src/domain/javascript/javascriptVitePreloadDependencies.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"; import { analyzeJavaScriptStaticSource, analyzeParsedJavaScriptStaticSource, -} from "./javascriptStaticAnalysis.js"; +} from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; describe("Vite preload dependency recovery", () => { diff --git a/src/domain/offsetDateTime.test.ts b/src/domain/offsetDateTime.test.ts index 7a53785930..f6fcf8586f 100644 --- a/src/domain/offsetDateTime.test.ts +++ b/src/domain/offsetDateTime.test.ts @@ -3,7 +3,6 @@ import { z } from "zod"; import { addOffsetDateTimeMilliseconds, - inspectRecordedDateTime, offsetDateTimeSchema, readRecordedDateTime, readRecordedStart, @@ -11,18 +10,6 @@ import { utcDateTimeSchema, } from "./offsetDateTime.js"; -it.each([ - ["2026-02-30T09:00:00Z", "invalid"], - ["1900-02-29 09:00:00", "invalid"], - ["2026-10-20T24:00:00Z", "invalid"], - ["2026-10-20T09:00:00+24:00", "invalid"], - ["2026-10-20 09:00:00.0000000001", "valid"], - ["Tue, 20 Oct 2026 09:00:00 GMT", "unrecognized"], - ["2026-10-20", "unrecognized"], -])("classifies recorded spelling %s as %s", (text, kind) => { - expect(inspectRecordedDateTime(text).kind).toBe(kind); -}); - it.each([ ["000001", 0, 10, "000001"], ["999999", 0.001, 11, ""], @@ -280,6 +267,7 @@ it.each([ "Tue, 20 Oct 2026 09:00:00 GMT", "Tue, 20 Oct 2026 14:30:00 GMT+0530", "October 20, 2026 09:00:00", + "2026-10-20", ])("preserves native start interpretation for %s", (value) => { const milliseconds = Date.parse(value); expect(Number.isFinite(milliseconds)).toBe(true); @@ -293,6 +281,7 @@ it.each([ "2026-02-30T09:00:00Z", "1900-02-29 09:00:00", "2026-10-20T24:00:00Z", + "2026-10-20T09:00:00+24:00", "not a recorded start", ])("keeps invalid start %s unknown without native rollover", (value) => { expect(readRecordedStart(value)).toBeUndefined(); diff --git a/src/domain/offsetDateTime.ts b/src/domain/offsetDateTime.ts index e896cafd24..300e286f7e 100644 --- a/src/domain/offsetDateTime.ts +++ b/src/domain/offsetDateTime.ts @@ -66,7 +66,7 @@ export interface RecordedDateTimeReading { } /** Distinguish invalid supported spellings from formats this reader does not interpret. */ -export const inspectRecordedDateTime = ( +const inspectRecordedDateTime = ( value: string, ): | { readonly kind: "valid"; readonly reading: RecordedDateTimeReading } diff --git a/src/domain/process/processCapture.comparison.test.ts b/src/domain/process/processCapture.comparison.test.ts index 4a4d6d110c..e7fa16c341 100644 --- a/src/domain/process/processCapture.comparison.test.ts +++ b/src/domain/process/processCapture.comparison.test.ts @@ -10,7 +10,6 @@ import { parseProcessCapture } from "./processCaptureParsing.js"; import type { FileState } from "./processCapture.js"; import { - deriveProcessComparisonStatus, PROCESS_COMPARISON_DIMENSIONS, processCaptureComparisonSchema, } from "./processComparison.js"; @@ -641,11 +640,6 @@ it.each(PROCESS_COMPARISON_DIMENSIONS)( (dimension) => { const baseline = compareFullyObserved(emptyCapture(), emptyCapture()); const result = { ...baseline, [dimension]: "contradiction" as const }; - expect( - deriveProcessComparisonStatus( - PROCESS_COMPARISON_DIMENSIONS.map((name) => result[name]), - ), - ).toBe("contradiction"); expect(processCaptureComparisonSchema.safeParse(result).success).toBe( false, ); @@ -673,9 +667,6 @@ it.each([ ["contradiction", other], [other, "contradiction"], ] as const) { - expect(deriveProcessComparisonStatus([terminal, interaction])).toBe( - expected, - ); expect( processCaptureComparisonSchema.parse({ ...compareFullyObserved(emptyCapture(), emptyCapture()), @@ -684,6 +675,14 @@ it.each([ status: expected, }), ).toMatchObject({ status: expected }); + expect( + processCaptureComparisonSchema.safeParse({ + ...compareFullyObserved(emptyCapture(), emptyCapture()), + terminal, + interaction, + status: expected === "truncated" ? "contradiction" : "truncated", + }).success, + ).toBe(false); } }, ); diff --git a/src/domain/process/processComparison.ts b/src/domain/process/processComparison.ts index 05f5a94d98..3547a33190 100644 --- a/src/domain/process/processComparison.ts +++ b/src/domain/process/processComparison.ts @@ -54,7 +54,7 @@ export interface ProcessCaptureComparisonOptions { } /** Derive the overall comparison status from every process dimension. */ -export const deriveProcessComparisonStatus = ( +const deriveProcessComparisonStatus = ( dimensions: readonly ComparisonStatus[], ): ComparisonStatus => { if (dimensions.includes("truncated")) return "truncated"; diff --git a/src/domain/rawBinary.test.ts b/src/domain/rawBinary.test.ts index 708cb72748..458325baa5 100644 --- a/src/domain/rawBinary.test.ts +++ b/src/domain/rawBinary.test.ts @@ -5,7 +5,6 @@ import { admitRawBinary, parseRawBinaryEntryPoint, rawBinarySelectionError, - validateRawBinaryLength, } from "./rawBinary.js"; describe("headerless format hints", () => { @@ -97,7 +96,6 @@ describe("raw-binary admission", () => { }); }); it("rejects an empty file as a target refusal while naming the extent", () => { - expect(validateRawBinaryLength(0).ok).toBe(false); const result = admitRawBinary({ architecture: "x86", entryPoint: "0x1000", diff --git a/src/domain/rawBinary.ts b/src/domain/rawBinary.ts index 8a23541747..9790675744 100644 --- a/src/domain/rawBinary.ts +++ b/src/domain/rawBinary.ts @@ -53,9 +53,7 @@ export const parseRawBinaryEntryPoint = ( }; /** Reject empty or non-integral file extents for a headerless image. */ -export const validateRawBinaryLength = ( - length: number, -): Result => +const validateRawBinaryLength = (length: number): Result => Number.isSafeInteger(length) && length >= 1 ? ok(null) : err("raw-binary requires at least one file byte"); diff --git a/src/domain/webBundleAnalyzer.test.ts b/src/domain/webBundleAnalyzer.test.ts index f0ba4e2cf3..a5540116bf 100644 --- a/src/domain/webBundleAnalyzer.test.ts +++ b/src/domain/webBundleAnalyzer.test.ts @@ -1,11 +1,9 @@ -import { - projectAnalysisView, - analysisViewRequestSchema, -} from "./analysisView/analysisView.js"; +import { projectAnalysisView } from "./analysisView/analysisView.js"; +import { analysisViewRequestSchema } from "./analysisView/analysisViewRequest.js"; import { describe, expect, it } from "vitest"; import { createWebPageInspection } from "../../tests/support/browser/webPageInspection.js"; -import { analyzeJavaScriptStaticSource } from "./javascript/javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../tests/fixtures/javascriptStaticAnalysis.js"; import { analyzeCapturedWebScript } from "./webBundleAnalyzer.js"; import { analyzeWebBundleFixture } from "../../tests/support/browser/analyzeWebBundleFixture.js"; import { buildCapturedWebBundleAnalysis } from "./webBundleAnalyzerResult.js"; diff --git a/src/domain/windows/ntfsMft.ts b/src/domain/windows/ntfsMft.ts index 36eaba78f1..4e7306ff76 100644 --- a/src/domain/windows/ntfsMft.ts +++ b/src/domain/windows/ntfsMft.ts @@ -8,7 +8,7 @@ import { MFT_INLINE_ATTRIBUTE_REFERENCE_LIMIT, type MftFileReference, } from "./ntfsMftAttributes.js"; -export type { MftFileReference, MftFileName } from "./ntfsMftAttributes.js"; +export type { MftFileReference } from "./ntfsMftAttributes.js"; import { err, ok, type Result } from "../result.js"; import type { RegionDecodeFailure, diff --git a/src/dotnet/ManagedMemberInspector.test.ts b/src/dotnet/ManagedMemberInspector.test.ts index 468696357b..2e6a2fa7f8 100644 --- a/src/dotnet/ManagedMemberInspector.test.ts +++ b/src/dotnet/ManagedMemberInspector.test.ts @@ -18,11 +18,11 @@ import { edges } from "./ManagedMemberRows.js"; import { parseUserStrings } from "./ManagedUserStrings.js"; import { readCompressed, - signature, isParsedTokenInstruction, type DecodedMethod, type ManagedMethod, } from "./ManagedMemberInspectorCore.js"; +import { signature } from "../../tests/fixtures/managedSignature.js"; import { decodeInstructions } from "./ManagedMemberInstructionDecoder.js"; import { ManagedReaderFailure } from "./ManagedReaderFailure.js"; import { managedTypeReferences } from "./ManagedTypeReferences.js"; @@ -2637,6 +2637,8 @@ it("decodes generic instantiation member signatures as complete coverage", () => }); it.each([ + ["null TypeDef row", Buffer.from([0x06, 0x12, 0x00])], + ["out-of-range TypeRef row", Buffer.from([0x06, 0x12, 0x25])], ["reserved TypeDefOrRef tag", Buffer.from([0x06, 0x12, 0x07])], ["missing TypeSpec row", Buffer.from([0x06, 0x12, 0x06])], ])( diff --git a/src/dotnet/ManagedMemberInspectorCore.ts b/src/dotnet/ManagedMemberInspectorCore.ts index ad5ff130d9..37da72d1e8 100644 --- a/src/dotnet/ManagedMemberInspectorCore.ts +++ b/src/dotnet/ManagedMemberInspectorCore.ts @@ -1184,30 +1184,6 @@ const projectSignature = ( }; }; -/** Standalone signature decoding used by tests and signature identity utilities. */ -export const signature = ( - blob: Buffer, - rawSha256?: string, - typeName: SignatureTypeName = () => null, - displayTypeName?: SignatureDisplayTypeName, -): ManagedSignature => { - const operands: TypeOperand[] = []; - return projectSignature( - blob, - rawSha256, - typeName, - decodeSignature( - blob, - undefined, - operands, - undefined, - "signature", - displayTypeName, - ), - operands, - ); -}; - /** Decode a metadata-owned signature while validating referenced table rows. */ export const metadataSignature = ( blob: Buffer, diff --git a/src/dotnet/ManagedSignatureResolution.test.ts b/src/dotnet/ManagedSignatureResolution.test.ts index 974359945f..1f47ab3671 100644 --- a/src/dotnet/ManagedSignatureResolution.test.ts +++ b/src/dotnet/ManagedSignatureResolution.test.ts @@ -11,10 +11,11 @@ import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js"; import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js"; import { parseTypes, - signature, + methodGenericArity, readTypeDefinitions, type SignatureTypeName, } from "./ManagedMemberInspectorCore.js"; +import { signature } from "../../tests/fixtures/managedSignature.js"; import { metadataRowOffset, readManagedMetadataLayout, @@ -59,30 +60,15 @@ const named = return name === undefined ? null : nameDigest(name); }; -describe("layoutless signature coordinates", () => { - it.each([ - [0, 2, 0], - [0x25, 1, 9], - [0x07, undefined, 1], - ] as const)( - "keeps layoutless operand %s decoded without admitting a metadata row", - (value, table, row) => { - const name = vi.fn(() => Buffer.alloc(32)); - const result = signature( - Buffer.from([0x06, 0x12, value]), - undefined, - name, - ); - - expect(result.parse_status).toBe("decoded"); - expect(result.resolved_sha256).toMatch(/^[0-9a-f]{64}$/u); - expect(name).toHaveBeenCalledTimes(1); - expect(name).toHaveBeenCalledWith( - expect.objectContaining({ value, coordinate: { table, row } }), - ); - }, - ); -}); +it.each([0, 0x25, 0x07])( + "recovers method generic arity without admitting type coordinate %i", + (value) => { + const layout = metadataLayout(buildManagedPeFixture()); + expect( + methodGenericArity(layout, Buffer.from([0x10, 0x01, 0, 0x12, value])), + ).toBe(1); + }, +); describe("resolved member signature digests", () => { it("names modifier, class, and generic type operands instead of their rows", () => { diff --git a/src/ghidra/GhidraLoadImageValues.test.ts b/src/ghidra/GhidraLoadImageValues.test.ts index 9fbbec32fd..e3e5ad1c07 100644 --- a/src/ghidra/GhidraLoadImageValues.test.ts +++ b/src/ghidra/GhidraLoadImageValues.test.ts @@ -12,10 +12,7 @@ import { } from "../domain/native/nativeLoadImage.js"; import type { NativeLoadImageObservation } from "../domain/native/nativeLoadImage.js"; import { parseGhidraInventoryResult } from "./GhidraInventoryValues.js"; -import { - GHIDRA_DOS_LOAD_VERIFICATION_LIMITATION, - prepareGhidraDosLoadImage, -} from "./GhidraLoadImageValues.js"; +import { prepareGhidraDosLoadImage } from "./GhidraLoadImageValues.js"; it("retains the provider symbol without claiming DOS verification of it", () => { const fixture = fixtureDosLoadImage(); @@ -31,7 +28,7 @@ it("retains the provider symbol without claiming DOS verification of it", () => "provider_name", ); expect(result.value.limitations).toContain( - GHIDRA_DOS_LOAD_VERIFICATION_LIMITATION, + "Verification covers original/modified FileBytes, every initialized target-backed header/module mapping, relocation records and the external entry point. Ghidra relocation symbol names are retained observations, not independently verified. Mapping ends are inclusive.", ); }); diff --git a/src/ghidra/GhidraLoadImageValues.ts b/src/ghidra/GhidraLoadImageValues.ts index 558da111eb..5e0679d411 100644 --- a/src/ghidra/GhidraLoadImageValues.ts +++ b/src/ghidra/GhidraLoadImageValues.ts @@ -15,7 +15,7 @@ import { ghidraRawBinaryLanguage } from "./GhidraRawBinaryLanguage.js"; const sha256 = (bytes: Buffer): string => createHash("sha256").update(bytes).digest("hex"); const address = (offset: number): string => `0x${offset.toString(16)}`; -export const GHIDRA_DOS_LOAD_VERIFICATION_LIMITATION = +const GHIDRA_DOS_LOAD_VERIFICATION_LIMITATION = "Verification covers original/modified FileBytes, every initialized target-backed header/module mapping, relocation records and the external entry point. Ghidra relocation symbol names are retained observations, not independently verified. Mapping ends are inclusive."; const limitations = [ GHIDRA_DOS_LOAD_VERIFICATION_LIMITATION, diff --git a/src/ghidra/GhidraProjectSnapshot.ts b/src/ghidra/GhidraProjectSnapshot.ts index 6257dbc0c4..c14db9e3a8 100644 --- a/src/ghidra/GhidraProjectSnapshot.ts +++ b/src/ghidra/GhidraProjectSnapshot.ts @@ -30,7 +30,7 @@ import type { GhidraProjectSelection } from "../config/ghidraExistingProject.js" import { ghidraProgramNameSchema } from "../config/ghidraExistingProject.js"; /** Reason for a selected project whose content no longer matches its committed profile. */ -export const GHIDRA_PROJECT_PROFILE_MISMATCH = +const GHIDRA_PROJECT_PROFILE_MISMATCH = "Selected Ghidra project content no longer matches the profile committed by open_binary; when it changed is not established"; /** Classify file cancellation/cleanup before the generic provider boundary. */ diff --git a/src/ghidra/GhidraProvider.behavior.test.ts b/src/ghidra/GhidraProvider.behavior.test.ts index 0b55eb00df..bb1941ff46 100644 --- a/src/ghidra/GhidraProvider.behavior.test.ts +++ b/src/ghidra/GhidraProvider.behavior.test.ts @@ -80,8 +80,6 @@ import { elf, thinMach } from "../domain/binaryTarget.fixture.js"; import { GhidraProvider } from "./GhidraProvider.js"; import { GhidraClient, type GhidraTargetSnapshot } from "./GhidraClient.js"; import { - GHIDRA_ANNOTATIONS_RETAINED, - GHIDRA_SESSION_RETAINED, ghidraDecompileTimeoutRecovery, ghidraMissingProgramGuidance, ghidraStartupTimeoutGuidance, @@ -92,7 +90,6 @@ import { AnalysisArtifactChangedError, AnalysisCancelledError, } from "../domain/analysisErrorCore.js"; -import { GHIDRA_PROJECT_PROFILE_MISMATCH } from "./GhidraProjectSnapshot.js"; import type { GhidraInstallationHost } from "./GhidraInstallation.js"; import { GHIDRA_MUTATING_OPERATIONS, @@ -119,6 +116,11 @@ import { AnalysisSnapshotCache } from "../application/binary/AnalysisSnapshotCac import { createEvidence } from "../domain/evidence.js"; import { createEvidenceBundle } from "../domain/evidenceBundle.js"; +const GHIDRA_SESSION_RETAINED = "The Ghidra session remains usable."; +const GHIDRA_ANNOTATIONS_RETAINED = + "The active analysis session and annotations remain available."; +const GHIDRA_PROJECT_PROFILE_MISMATCH = + "Selected Ghidra project content no longer matches the profile committed by open_binary; when it changed is not established"; const INSTALL = "/opt/ghidra_12.1.4_PUBLIC"; afterEach(() => vi.restoreAllMocks()); const installationHost = ( diff --git a/src/ghidra/GhidraProviderClient.ts b/src/ghidra/GhidraProviderClient.ts index c35551a5d9..ba12ccf820 100644 --- a/src/ghidra/GhidraProviderClient.ts +++ b/src/ghidra/GhidraProviderClient.ts @@ -1837,10 +1837,10 @@ const projectObservedJvmMemoryFailure = ( }; /** Outcome addition that only a caller keeping the Ghidra session receives. */ -export const GHIDRA_SESSION_RETAINED = "The Ghidra session remains usable."; +const GHIDRA_SESSION_RETAINED = "The Ghidra session remains usable."; /** The same addition when a failed search leaves session annotations intact. */ -export const GHIDRA_ANNOTATIONS_RETAINED = +const GHIDRA_ANNOTATIONS_RETAINED = "The active analysis session and annotations remain available."; /** What a new Ghidra session repeats in the configured startup mode. */ diff --git a/src/gpu/GpuCodeObjectsProvider.ts b/src/gpu/GpuCodeObjectsProvider.ts index c587150c58..b8a30d548e 100644 --- a/src/gpu/GpuCodeObjectsProvider.ts +++ b/src/gpu/GpuCodeObjectsProvider.ts @@ -255,8 +255,8 @@ export class GpuCodeObjectsProvider implements GpuCodeObjectsPort { async #read( file: FileHandle, input: GpuCodeObjectsInput, - signal?: AbortSignal, - retainProgress?: (progress: { + signal: AbortSignal | undefined, + retainProgress: (progress: { inventory: GpuInventory; size: number; }) => void, @@ -272,7 +272,7 @@ export class GpuCodeObjectsProvider implements GpuCodeObjectsPort { "GPU source does not match the inventory's selected SHA-256", ); const inventory = new GpuInventory(input, signal); - retainProgress?.({ inventory, size }); + retainProgress({ inventory, size }); const format = await inventory.inspect(source); const after = await file.stat({ bigint: true }); const named = await lstat(input.path, { bigint: true }).catch( diff --git a/src/hopper/HopperFileOffset.ts b/src/hopper/HopperFileOffset.ts index 71a7d60f52..ab8c13bdfd 100644 --- a/src/hopper/HopperFileOffset.ts +++ b/src/hopper/HopperFileOffset.ts @@ -54,16 +54,16 @@ type Observation = z.infer; export interface HopperFileOffsetOwnership { readonly signal?: AbortSignal; /** Adopt the handle whose close rejected; the caller owns later retries. */ - readonly retainFailedClose?: (owner: OwnedFileHandle) => void; + readonly retainFailedClose: (owner: OwnedFileHandle) => void; /** Bind the typed cleanup marker to that same owner before it is returned. */ - readonly onCleanupMarker?: (marker: ProviderCleanupError) => void; + readonly onCleanupMarker: (marker: ProviderCleanupError) => void; } /** Bind native image-relative offsets to coordinates in the admitted source bytes. */ export async function mapHopperFileOffset( target: BinaryTarget, value: JsonValue, - ownership?: HopperFileOffsetOwnership, + ownership: HopperFileOffsetOwnership, ): Promise> { const parsed = observationSchema.safeParse(value); if (!parsed.success) @@ -89,10 +89,10 @@ export async function mapHopperFileOffset( return unavailable( "The selected database digest does not identify its original executable bytes. Open that executable to verify original-file coordinates; read_bytes still exposes the database's native bytes.", ); - const signal = ownership?.signal; + const signal = ownership.signal; if (signal?.aborted) return err(cancelled(partial, signal.reason)); const publish = (marker: ProviderCleanupError): ProviderCleanupError => { - ownership?.onCleanupMarker?.(marker); + ownership.onCleanupMarker(marker); return marker; }; const verification: OffsetVerification = { @@ -112,7 +112,7 @@ export async function mapHopperFileOffset( const captured = await snapshotFromPath( target.path, signal, - ownership?.retainFailedClose, + ownership.retainFailedClose, ); return coordinatesForSnapshot(verification, captured).result; } catch (cause: unknown) { @@ -158,7 +158,7 @@ async function snapshotFromPath( new OwnedFileHandle(handle), { ...(signal === undefined ? {} : { signal }), - ...(retainFailedClose === undefined ? {} : { retainFailedClose }), + retainFailedClose, }, async (file) => { if (!(await file.stat()).isFile()) diff --git a/src/hopper/HopperImageCopy.ts b/src/hopper/HopperImageCopy.ts index e710ad16e9..380633f5b0 100644 --- a/src/hopper/HopperImageCopy.ts +++ b/src/hopper/HopperImageCopy.ts @@ -17,9 +17,6 @@ export interface HopperImageCopyOptions { readonly resourceScope: ArtifactResourceScope; readonly fileSystem?: HopperImageCopyFileSystem; readonly signal?: AbortSignal; - readonly observer?: { - readonly afterChunkWritten?: (copiedBytes: number) => Promise; - }; } const defaultFileSystem: HopperImageCopyFileSystem = { open }; @@ -37,7 +34,6 @@ export const prepareHopperExecutableImage = async ( resourceScope: options.resourceScope, fileSystem: options.fileSystem, signal: options.signal, - observer: options.observer, selectBytes: (bytes) => { hash.update(bytes); return bytes; @@ -95,7 +91,6 @@ export const copyHopperSource = async (options: { readonly sourcePath: string; readonly destination: string; readonly signal: AbortSignal | undefined; - readonly observer: HopperImageCopyOptions["observer"] | undefined; readonly resourceScope: ArtifactResourceScope; readonly fileSystem: HopperImageCopyFileSystem | undefined; readonly selectBytes: (chunk: Buffer, position: number) => Buffer | undefined; @@ -136,7 +131,6 @@ const performHopperImageCopy = async (options: { readonly sourcePath: string; readonly destination: string; readonly signal: AbortSignal | undefined; - readonly observer: HopperImageCopyOptions["observer"] | undefined; readonly fileSystem: HopperImageCopyFileSystem | undefined; readonly selectBytes: (chunk: Buffer, position: number) => Buffer | undefined; }): Promise => { @@ -182,7 +176,6 @@ const performHopperImageCopy = async (options: { if (selected !== undefined && selected.length > 0) { await output.handle.writeFile(selected); copied += selected.length; - await options.observer?.afterChunkWritten?.(copied); } position += bytes.length; } diff --git a/src/hopper/HopperMachOImage.copyLifecycle.test.ts b/src/hopper/HopperMachOImage.copyLifecycle.test.ts index 548ad5d0aa..cb2c924d95 100644 --- a/src/hopper/HopperMachOImage.copyLifecycle.test.ts +++ b/src/hopper/HopperMachOImage.copyLifecycle.test.ts @@ -1,11 +1,12 @@ import { createHash } from "node:crypto"; -import { writeFile } from "node:fs/promises"; +import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { afterEach, expect, it, vi } from "vitest"; import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { createInspectionResources } from "../../tests/fixtures/inspectionResources.js"; +import { hopperImageCopyFileSystem } from "../../tests/fixtures/hopperImageCopyFileSystem.js"; import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js"; import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; import { BinaryTargetError } from "../domain/configurationErrors.js"; @@ -62,7 +63,7 @@ const createFileSystemWithCloseHook = async ( const prepareWithChunkWriteFailure = ( fixture: Awaited>, cause: Error, - options: Omit[4], "observer">, + options: Parameters[4], ) => prepareHopperMachOImage( fixture.sourcePath, @@ -71,17 +72,19 @@ const prepareWithChunkWriteFailure = ( fixture.destination, { ...options, - observer: { - afterChunkWritten: async () => { + fileSystem: hopperImageCopyFileSystem( + fixture.destination, + async () => { throw cause; }, - }, + options.fileSystem?.open, + ), }, ); -it("keeps an observer failure primary and closes real source handles", async () => { +it("keeps a partial write failure primary and closes real source handles", async () => { const fixture = await createThinCopyFixture("rea-hopper-copy-primary-"); - const writeFailure = new Error("controlled copy observer failure"); + const writeFailure = new Error("controlled partial write failure"); const prepared = await prepareWithChunkWriteFailure(fixture, writeFailure, { resourceScope: new ArtifactResourceScope(), }); @@ -89,6 +92,11 @@ it("keeps an observer failure primary and closes real source handles", async () if (prepared.ok) throw new Error("A failed copy was accepted"); expect(prepared.error.cause).toBe(writeFailure); expect(prepared.error.cleanupIncomplete).toBe(false); + const source = await readFile(fixture.sourcePath); + const partial = await readFile(fixture.destination); + expect(partial.length).toBeGreaterThan(0); + expect(partial.length).toBeLessThan(source.length); + expect(partial.equals(source.subarray(0, partial.length))).toBe(true); }); it("keeps a copy failure primary while retaining and retrying a real source handle", async () => { @@ -110,7 +118,7 @@ it("keeps a copy failure primary while retaining and retrying a real source hand }, ); const resourceScope = new ArtifactResourceScope(); - const writeFailure = new Error("controlled copy observer failure"); + const writeFailure = new Error("controlled partial write failure"); const prepared = await prepareWithChunkWriteFailure(fixture, writeFailure, { resourceScope, fileSystem, @@ -191,12 +199,13 @@ it("keeps cancellation observed before close primary and retains an output-close fixture.image, fixture.destination, { - fileSystem, + fileSystem: hopperImageCopyFileSystem( + fixture.destination, + async () => controller.abort(reason), + fileSystem.open, + ), resourceScope, signal: controller.signal, - observer: { - afterChunkWritten: async () => controller.abort(reason), - }, }, ); diff --git a/src/hopper/HopperMachOImage.test.ts b/src/hopper/HopperMachOImage.test.ts index 06019786f7..49a81f5530 100644 --- a/src/hopper/HopperMachOImage.test.ts +++ b/src/hopper/HopperMachOImage.test.ts @@ -15,6 +15,7 @@ import { join } from "node:path"; import { afterEach, expect, it, vi } from "vitest"; import { createInspectionResources } from "../../tests/fixtures/inspectionResources.js"; +import { hopperImageCopyFileSystem } from "../../tests/fixtures/hopperImageCopyFileSystem.js"; import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js"; import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { BinaryTargetError } from "../domain/configurationErrors.js"; @@ -445,12 +446,13 @@ it("retains the caller reason when a FAT copy is cancelled after its final write { resourceScope: new ArtifactResourceScope(), signal: controller.signal, - observer: { - afterChunkWritten: async (copiedBytes) => { + fileSystem: hopperImageCopyFileSystem( + destination, + async (copiedBytes) => { expect(copiedBytes).toBe(FAT32_SLICE_SIZE); controller.abort(reason); }, - }, + ), }, ); @@ -791,12 +793,13 @@ it("retains an owned partial thin image and exact cancellation reason after a wr { resourceScope: new ArtifactResourceScope(), signal: controller.signal, - observer: { - afterChunkWritten: async (copiedBytes) => { + fileSystem: hopperImageCopyFileSystem( + destination, + async (copiedBytes) => { announceWrite?.(copiedBytes); await writeGate; }, - }, + ), }, ); diff --git a/src/hopper/HopperMachOImage.ts b/src/hopper/HopperMachOImage.ts index 050ac35a2e..7bfcdea903 100644 --- a/src/hopper/HopperMachOImage.ts +++ b/src/hopper/HopperMachOImage.ts @@ -450,7 +450,7 @@ export const prepareHopperMachOImage = async ( resourceScope: options.resourceScope, fileSystem: options.fileSystem, }; - const { signal, observer } = options; + const { signal } = options; if (signal?.aborted) return err(new HopperCancelledError(signal.reason)); if (image.method === "thin_verified_copy") { const prepared = await prepareThinHopperMachOImage( @@ -458,7 +458,7 @@ export const prepareHopperMachOImage = async ( sourceSha256, image, destination, - { signal, observer, ...copyDependencies }, + { signal, ...copyDependencies }, ); return signal?.aborted && prepared.ok ? err(new HopperCancelledError(signal.reason)) @@ -474,7 +474,6 @@ export const prepareHopperMachOImage = async ( sourcePath, destination, signal, - observer, ...copyDependencies, selectBytes: (bytes, position) => { hash.update(bytes); @@ -533,12 +532,11 @@ const prepareThinHopperMachOImage = async ( destination: string, options: { readonly signal: AbortSignal | undefined; - readonly observer: HopperImageCopyOptions["observer"]; readonly resourceScope: ArtifactResourceScope; readonly fileSystem?: HopperImageCopyOptions["fileSystem"]; }, ): Promise> => { - const { signal, observer, resourceScope, fileSystem } = options; + const { signal, resourceScope, fileSystem } = options; const hash = createHash("sha256"); const prefix = Buffer.alloc(12); let prefixLength = 0; @@ -546,7 +544,6 @@ const prepareThinHopperMachOImage = async ( sourcePath, destination, signal, - observer, resourceScope, fileSystem, selectBytes: (bytes) => { diff --git a/src/ida/IdaMcpConnection.test.ts b/src/ida/IdaMcpConnection.test.ts index 5cd98628f1..7c2291f56c 100644 --- a/src/ida/IdaMcpConnection.test.ts +++ b/src/ida/IdaMcpConnection.test.ts @@ -9,7 +9,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { createIdaMcpConnection, decodeIdaToolResult, - decodeIdaJsonResource, } from "./IdaMcpConnection.js"; import { createIdaDiagnosticRedactor } from "./IdaDiagnosticRedactor.js"; import { @@ -189,6 +188,7 @@ const mockedHttpBoundary = ({ serverName, tools, call, + readResource, }: { readonly serverName: string; readonly tools: readonly { @@ -199,6 +199,7 @@ const mockedHttpBoundary = ({ request: MockedIdaHttpRequest, signal: AbortSignal | null | undefined, ) => Response | Promise; + readonly readResource?: (request: MockedIdaHttpRequest) => unknown; }): void => { vi.stubGlobal( "fetch", @@ -212,7 +213,7 @@ const mockedHttpBoundary = ({ id: request.id, result: { protocolVersion: request.params?.protocolVersion, - capabilities: { tools: {} }, + capabilities: { tools: {}, resources: {} }, serverInfo: { name: serverName, version: "1" }, }, }); @@ -228,6 +229,12 @@ const mockedHttpBoundary = ({ result: { tools }, }); if (request.method === "tools/call") return call(request, init?.signal); + if (request.method === "resources/read" && readResource !== undefined) + return jsonRpcResponse({ + jsonrpc: "2.0", + id: request.id, + result: readResource(request), + }); throw new Error(`Unexpected fixture request: ${request.method}`); }), ); @@ -451,6 +458,37 @@ describe("retained IDA diagnostic errors", () => { }); }); +const withIdaResource = async ( + response: unknown, + verify: ( + connection: ReturnType, + ) => Promise, +): Promise => { + mockedHttpBoundary({ + serverName: "ida-resource-fixture", + tools: [], + call: () => { + throw new Error("Unexpected tool call"); + }, + readResource: () => response, + }); + const connection = createIdaMcpConnection( + { + url: "http://127.0.0.1:12345/mcp", + headers: {}, + mode: "attached", + timeoutMs: 1000, + }, + {}, + ); + try { + await connection.connect(); + await verify(connection); + } finally { + await connection.close(); + } +}; + it.each([ { code: SdkErrorCode.ClientHttpNotImplemented, @@ -536,29 +574,54 @@ it.each([ "application/json", "Application/JSON", "application/json; charset=utf-8", -])("accepts SDK JSON metadata resources with media type %s", (mimeType) => { - expect( - decodeIdaJsonResource("ida://idb/metadata", { - contents: [ - { - uri: "ida://idb/metadata", - ...(mimeType === undefined ? {} : { mimeType }), - text: '{"sha256":"recorded"}', - }, - ], - }), - ).toEqual({ sha256: "recorded" }); -}); -it("keeps JSON resource identity, media type and JSON parsing strict", () => { +])( + "accepts SDK JSON metadata resources with media type %s", + async (mimeType) => { + await withIdaResource( + { + contents: [ + { + uri: "ida://idb/metadata", + ...(mimeType === undefined ? {} : { mimeType }), + text: '{"sha256":"recorded"}', + }, + ], + }, + async (connection) => { + if (connection.readResource === undefined) + throw new Error("IDA connection does not support resource reads"); + await expect( + connection.readResource("ida://idb/metadata"), + ).resolves.toEqual({ sha256: "recorded" }); + }, + ); + }, +); +it("keeps JSON resource identity, media type and JSON parsing strict", async () => { const resource = (uri: string, mimeType: string, text: string) => ({ contents: [{ uri, mimeType, text }], }); - for (const reply of [ - resource("wrong://resource", "application/json", "{}"), - resource("ida://idb/metadata", "text/plain", "{}"), - resource("ida://idb/metadata", "application/json", "not JSON"), - ]) - expect(() => decodeIdaJsonResource("ida://idb/metadata", reply)).toThrow(); + for (const [reply, message] of [ + [ + resource("wrong://resource", "application/json", "{}"), + /ida:\/\/idb\/metadata/u, + ], + [ + resource("ida://idb/metadata", "text/plain", "{}"), + /Expected a JSON resource media type/u, + ], + [ + resource("ida://idb/metadata", "application/json", "not JSON"), + /IDA MCP returned malformed JSON text/u, + ], + ] as const) + await withIdaResource(reply, async (connection) => { + if (connection.readResource === undefined) + throw new Error("IDA connection does not support resource reads"); + await expect( + connection.readResource("ida://idb/metadata"), + ).rejects.toThrow(message); + }); }); it("uses the actual SDK abort outcome to preserve the caller reason", async () => { diff --git a/src/ida/IdaMcpConnection.ts b/src/ida/IdaMcpConnection.ts index eb9ca880af..eda57d9f10 100644 --- a/src/ida/IdaMcpConnection.ts +++ b/src/ida/IdaMcpConnection.ts @@ -694,10 +694,7 @@ export const createIdaMcpConnection = ( }; /** Resource metadata follows the SDK's optional media type; content/identity stay strict. */ -export const decodeIdaJsonResource = ( - uri: string, - response: unknown, -): JsonValue => { +const decodeIdaJsonResource = (uri: string, response: unknown): JsonValue => { const resource = z .object({ contents: z diff --git a/src/il2cpp/Il2cppMetadataProvider.ts b/src/il2cpp/Il2cppMetadataProvider.ts index f7a7951917..1f89e6354d 100644 --- a/src/il2cpp/Il2cppMetadataProvider.ts +++ b/src/il2cpp/Il2cppMetadataProvider.ts @@ -73,9 +73,9 @@ const inferredReferenceForm = (architecture: string) => const failureForPrimary = ( facts: ArtifactFilePrimaryFacts, path: string, - field: "path" | "binary_path" = "path", - maximum = INPUT_BYTES, - metadataPath = path, + field: "path" | "binary_path", + maximum: number, + metadataPath: string, ): AnalysisError => { const { cause, origin } = facts; if (origin === "owned-or-classified" && cause instanceof AnalysisError) diff --git a/src/javascript/analysis/JavaScriptAnalysisTransfer.ts b/src/javascript/analysis/JavaScriptAnalysisTransfer.ts index a0065f4ec0..9b89cd5b80 100644 --- a/src/javascript/analysis/JavaScriptAnalysisTransfer.ts +++ b/src/javascript/analysis/JavaScriptAnalysisTransfer.ts @@ -133,7 +133,7 @@ const assertTransferByteBudget = (transferBytes: number): void => { const encodeTransferRecord = ( input: unknown, previousBytes: number, - transferBytes = TRANSFER_BYTES, + transferBytes: number, ) => { const record = recordSchema.parse(input); const line = `${JSON.stringify(record)}\n`; diff --git a/src/javascript/analysis/JavaScriptAnalysisWorkerSchemas.test.ts b/src/javascript/analysis/JavaScriptAnalysisWorkerSchemas.test.ts index 50475dec7c..a908f2dce6 100644 --- a/src/javascript/analysis/JavaScriptAnalysisWorkerSchemas.test.ts +++ b/src/javascript/analysis/JavaScriptAnalysisWorkerSchemas.test.ts @@ -3,7 +3,7 @@ import { expect, it } from "vitest"; import { analyzeJavaScriptStaticSource, analyzeParsedJavaScriptStaticSource, -} from "../../domain/javascript/javascriptStaticAnalysis.js"; +} from "../../../tests/fixtures/javascriptStaticAnalysis.js"; import { parseJavaScriptSource } from "../../domain/javascript/javascriptSourceParser.js"; const analyzeCommonJsSource = (source: string) => { diff --git a/src/kuna/KunaProjectReader.ts b/src/kuna/KunaProjectReader.ts index 6198762376..49c5dc77b6 100644 --- a/src/kuna/KunaProjectReader.ts +++ b/src/kuna/KunaProjectReader.ts @@ -56,9 +56,9 @@ export interface KunaProjectText { } /** Session scope that adopts a span descriptor before the read opens it. */ -export interface KunaSpanOwnership { - readonly retainFailedClose?: (path: string, owner: OwnedFileHandle) => void; - readonly bindSpanCleanup?: ( +interface KunaSpanOwnership { + readonly retainFailedClose: (path: string, owner: OwnedFileHandle) => void; + readonly bindSpanCleanup: ( owner: OwnedFileHandle, marker: ProviderCleanupError, ) => void; @@ -66,13 +66,13 @@ export interface KunaSpanOwnership { * Check retained cleanup and open one descriptor. The session holds this * together with finishSpan so a failed close is retained before the next open. */ - readonly admitSpan?: ( + readonly admitSpan: ( operation: string, signal: AbortSignal | undefined, open: () => Promise, ) => Promise>; /** Close one admitted descriptor before another span may open. */ - readonly finishSpan?: (finish: () => Promise) => Promise; + readonly finishSpan: (finish: () => Promise) => Promise; } const readSpan = async ( @@ -105,10 +105,9 @@ const readSpan = async ( return err( new AnalysisCancelledError(operation, { cause: cancelled.reason }), ); - const opened = - ownership.admitSpan === undefined - ? ok(await open(path, "r")) - : await ownership.admitSpan(operation, signal, () => open(path, "r")); + const opened = await ownership.admitSpan(operation, signal, () => + open(path, "r"), + ); if (!opened.ok) return opened; const owner = new OwnedFileHandle(opened.value); let read: Result; @@ -146,12 +145,8 @@ const readSpan = async ( // A failed read keeps its own diagnostic. A completed read records // cancellation when the caller aborts before the queued close settles. ...(read.ok && signal !== undefined ? { signal } : {}), - ...(ownership.retainFailedClose === undefined - ? {} - : { - retainFailedClose: (failed) => - ownership.retainFailedClose?.(path, failed), - }), + retainFailedClose: (failed) => + ownership.retainFailedClose(path, failed), }, async () => { if ( @@ -165,10 +160,7 @@ const readSpan = async ( }, ); try { - const value = - ownership.finishSpan === undefined - ? await finish() - : await ownership.finishSpan(finish); + const value = await ownership.finishSpan(finish); return ok(value); } catch (cause: unknown) { if (spanCancellation !== undefined && cause === spanCancellation) @@ -261,7 +253,7 @@ const spanCloseFailure = ( }, { operation, cause: failure }, ); - ownership.bindSpanCleanup?.(failure.owner, marker); + ownership.bindSpanCleanup(failure.owner, marker); if (failure.completionCancellation !== undefined) return analysisErrorWithCleanupFailure( new AnalysisCancelledError(operation, { diff --git a/src/model/ModelArtifactProvider.ts b/src/model/ModelArtifactProvider.ts index b4777a7c7c..a5b6512aad 100644 --- a/src/model/ModelArtifactProvider.ts +++ b/src/model/ModelArtifactProvider.ts @@ -240,8 +240,8 @@ export class ModelArtifactProvider implements ModelArtifactPort { async #read( file: FileHandle, input: ModelArtifactInput, - signal?: AbortSignal, - retainProgress?: (progress: ModelArtifactPartial) => void, + signal: AbortSignal | undefined, + retainProgress: (progress: ModelArtifactPartial) => void, ): Promise { const initial = await file.stat({ bigint: true }); if (initial.size > BigInt(Number.MAX_SAFE_INTEGER)) @@ -282,7 +282,7 @@ export class ModelArtifactProvider implements ModelArtifactPort { "No model behavior or tensor payload values were interpreted.", ], }; - retainProgress?.(partial); + retainProgress(partial); const parsed = format === "gguf" ? await new GgufHeader(reader).read({ diff --git a/src/native/LldbRetainedObservations.ts b/src/native/LldbRetainedObservations.ts index 3fa72d86b4..ec7d3376d0 100644 --- a/src/native/LldbRetainedObservations.ts +++ b/src/native/LldbRetainedObservations.ts @@ -102,9 +102,7 @@ export const projectLldbPartialObservation = (input: { }); /** Parse completed JSONL records while retaining valid records around bad ones. */ -export const parseLldbObservationJournal = ( - bytes: Uint8Array, -): ParsedLldbJournal => { +const parseLldbObservationJournal = (bytes: Uint8Array): ParsedLldbJournal => { const hasTornTail = bytes.length > 0 && bytes[bytes.length - 1] !== 0x0a; const events: ParsedLldbJournal["events"] = []; const otherStops: string[] = []; diff --git a/src/native/MachoBinaryProjection.ts b/src/native/MachoBinaryProjection.ts index 573786c144..9daa51d26d 100644 --- a/src/native/MachoBinaryProjection.ts +++ b/src/native/MachoBinaryProjection.ts @@ -136,7 +136,7 @@ const STRING_FIELDS = new Map([ [0x39, "triple"], ]); -const bytesText = (input: Uint8Array, fixed = false) => { +const bytesText = (input: Uint8Array, fixed: boolean) => { const end = fixed ? input.indexOf(0) : -1; const bytes = end < 0 ? input : input.subarray(0, end); try { diff --git a/src/native/NativeMachoInspection.fixture.ts b/src/native/NativeMachoInspection.fixture.ts index dac0521bf8..04e8c92f4a 100644 --- a/src/native/NativeMachoInspection.fixture.ts +++ b/src/native/NativeMachoInspection.fixture.ts @@ -1,5 +1,6 @@ import { buildVersionCommand, + CPU, dylibCommand, LC, machoImage, @@ -7,6 +8,88 @@ import { segmentCommand, } from "../artifacts/apple/MachoImage.fixture.js"; import { readMachoCommandNames } from "./MachoCommandNames.js"; +import { + assertSelectedMachoSlice, + resolveSelectedMachoSlice, +} from "./NativeMachoSelectedSlice.js"; +import type { NativeMachoSource } from "./NativeMachoSource.js"; +import type { ResolvedMachoSlice } from "./NativeMachoSelectedSlice.js"; + +interface InspectionFatRow { + readonly offset: number; + readonly size: number; + readonly alignment: number; + readonly cpu: { readonly type: number; readonly subtype: number }; + readonly image: Uint8Array; +} + +/** Build independent on-disk FAT declarations for the resolver exercised by inspection. */ +export const inspectionFatContext = ( + image = inspectionImage(), + rows: readonly InspectionFatRow[] = [ + { + offset: 16384, + size: 4096, + alignment: 14, + cpu: CPU.x86_64, + image: machoImage({ cpu: CPU.x86_64 }), + }, + { offset: 32768, size: 8192, alignment: 14, cpu: CPU.arm64, image }, + ], +) => { + const bytes = Buffer.alloc( + Math.max(...rows.map(({ offset, size }) => offset + size)), + ); + bytes.writeUInt32BE(0xcafebabe, 0); + bytes.writeUInt32BE(rows.length, 4); + rows.forEach(({ offset, size, alignment, cpu, image: slice }, index) => { + if (slice.length > size) + throw new Error("Mach-O fixture slice exceeds its FAT row"); + const at = 8 + index * 20; + bytes.writeUInt32BE(cpu.type, at); + bytes.writeUInt32BE(cpu.subtype >>> 0, at + 4); + bytes.writeUInt32BE(offset, at + 8); + bytes.writeUInt32BE(size, at + 12); + bytes.writeUInt32BE(alignment, at + 16); + bytes.set(slice, offset); + }); + return inspectionFatBytesContext(bytes); +}; + +/** Inspect and resolve from the same FAT bytes supplied by a test. */ +export const inspectionFatBytesContext = (bytes: Uint8Array) => { + const source: NativeMachoSource = { + bytes: bytes.length, + readSlice: async (_start, _size, offset, length) => + bytes.subarray(offset, offset + length), + }; + return { + resolveSlice: (selected: Parameters[1]) => + resolveSelectedMachoSlice(source, selected, 10000), + readCommandNames: async ( + offset: number, + size: number | null, + selected: ResolvedMachoSlice | undefined, + ) => { + const slice = bytes.subarray( + offset, + offset + (size ?? bytes.length - offset), + ); + const binary = await readMachoCommandNames( + readerOf(slice), + slice.length, + 1024 * 1024, + 10000, + ); + if (size !== null) { + if (selected === undefined) + throw new Error("FAT fixture slice was not resolved"); + assertSelectedMachoSlice(source, selected, binary); + } + return binary; + }, + }; +}; /** Synthetic on-disk command records consumed by the production binary reader. */ export const inspectionImage = ( diff --git a/src/native/NativeMachoInspection.test.ts b/src/native/NativeMachoInspection.test.ts index bb5a0d7850..55b9898ad2 100644 --- a/src/native/NativeMachoInspection.test.ts +++ b/src/native/NativeMachoInspection.test.ts @@ -7,7 +7,6 @@ import { fatImage, machoImage, lazyLoadDylibInfoCommand, - readerOf, } from "../artifacts/apple/MachoImage.fixture.js"; import { AnalysisCancelledError, @@ -22,8 +21,9 @@ import { inspectNativeMacho, type MachoEncryptionRead, } from "./NativeMachoInspection.js"; -import { readMachoCommandNames } from "./MachoCommandNames.js"; import { + inspectionFatBytesContext, + inspectionFatContext, inspectionImage, inspectionReader, unreadEncryption, @@ -48,12 +48,19 @@ const capture = (tool: string, stdout: string): NativeCommandCapture => ({ signal: null, }); +// Some selector tests stop on lipo admission or a cancelled command; thin +// images also never need a FAT slice resolver. +const unexpectedSliceResolution = (): never => { + throw new Error("Unexpected FAT slice resolution"); +}; + const inspect = async (lipo: string, architecture: "x86" | "arm64") => { const commands: { readonly tool: string; readonly args: readonly string[]; }[] = []; const result = await inspectNativeMacho({ + resolveSlice: unexpectedSliceResolution, readCommandNames: inspectionReader(), target: { kind: "executable", @@ -97,6 +104,7 @@ describe("admitted Mach-O slice tools", () => { readonly args: readonly string[]; }[] = []; const result = await inspectNativeMacho({ + resolveSlice: unexpectedSliceResolution, readCommandNames: inspectionReader(), target: { kind: "executable", @@ -193,17 +201,15 @@ describe("Mach-O tool architecture selection", () => { .join(""); const runner = new NativeFixtureRunner({ lipo }); const readOffsets: number[] = []; + const context = inspectionFatBytesContext(fat); const result = await inspectNativeMacho({ + resolveSlice: context.resolveSlice, target: nativeMachoTarget("/fixture/app"), run: (tool, args) => runner.run(tool, args), readEncryption: unreadEncryption, - readCommandNames: (offset, size) => { + readCommandNames: (offset, size, selected) => { readOffsets.push(offset); - const image = fat.subarray( - offset, - offset + (size ?? fat.length - offset), - ); - return readMachoCommandNames(readerOf(image), image.length, 1024, 100); + return context.readCommandNames(offset, size, selected); }, invocation: (capture) => ({ tool: capture.tool, @@ -287,10 +293,16 @@ describe("Mach-O tool architecture selection", () => { }); describe("Mach-O inspection container format", () => { - const runInspection = async (lipo: string) => { + const runInspection = async ( + lipo: string, + context = inspectionFatContext(), + ) => { const runner = new NativeFixtureRunner({ lipo }); return inspectNativeMacho({ - readCommandNames: inspectionReader(), + resolveSlice: context.resolveSlice, + readCommandNames: lipo.startsWith("Non-fat") + ? inspectionReader() + : context.readCommandNames, target: nativeMachoTarget("/fixture/app"), run: (tool, args) => runner.run(tool, args), invocation: (capture) => ({ @@ -316,6 +328,22 @@ describe("Mach-O inspection container format", () => { "architecture arm64\n cputype CPU_TYPE_ARM64\n cpusubtype CPU_SUBTYPE_ARM64_ALL\n offset 32768\n size 4096\n align 2^12\n"; const result = await runInspection( `Fat header in: /fixture/app\nfat_magic 0xcafebabe\nnfat_arch 2\n${reversed ? all + v8 : v8 + all}`, + inspectionFatContext(undefined, [ + { + offset: 16384, + size: 4096, + alignment: 12, + cpu: { type: CPU.arm64.type, subtype: 1 }, + image: machoImage({ cpu: { type: CPU.arm64.type, subtype: 1 } }), + }, + { + offset: 32768, + size: 4096, + alignment: 12, + cpu: CPU.arm64, + image: inspectionImage(), + }, + ]), ); expect(result.ok).toBe(true); if (!result.ok) return; @@ -333,7 +361,7 @@ describe("Mach-O inspection container format", () => { it.each([ { framing: "one-slice FAT", - lipo: `Fat header in: /fixture/app\nfat_magic 0xcafebabe\nnfat_arch 1\narchitecture arm64\n cputype CPU_TYPE_ARM64\n cpusubtype CPU_SUBTYPE_ARM64_ALL\n offset 4096\n size 128\n align 2^12\n`, + lipo: `Fat header in: /fixture/app\nfat_magic 0xcafebabe\nnfat_arch 1\narchitecture arm64\n cputype CPU_TYPE_ARM64\n cpusubtype CPU_SUBTYPE_ARM64_ALL\n offset 4096\n size 8192\n align 2^12\n`, expected: "mach-o-universal", }, { @@ -342,7 +370,18 @@ describe("Mach-O inspection container format", () => { expected: "mach-o", }, ] as const)("reports parsed $framing framing", async ({ lipo, expected }) => { - const result = await runInspection(lipo); + const result = await runInspection( + lipo, + inspectionFatContext(undefined, [ + { + offset: 4096, + size: 8192, + alignment: 12, + cpu: CPU.arm64, + image: inspectionImage(), + }, + ]), + ); if (!result.ok) throw result.error; expect(result.value.result).toMatchObject({ format: expected }); }); @@ -352,6 +391,7 @@ describe("Mach-O inspection container format", () => { lipo: "Non-fat file: /fixture/app is architecture: arm64\n", }); const result = await inspectNativeMacho({ + resolveSlice: unexpectedSliceResolution, readCommandNames: inspectionReader( inspectionImage({ commands: [ @@ -406,9 +446,7 @@ describe("Mach-O UUID reconciliation", () => { dwarfdump, }); const result = await inspectNativeMacho({ - readCommandNames: inspectionReader( - inspectionImage({ uuids: binaryUuids }), - ), + ...inspectionFatContext(inspectionImage({ uuids: binaryUuids })), target: nativeMachoTarget(targetPath), run: (tool, args) => runner.run(tool, args), readEncryption: unreadEncryption, @@ -538,7 +576,7 @@ describe("Mach-O slice command scheduling", () => { const inspectWith = (run: ReturnType["run"]) => inspectNativeMacho({ - readCommandNames: inspectionReader(), + ...inspectionFatContext(), target: nativeMachoTarget("/fixture/app"), run, readEncryption: unreadEncryption, @@ -617,7 +655,7 @@ describe("Mach-O encryption reporting", () => { const inspectEncryption = async (encryption: MachoEncryptionRead) => { const runner = new NativeFixtureRunner(); const result = await inspectNativeMacho({ - readCommandNames: inspectionReader(), + ...inspectionFatContext(), target: nativeMachoTarget("/fixture/app"), run: (tool, args) => runner.run(tool, args), readEncryption: () => Promise.resolve(encryption), @@ -722,7 +760,7 @@ it.each(["10", "DSYM", "2"])( "section '__TEXT/__text' has offset=0 but is not a zero-fill section type\n"; const runner = new NativeFixtureRunner(); const result = await inspectNativeMacho({ - readCommandNames: inspectionReader( + ...inspectionFatContext( inspectionImage({ fileType: fileType === "2" ? 2 : 10 }), ), target: nativeMachoTarget("/fixture/debug-companion"), @@ -825,7 +863,7 @@ describe("dSYM diagnostic admission", () => { ])("preserves $name", async ({ stdout, stderr, accepted }) => { const runner = new NativeFixtureRunner(); const result = await inspectNativeMacho({ - readCommandNames: inspectionReader(inspectionImage({ fileType: 10 })), + ...inspectionFatContext(inspectionImage({ fileType: 10 })), target: nativeMachoTarget("/fixture/debug-companion"), readEncryption: unreadEncryption, run: async (tool, args, _signal, acceptNonZeroIf) => { diff --git a/src/native/NativeMachoInspection.ts b/src/native/NativeMachoInspection.ts index 5fc1371c6b..764598f3b1 100644 --- a/src/native/NativeMachoInspection.ts +++ b/src/native/NativeMachoInspection.ts @@ -78,7 +78,7 @@ interface NativeMachoInspectionContext { readonly invocation: ( capture: NativeCommandCapture, ) => NativeCommandInvocation; - readonly resolveSlice?: ( + readonly resolveSlice: ( selected: LipoArchitecture, ) => Promise; /** Borrow binary framing from the same identity-bound target as the tools. */ @@ -230,16 +230,7 @@ export const inspectNativeMacho = async ( const range = format === "thin" ? { offset: 0, size: null } - : context.resolveSlice !== undefined - ? await context.resolveSlice(selected!) - : { offset: selected!.file_offset, size: selected!.size }; - if (range.offset === null || (format === "fat" && range.size === null)) - return err( - new AnalysisOutputError( - "inspect_macho", - "The selected binary slice range is unavailable", - ), - ); + : await context.resolveSlice(selected!); if ( format === "fat" && (selected!.file_offset === null || selected!.size === null) diff --git a/src/native/NativeMachoVtoolRefusal.test.ts b/src/native/NativeMachoVtoolRefusal.test.ts index 1dda724b52..5c5895979e 100644 --- a/src/native/NativeMachoVtoolRefusal.test.ts +++ b/src/native/NativeMachoVtoolRefusal.test.ts @@ -11,6 +11,7 @@ import { VTOOL_CONTAINER_REFUSAL_LIMITATION, } from "./NativeMachoInspection.js"; import { + inspectionFatContext, inspectionReader, unreadEncryption, } from "./NativeMachoInspection.fixture.js"; @@ -27,8 +28,11 @@ const inspect = async (options: { ? { lipo: `Non-fat file: ${path} is architecture: arm64\n` } : {}, ); + const context = inspectionFatContext(); return inspectNativeMacho({ - readCommandNames: inspectionReader(), + resolveSlice: context.resolveSlice, + readCommandNames: + options.format === "thin" ? inspectionReader() : context.readCommandNames, readEncryption: unreadEncryption, target: nativeMachoTarget(path), run: async (tool, args, _signal, acceptNonZeroIf) => { diff --git a/src/native/android/AndroidJniLibrary.ts b/src/native/android/AndroidJniLibrary.ts index 2a3292d61b..39197fc004 100644 --- a/src/native/android/AndroidJniLibrary.ts +++ b/src/native/android/AndroidJniLibrary.ts @@ -32,9 +32,7 @@ const MAXIMUM_TABLE_ENTRIES = 65_536; const hex = (value: bigint): string => `0x${value.toString(16)}`; /** Strict Modified UTF-8: no four-byte UTF-8, overlong forms or replacements. */ -export const decodeJniModifiedUtf8 = ( - bytes: Uint8Array, -): string | undefined => { +const decodeJniModifiedUtf8 = (bytes: Uint8Array): string | undefined => { const chunks: string[] = []; let units: number[] = []; for (let at = 0; at < bytes.length;) { diff --git a/src/native/apple/AppleIpsProvider.ts b/src/native/apple/AppleIpsProvider.ts index a6f83d768e..9e26899ec0 100644 --- a/src/native/apple/AppleIpsProvider.ts +++ b/src/native/apple/AppleIpsProvider.ts @@ -258,7 +258,7 @@ const resourceRecovery = ( const appleIpsFailurePrimary = ( facts: ArtifactFilePrimaryFacts, input: InspectAppleIpsInput, - field: "path" | "join_path" = "path", + field: "path" | "join_path", ): AnalysisError => { const { cause, origin } = facts; if (origin === "owned-or-classified" && cause instanceof AnalysisError) diff --git a/src/native/exports/NativeExportsView.test.ts b/src/native/exports/NativeExportsView.test.ts index 1415492c19..a23c434138 100644 --- a/src/native/exports/NativeExportsView.test.ts +++ b/src/native/exports/NativeExportsView.test.ts @@ -7,10 +7,10 @@ import { import { freezeJsonSnapshot } from "../../domain/immutableJson.js"; import { digestCanonicalValue } from "../../domain/canonicalDigest.js"; import { - analysisViewRequestSchema, projectAnalysisView, type AnalysisViewParent, } from "../../domain/analysisView/analysisView.js"; +import { analysisViewRequestSchema } from "../../domain/analysisView/analysisViewRequest.js"; const artifact = { path: "/fixtures/exports", diff --git a/src/native/pe/PeCertificateAsn1.ts b/src/native/pe/PeCertificateAsn1.ts index 29ff27ebf6..f86521101f 100644 --- a/src/native/pe/PeCertificateAsn1.ts +++ b/src/native/pe/PeCertificateAsn1.ts @@ -113,7 +113,7 @@ const chargeDecodedName = (budget: DecodedNameBudget, bytes: number): void => { }; /** Decode an OBJECT IDENTIFIER content buffer as dotted decimal. */ -export const decodeOid = (content: Buffer): string => { +const decodeOid = (content: Buffer): string => { const decoded = decodeAsn1Oid(content, { bytes: MAX_OID_BYTES, arcBytes: MAX_OID_ARC_BYTES, diff --git a/src/native/pe/PeCertificateParser.test.ts b/src/native/pe/PeCertificateParser.test.ts index 4742cd999f..eed0e0b526 100644 --- a/src/native/pe/PeCertificateParser.test.ts +++ b/src/native/pe/PeCertificateParser.test.ts @@ -16,15 +16,11 @@ import { MAX_DECODED_NAME_BYTES, MAX_OID_ARC_BYTES, MAX_OID_BYTES, - decodeOid, parseCertificate, PeCertificateAsn1Failure, readAsn1, } from "./PeCertificateAsn1.js"; -import { - parsePeCertificates, - UNSIGNED_ATTRIBUTES_LIMITATION, -} from "./PeCertificateParser.js"; +import { parsePeCertificates } from "./PeCertificateParser.js"; const defaults = inspectPeCertificatesInputSchema.parse({ path: "/artifacts/app.exe", @@ -96,7 +92,9 @@ it("reports unsigned attribute OIDs and incomplete nested coverage", () => { status: "partial", certificates: 1, }); - expect(report.limitations).toContain(UNSIGNED_ATTRIBUTES_LIMITATION); + expect(report.limitations).toContain( + "Signer unsigned attribute contents are not decoded; nested signatures and timestamp token certificates and times may be omitted.", + ); const control = parse(peCertificateFixture(plus)); expect( control.entries[0]?.pkcs7?.signers[0]?.unsigned_attribute_oids, @@ -687,11 +685,25 @@ it("rejects headers whose section table does not fit SizeOfHeaders", () => { }); it("decodes OBJECT IDENTIFIER arcs without truncation", () => { - expect(decodeOid(Buffer.from([0x88, 0x37, 0x03]))).toBe("2.999.3"); - expect(decodeOid(Buffer.from([0x69, 0x90, 0x80, 0x80, 0x80, 0x00]))).toBe( - "2.25.4294967296", + const algorithms = [ + [0x88, 0x37, 0x03], + [0x69, 0x90, 0x80, 0x80, 0x80, 0x00], + [0x2a, 0x86, 0x48], + ].map((oid) => derSeq(der(0x06, Buffer.from(oid)))); + const signer = derSeq( + derInt(1), + derSeq(derSeq(), derInt(1)), + SHA256_ALG, + SHA256_ALG, + der(0x04, Buffer.from([1])), ); - expect(decodeOid(Buffer.from([0x2a, 0x86, 0x48]))).toBe("1.2.840"); + const payload = contentInfo( + derSeq(derInt(1), derSet(...algorithms), derSeq(DATA_OID), derSet(signer)), + ); + expect( + parse(peCertificateFixture(false, payload)).entries[0]?.pkcs7 + ?.digest_algorithm_oids, + ).toEqual(["2.999.3", "2.25.4294967296", "1.2.840"]); }); it("rejects an oversized OBJECT IDENTIFIER before growing an arc", () => { @@ -704,7 +716,11 @@ it("rejects an oversized OBJECT IDENTIFIER before growing an arc", () => { const arc = Buffer.alloc(MAX_OID_ARC_BYTES + 1, 0x80); arc[0] = 0x81; arc[arc.length - 1] = 0x00; - expect(() => decodeOid(Buffer.concat([Buffer.from([0x2a]), arc]))).toThrow( + const arcOid = derSeq( + der(0x06, Buffer.concat([Buffer.from([0x2a]), arc])), + der(0xa0, derSeq(derInt(1))), + ); + expect(() => parse(peCertificateFixture(false, arcOid))).toThrow( /arc exceeds/u, ); }); diff --git a/src/native/pe/PeCertificateParser.ts b/src/native/pe/PeCertificateParser.ts index 53dd393395..403f467239 100644 --- a/src/native/pe/PeCertificateParser.ts +++ b/src/native/pe/PeCertificateParser.ts @@ -43,7 +43,7 @@ const LIMITATIONS = [ const TELETEX_LIMITATION = "TeletexString values use OpenSSL's ISO-8859-1 compatibility mapping; this is not a full T.61 decoder."; -export const UNSIGNED_ATTRIBUTES_LIMITATION = +const UNSIGNED_ATTRIBUTES_LIMITATION = "Signer unsigned attribute contents are not decoded; nested signatures and timestamp token certificates and times may be omitted."; const ABSENT_LIMITATIONS = [ diff --git a/src/native/pwndbg/PwndbgCoreContext.ts b/src/native/pwndbg/PwndbgCoreContext.ts index cc04055542..4fab98a199 100644 --- a/src/native/pwndbg/PwndbgCoreContext.ts +++ b/src/native/pwndbg/PwndbgCoreContext.ts @@ -74,7 +74,6 @@ export const inspectPwndbgCore = async ({ options, runCommand, timeoutMs, - probePath, producerLimits = resolvePwntoolsLimits(), }: { readonly environment: Readonly; @@ -87,11 +86,6 @@ export const inspectPwndbgCore = async ({ /** Effective `pwntools_timeout_ms` for this debugger command. */ readonly timeoutMs: number; readonly producerLimits?: PwntoolsProducerLimits; - readonly probePath?: ( - path: string, - kind: "file" | "directory", - mode: number, - ) => Promise; }): Promise => { const gdb = environment.REA_PWNDBG_GDB ?? "/usr/bin/gdb"; const entry = environment.REA_PWNDBG_GDBINIT ?? ""; @@ -120,18 +114,12 @@ export const inspectPwndbgCore = async ({ if (!isAbsolute(path)) throw new Error("Expected an absolute caller-supplied path."); const mode = key === "REA_PWNDBG_GDB" ? constants.X_OK : constants.R_OK; - if (probePath !== undefined) { - await probePath(path, kind, mode); - } else { - await access(path, mode); - checkCancelled(); - const information = await stat(path); - checkCancelled(); - if ( - kind === "file" ? !information.isFile() : !information.isDirectory() - ) - throw new Error(`Expected a ${kind}.`); - } + await access(path, mode); + checkCancelled(); + const information = await stat(path); + checkCancelled(); + if (kind === "file" ? !information.isFile() : !information.isDirectory()) + throw new Error(`Expected a ${kind}.`); checkCancelled(); } catch (cause: unknown) { if (cause instanceof AnalysisCancelledError) throw cause; diff --git a/src/process/LinuxCgroupMemory.test.ts b/src/process/LinuxCgroupMemory.test.ts index 1e42c1d687..e69862beb8 100644 --- a/src/process/LinuxCgroupMemory.test.ts +++ b/src/process/LinuxCgroupMemory.test.ts @@ -3,88 +3,19 @@ import { join } from "node:path"; import { expect, it } from "vitest"; import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js"; -import { - linuxCgroupDirectory, - parseLinuxCgroup2Mounts, - parseLinuxCgroupMemoryMax, - parseLinuxCgroupOomKills, - parseLinuxCgroupV2Path, - readLinuxCgroupMemoryState, -} from "./LinuxCgroupMemory.js"; +import { readLinuxCgroupMemoryState } from "./LinuxCgroupMemory.js"; const events = (oomKill: number) => `low 0\nhigh 0\nmax 772\noom 3\noom_kill ${String(oomKill)}\noom_group_kill 0\n`; -it("reads the cgroup v2 row and ignores v1 controller rows", () => { - expect( - parseLinuxCgroupV2Path( - "12:memory:/docker/abc\n0::/system.slice/rea.service\n", - ), - ).toBe("/system.slice/rea.service"); - expect(parseLinuxCgroupV2Path("0::/\n")).toBe("/"); -}); - -it.each([ - "12:memory:/docker/abc\n", - "0::/a\n0::/b\n", - "0::relative\n", - "0::/a/../b\n", -])("does not infer a cgroup v2 path from %j", (value) => { - expect(parseLinuxCgroupV2Path(value)).toBeUndefined(); -}); - -it("reads oom_kill without confusing it with oom or oom_group_kill", () => { - expect(parseLinuxCgroupOomKills(events(1))).toBe(1); - expect(parseLinuxCgroupOomKills("oom 3\noom_group_kill 2\n")).toBeUndefined(); - expect(parseLinuxCgroupOomKills("oom_kill 1\noom_kill 2\n")).toBeUndefined(); - expect( - parseLinuxCgroupOomKills("oom_kill 9007199254740992\n"), - ).toBeUndefined(); -}); - -it("reads a byte limit and the unlimited token", () => { - expect(parseLinuxCgroupMemoryMax("3221225472\n")).toBe(3_221_225_472); - expect(parseLinuxCgroupMemoryMax("max\n")).toBe("max"); - expect(parseLinuxCgroupMemoryMax("3g\n")).toBeUndefined(); -}); - const mountInfoRow = (root: string, mountPoint: string, type = "cgroup2") => `35 24 0:30 ${root} ${mountPoint} rw,nosuid,nodev,noexec,relatime shared:9 - ${type} cgroup2 rw,nsdelegate`; -it("lists cgroup2 mounts and decodes escaped paths", () => { - const mounts = parseLinuxCgroup2Mounts( - [ - "22 1 259:1 / / rw,relatime shared:1 - ext4 /dev/root rw", - mountInfoRow("/jobs", "/sys/fs/cgroup"), - mountInfoRow("/", "/mnt/cgroup\\040root"), - "40 24 0:31 / /sys/fs/cgroup/memory rw - cgroup cgroup rw,memory", - ].join("\n"), - ); - expect(mounts).toEqual([ - { root: "/jobs", mountPoint: "/sys/fs/cgroup" }, - { root: "/", mountPoint: "/mnt/cgroup root" }, - ]); -}); - -it("joins a cgroup path from the root of the mount that exposes it", () => { - const full = { root: "/", mountPoint: "/mnt/full" }; - const subtree = { root: "/jobs", mountPoint: "/sys/fs/cgroup" }; - expect(linuxCgroupDirectory([subtree], "/jobs/rea")).toBe( - "/sys/fs/cgroup/rea", - ); - expect(linuxCgroupDirectory([subtree], "/jobs")).toBe("/sys/fs/cgroup"); - expect(linuxCgroupDirectory([full, subtree], "/jobs/rea")).toBe( - "/sys/fs/cgroup/rea", - ); - expect(linuxCgroupDirectory([full], "/jobs/rea")).toBe("/mnt/full/jobs/rea"); - expect(linuxCgroupDirectory([subtree], "/jobsite/rea")).toBeUndefined(); - expect(linuxCgroupDirectory([], "/rea")).toBeUndefined(); -}); - /** A cgroup2 mount whose root is `/jobs`, holding this process's group `/jobs/rea`. */ const cgroupTree = async ( files: Readonly>, membership = "0::/jobs/rea\n", + mountRoot = "/jobs", ) => { const root = await createTestTempDirectory("rea-cgroup-"); const mountPoint = join(root, "cgroup"); @@ -94,10 +25,117 @@ const cgroupTree = async ( const procCgroupPath = join(root, "proc-self-cgroup"); await writeFile(procCgroupPath, membership); const procMountInfoPath = join(root, "proc-self-mountinfo"); - await writeFile(procMountInfoPath, `${mountInfoRow("/jobs", mountPoint)}\n`); + await writeFile( + procMountInfoPath, + `${mountInfoRow(mountRoot, mountPoint)}\n`, + ); return { platform: "linux" as const, procCgroupPath, procMountInfoPath }; }; +it.each([ + { membership: "12:memory:/jobs/rea\n", mountRoot: "/jobs" }, + { membership: "0::/jobs/rea\n0::/jobs/rea\n", mountRoot: "/jobs" }, + { membership: "0::jobs/rea\n", mountRoot: "jobs" }, + { membership: "0::/jobs/rea/../rea\n", mountRoot: "/jobs" }, +])( + "rejects invalid cgroup membership $membership", + async ({ membership, mountRoot }) => { + const options = await cgroupTree( + { "memory.events": events(1) }, + membership, + mountRoot, + ); + await expect(readLinuxCgroupMemoryState(options)).resolves.toBeUndefined(); + }, +); + +it("selects the v2 membership among v1 rows", async () => { + const options = await cgroupTree( + { "memory.events": events(1) }, + "12:memory:/docker/abc\n0::/jobs/rea\n", + ); + await expect(readLinuxCgroupMemoryState(options)).resolves.toMatchObject({ + path: "/jobs/rea", + oomKills: 1, + }); +}); + +it.each([ + "oom 3\noom_group_kill 2\n", + "oom_kill 1\noom_kill 2\n", + "oom_kill 9007199254740992\n", +])("rejects a missing, duplicated, or unsafe kill count: %j", async (value) => { + const options = await cgroupTree({ "memory.events": value }); + await expect(readLinuxCgroupMemoryState(options)).resolves.toBeUndefined(); +}); + +it("retains an unknown memory limit when the token is malformed", async () => { + const options = await cgroupTree({ + "memory.events": events(0), + "memory.max": "3g\n", + }); + await expect(readLinuxCgroupMemoryState(options)).resolves.toMatchObject({ + memoryMax: null, + }); +}); + +it("selects the closest covering cgroup2 mount and decodes escaped mount paths", async () => { + const root = await createTestTempDirectory("rea-cgroup-mounts-"); + const full = join(root, "full"); + const subtree = join(root, "cgroup root"); + await mkdir(join(full, "jobs", "rea"), { recursive: true }); + await mkdir(join(subtree, "rea"), { recursive: true }); + await writeFile(join(full, "jobs", "rea", "memory.events"), events(9)); + await writeFile(join(subtree, "rea", "memory.events"), events(1)); + const procCgroupPath = join(root, "proc-self-cgroup"); + const procMountInfoPath = join(root, "proc-self-mountinfo"); + const options = { + platform: "linux" as const, + procCgroupPath, + procMountInfoPath, + }; + await writeFile(procCgroupPath, "0::/jobs/rea\n"); + await writeFile( + procMountInfoPath, + [ + mountInfoRow("/", full), + mountInfoRow("/jobs", subtree.replaceAll(" ", "\\040")), + mountInfoRow("/jobs/rea", join(root, "ignored"), "cgroup"), + ].join("\n"), + ); + await expect(readLinuxCgroupMemoryState(options)).resolves.toMatchObject({ + path: "/jobs/rea", + oomKills: 1, + }); + + await writeFile(join(subtree, "memory.events"), events(2)); + await writeFile(procCgroupPath, "0::/jobs\n"); + await expect(readLinuxCgroupMemoryState(options)).resolves.toMatchObject({ + path: "/jobs", + oomKills: 2, + }); + + await writeFile(procMountInfoPath, mountInfoRow("/", full)); + await writeFile(procCgroupPath, "0::/jobs/rea\n"); + await expect(readLinuxCgroupMemoryState(options)).resolves.toMatchObject({ + path: "/jobs/rea", + oomKills: 9, + }); + + await writeFile(join(full, "memory.events"), events(3)); + await writeFile(procCgroupPath, "0::/\n"); + await expect(readLinuxCgroupMemoryState(options)).resolves.toMatchObject({ + path: "/", + oomKills: 3, + }); + + await writeFile(procMountInfoPath, mountInfoRow("/jobs", subtree)); + await writeFile(procCgroupPath, "0::/jobsite/rea\n"); + await expect(readLinuxCgroupMemoryState(options)).resolves.toBeUndefined(); + await writeFile(procMountInfoPath, ""); + await expect(readLinuxCgroupMemoryState(options)).resolves.toBeUndefined(); +}); + it("reads the process's own group below a subtree cgroup2 mount", async () => { const options = await cgroupTree({ "memory.events": events(1), diff --git a/src/process/LinuxCgroupMemory.ts b/src/process/LinuxCgroupMemory.ts index 8ca7174589..e8a780c70a 100644 --- a/src/process/LinuxCgroupMemory.ts +++ b/src/process/LinuxCgroupMemory.ts @@ -20,7 +20,7 @@ export interface LinuxCgroupMemoryState { } /** One cgroup2 mount from `/proc/self/mountinfo`. */ -export interface LinuxCgroup2Mount { +interface LinuxCgroup2Mount { /** Hierarchy path that the mount exposes at its mount point. */ readonly root: string; readonly mountPoint: string; @@ -31,7 +31,7 @@ const KERNEL_FILE_BYTES = 16 * 1024; const MOUNT_INFO_BYTES = 4 * 1024 * 1024; /** Read the single cgroup v2 (`0::`) row of one `/proc//cgroup` record. */ -export const parseLinuxCgroupV2Path = (value: string): string | undefined => { +const parseLinuxCgroupV2Path = (value: string): string | undefined => { let path: string | undefined; for (const line of value.split("\n")) { if (!line.startsWith("0::")) continue; @@ -43,7 +43,7 @@ export const parseLinuxCgroupV2Path = (value: string): string | undefined => { }; /** Read the single `oom_kill` counter of one cgroup v2 `memory.events` file. */ -export const parseLinuxCgroupOomKills = (value: string): number | undefined => { +const parseLinuxCgroupOomKills = (value: string): number | undefined => { let count: number | undefined; for (const line of value.split("\n")) { const match = /^oom_kill ([0-9]+)$/u.exec(line); @@ -55,7 +55,7 @@ export const parseLinuxCgroupOomKills = (value: string): number | undefined => { }; /** Read one cgroup v2 `memory.max` file; `max` means no limit. */ -export const parseLinuxCgroupMemoryMax = ( +const parseLinuxCgroupMemoryMax = ( value: string, ): number | "max" | undefined => { const token = value.trim(); @@ -72,9 +72,7 @@ const mountInfoField = (value: string): string => ); /** List the cgroup2 mounts of one `/proc//mountinfo` record. */ -export const parseLinuxCgroup2Mounts = ( - value: string, -): readonly LinuxCgroup2Mount[] => +const parseLinuxCgroup2Mounts = (value: string): readonly LinuxCgroup2Mount[] => value.split("\n").flatMap((line) => { const fields = line.split(" "); const separator = fields.indexOf("-"); @@ -96,7 +94,7 @@ export const parseLinuxCgroup2Mounts = ( * Locate a cgroup path below the mount whose root covers it most closely; a * mount that exposes only part of the hierarchy is joined from its own root. */ -export const linuxCgroupDirectory = ( +const linuxCgroupDirectory = ( mounts: readonly LinuxCgroup2Mount[], path: string, ): string | undefined => { diff --git a/src/process/ProcessOwnership.ts b/src/process/ProcessOwnership.ts index abd484565f..e11b32150e 100644 --- a/src/process/ProcessOwnership.ts +++ b/src/process/ProcessOwnership.ts @@ -1434,8 +1434,8 @@ const processOwnershipFailures = async ( members: readonly ProcessTableEntry[], ownership: OwnedProcessGroup, host: ProcessOwnershipHost, - expectedIdentities: ReadonlyMap = new Map(), - exitSettlement: ProcessExitSettlementBudget = { deadline: undefined }, + expectedIdentities: ReadonlyMap, + exitSettlement: ProcessExitSettlementBudget, ): Promise<{ readonly failures: readonly ProcessOwnershipValidationFailure[]; readonly validatedMembers: readonly ProcessTableEntry[]; @@ -1666,9 +1666,9 @@ const scanTokenOwnedProcesses = async ( processes: readonly ProcessTableEntry[], runId: string, host: ProcessOwnershipHost, - captureBaseline?: ProcessOwnershipBaseline, - relation?: OwnershipSweepRelation, - exitSettlement: ProcessExitSettlementBudget = { deadline: undefined }, + captureBaseline: ProcessOwnershipBaseline | undefined, + relation: OwnershipSweepRelation | undefined, + exitSettlement: ProcessExitSettlementBudget, ): Promise => { if (host.processIdentities !== undefined) return scanWithOwnershipEvidence( diff --git a/src/process/capture/ProcessCaptureLifecycle.ts b/src/process/capture/ProcessCaptureLifecycle.ts index c3e9c6932e..ea1029b8ae 100644 --- a/src/process/capture/ProcessCaptureLifecycle.ts +++ b/src/process/capture/ProcessCaptureLifecycle.ts @@ -1371,12 +1371,6 @@ class ProcessCaptureResourceOwner { const cleanupFailureMessage = (cause: unknown, fallback: string): string => cause instanceof Error ? cause.message || cause.name || fallback : fallback; -/** Release one capture's resources, retaining the exact owner for scoped retry. */ -export const releaseProcessResources = async ( - options: ProcessCaptureResourceCleanupOptions, -): Promise => - new ProcessCaptureResourceOwner(options).release(); - /** Run-local owner collection for resources whose first cleanup did not finish. */ export class ProcessCaptureResourceScope { readonly #pending = new Set(); diff --git a/src/process/capture/ProcessCaptureObservation.test.ts b/src/process/capture/ProcessCaptureObservation.test.ts index 0c3181d5b9..43aa49000a 100644 --- a/src/process/capture/ProcessCaptureObservation.test.ts +++ b/src/process/capture/ProcessCaptureObservation.test.ts @@ -5,7 +5,6 @@ import { snapshotRoots, } from "./FilesystemSnapshot.js"; import { buildCaptureResult } from "./ProcessCaptureLifecycle.js"; -import { isInitializedPtyRoot } from "./ProcessSampling.js"; import { readLinuxChildren } from "./LinuxProcessSampling.js"; import { TerminalRenderer } from "./TerminalRenderer.js"; import { processScenarioSchema } from "../../domain/process/processScenario.js"; @@ -231,70 +230,6 @@ it("collects and deduplicates children from every Linux thread", async () => { }); }); -it("admits PTY samples only after stable session and token setup", () => { - const initialized = { - pid: 100, - parent_pid: 10, - process_group_id: 100, - session_id: 100, - startTime: "200", - }; - expect( - isInitializedPtyRoot({ - rootPid: 100, - expectedRunId: "run-token", - before: { ...initialized, process_group_id: 10, session_id: 10 }, - observedRunId: undefined, - after: initialized, - }), - ).toBe(false); - expect( - isInitializedPtyRoot({ - rootPid: 100, - expectedRunId: "run-token", - before: initialized, - observedRunId: "run-token", - after: { ...initialized, startTime: "201" }, - }), - ).toBe(false); - expect( - isInitializedPtyRoot({ - rootPid: 100, - expectedRunId: "run-token", - before: initialized, - observedRunId: "other-run", - after: initialized, - }), - ).toBe(false); - expect( - isInitializedPtyRoot({ - rootPid: 100, - expectedRunId: "run-token", - before: initialized, - observedRunId: "run-token", - after: { ...initialized, parent_pid: 11 }, - }), - ).toBe(false); - expect( - isInitializedPtyRoot({ - rootPid: 100, - expectedRunId: "run-token", - before: initialized, - observedRunId: "run-token", - after: initialized, - }), - ).toBe(true); - expect( - isInitializedPtyRoot({ - rootPid: 100, - expectedRunId: "run-token", - before: { ...initialized, session_id: null }, - observedRunId: "run-token", - after: { ...initialized, session_id: null }, - }), - ).toBe(true); -}); - it("cancels filesystem snapshots before traversing declared roots", async () => { const controller = new AbortController(); controller.abort(); diff --git a/src/process/capture/ProcessCaptureProgress.ts b/src/process/capture/ProcessCaptureProgress.ts index 5cbe34cdc2..15efe19424 100644 --- a/src/process/capture/ProcessCaptureProgress.ts +++ b/src/process/capture/ProcessCaptureProgress.ts @@ -64,9 +64,8 @@ const formatProgressMessage = ( /** Serialize observed capture status through one rate-limited reporter. */ export const createProcessCaptureProgressTracker = ( progress: ProcessCaptureProgress | undefined, - now: () => number = monotonicProcessCaptureClock, ) => { - const started = now(); + const started = monotonicProcessCaptureClock(); let completed = 0; let live = true; let next: Parameters[0] | undefined; @@ -106,7 +105,7 @@ export const createProcessCaptureProgressTracker = ( completed, total: null as null, message: formatProgressMessage( - Math.max(0, now() - started), + Math.max(0, monotonicProcessCaptureClock() - started), update.counts, update.disposition, update.cleanup, diff --git a/src/process/capture/ProcessHarness.test.ts b/src/process/capture/ProcessHarness.test.ts index a85f36bafe..3743042559 100644 --- a/src/process/capture/ProcessHarness.test.ts +++ b/src/process/capture/ProcessHarness.test.ts @@ -9,7 +9,6 @@ import { } from "./ProcessHarness.js"; import { settleProcessCaptureJournal, - releaseProcessResources, ProcessCaptureResourceScope, createProcessCaptureObservationBuffer, prepareProcessCapture, @@ -345,7 +344,7 @@ it("keeps empty cleanup exception messages actionable in the report", async () = throw new Error(""); }, }; - const outcome = await releaseProcessResources({ + const outcome = await new ProcessCaptureResourceScope().release({ timers: new Set(), terminal: undefined, renderer: undefined, @@ -451,7 +450,7 @@ it("retains unrelated withheld processes through cleanup, capture, and JSON", as }), removeTemporaryRoot: async () => undefined, }; - const outcome = await releaseProcessResources({ + const outcome = await new ProcessCaptureResourceScope().release({ timers: new Set(), terminal: { pid: 321 }, renderer: undefined, @@ -506,7 +505,7 @@ it("passes sampled detached groups into cleanup even when their token is unknown removeTemporaryRoot: async () => undefined, }; - const outcome = await releaseProcessResources({ + const outcome = await new ProcessCaptureResourceScope().release({ timers: new Set(), terminal: { pid: 321 }, renderer: undefined, diff --git a/src/process/capture/ProcessHarness.ts b/src/process/capture/ProcessHarness.ts index c85b782437..aa765e1949 100644 --- a/src/process/capture/ProcessHarness.ts +++ b/src/process/capture/ProcessHarness.ts @@ -788,10 +788,10 @@ const captureProgressDisposition = ( const runProcessScenario = async ( resourceScope: ProcessCaptureResourceScope, scenario: ProcessScenario, - signal?: AbortSignal, - hostEnvironment: Readonly> = process.env, - hostPlatform: NodeJS.Platform = process.platform, - captureSnapshot: typeof snapshotRoots = snapshotRoots, + signal: AbortSignal | undefined, + hostEnvironment: Readonly>, + hostPlatform: NodeJS.Platform, + captureSnapshot: typeof snapshotRoots, cleanupHost?: ProcessCaptureCleanupHost, progress?: ProcessCaptureProgress, finalizationHost?: FinalizationHost, diff --git a/src/process/capture/ProcessSampling.procReadCoverage.test.ts b/src/process/capture/ProcessSampling.procReadCoverage.test.ts index 8df6b8abdd..5e52f6fa7e 100644 --- a/src/process/capture/ProcessSampling.procReadCoverage.test.ts +++ b/src/process/capture/ProcessSampling.procReadCoverage.test.ts @@ -257,12 +257,12 @@ const setUpFixture = (): void => { fixture.children.set("200:200", "300"); }; -const start = () => { +const start = (runId = fixture.runId) => { const samples: ProcessSample[] = []; const resourceScope = new ProcessCaptureResourceScope(); const stop = startProcessSampler({ rootPid: 100, - runId: fixture.runId, + runId, started: 0, limit: 20, samples, @@ -587,6 +587,8 @@ it.each([ { description: "PID", mutation: { read: 2, pid: 101 } }, { description: "start time", mutation: { read: 2, start: 9999 } }, { description: "parent", mutation: { read: 2, parent: 2 } }, + { description: "process group", mutation: { read: 2, processGroupId: 10 } }, + { description: "session", mutation: { read: 2, sessionId: 10 } }, ] as const)( "does not attribute a root whose initialized identity changes by $description", async ({ mutation }) => { @@ -600,6 +602,52 @@ it.each([ }, ); +it.each([{ processGroupId: 10 }, { sessionId: 10 }])( + "does not inspect the token before PTY initialization: %o", + async (mutation) => { + setUpFixture(); + fixture.statMutations.set(100, { + read: 1, + ...mutation, + }); + const { samples, stop } = start(); + await waitFor(() => (fixture.statReads.get(100) ?? 0) > 0); + await flushImmediateProcPass(); + await stop(); + expect(fixture.runIdReads).toBe(0); + expect(samples).toEqual([]); + }, +); + +it("does not attribute a root bearing a different run token", async () => { + setUpFixture(); + const { samples, stop } = start("another-run-token"); + await waitFor(() => fixture.runIdReads > 0); + await flushImmediateProcPass(); + await stop(); + expect(samples).toEqual([]); +}); + +it("admits a stable Darwin PTY root when ps does not report a session ID", async () => { + setUpFixture(); + Object.defineProperty(process, "platform", { + configurable: true, + value: "darwin", + }); + fixture.psOutput = "100 1 100 0 fixture-command\n"; + try { + const { samples, stop } = start(); + await waitFor(() => samples.some(({ pid }) => pid === 100)); + await stop(); + expect(samples[0]).toMatchObject({ pid: 100, session_id: null }); + } finally { + Object.defineProperty(process, "platform", { + configurable: true, + value: "linux", + }); + } +}); + it.each([0, -1])( "keeps completed samples without false failures when root relations disappear (%s)", async (relation) => { diff --git a/src/process/capture/ProcessSampling.ts b/src/process/capture/ProcessSampling.ts index 49575c87c5..a4f2e01b51 100644 --- a/src/process/capture/ProcessSampling.ts +++ b/src/process/capture/ProcessSampling.ts @@ -47,7 +47,7 @@ type ProcessIdentitySnapshot = Pick< >; /** Prove that forkpty completed session setup and installed the run token. */ -export const isInitializedPtyRoot = (options: { +const isInitializedPtyRoot = (options: { readonly rootPid: number; readonly expectedRunId: string; readonly before: ProcessIdentitySnapshot; diff --git a/src/process/capture/ProcessTerminalObservation.test.ts b/src/process/capture/ProcessTerminalObservation.test.ts index eba902af7e..9c8cb474e9 100644 --- a/src/process/capture/ProcessTerminalObservation.test.ts +++ b/src/process/capture/ProcessTerminalObservation.test.ts @@ -20,7 +20,7 @@ import { processScenarioSchema } from "../../domain/process/processScenario.js"; import { createProcessCaptureObservationBuffer, resolveProcessResult, - releaseProcessResources, + ProcessCaptureResourceScope, captureTerminalFrames, } from "./ProcessCaptureLifecycle.js"; import { createProcessTerminalFinalizer } from "./ProcessTerminalObservation.js"; @@ -222,7 +222,7 @@ it.each(["cancelled", "failed"] as const)( }, ); emitOutput("root 100\r\n"); - const cleanup = await releaseProcessResources({ + const cleanup = await new ProcessCaptureResourceScope().release({ timers: new Set([timer]), terminal: { pid: 100 }, renderer: owner, diff --git a/src/server/environment/environmentCapacity.test.ts b/src/server/environment/environmentCapacity.test.ts index d6e0934e9c..d7dfe71871 100644 --- a/src/server/environment/environmentCapacity.test.ts +++ b/src/server/environment/environmentCapacity.test.ts @@ -48,10 +48,13 @@ it("charges environment identity, aggregate tools and escaped text before produc query_parameter_names: [], })); expect( - environmentToolDefinitions({ - ...source, - manifest: { ...source.manifest, tools }, - }).ok, + environmentToolDefinitions( + { + ...source, + manifest: { ...source.manifest, tools }, + }, + source.manifest.name, + ).ok, ).toBe(false); expect( environmentToolDefinitions( @@ -59,7 +62,7 @@ it("charges environment identity, aggregate tools and escaped text before produc "x".repeat(ENVIRONMENT_CATALOG_LIMIT_BYTES), ).ok, ).toBe(false); - const normal = environmentToolDefinitions(source); + const normal = environmentToolDefinitions(source, source.manifest.name); if (!normal.ok) throw normal.error; expect( Buffer.byteLength( diff --git a/src/server/environment/serveEnvironment.test.ts b/src/server/environment/serveEnvironment.test.ts index 0ecb0f93be..9bd4f5e16c 100644 --- a/src/server/environment/serveEnvironment.test.ts +++ b/src/server/environment/serveEnvironment.test.ts @@ -38,7 +38,7 @@ const serveEnvironment = async ( if (!result.ok) throw result.error; return result.value; }; -const definitions = environmentToolDefinitions(source); +const definitions = environmentToolDefinitions(source, source.manifest.name); /** * The agentenv echo example's card, key for key. Only `name` and the tool diff --git a/src/server/environment/stateIntake.test.ts b/src/server/environment/stateIntake.test.ts index 9bab225321..60119728e7 100644 --- a/src/server/environment/stateIntake.test.ts +++ b/src/server/environment/stateIntake.test.ts @@ -3,12 +3,7 @@ import { expect, it, vi } from "vitest"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import { STATE_INTAKE_LIMIT_BYTES } from "../../application/environment/environmentStateDocument.js"; -import { - DEFAULT_STATE_INTAKE, - fetchStateDocument, - partDocument, - STATE_FETCH_TIMEOUT_MS, -} from "./stateIntake.js"; +import { DEFAULT_STATE_INTAKE, partDocument } from "./stateIntake.js"; const inlineDocument = async (bytes: string) => { const files = new ArtifactResourceScope(); @@ -67,19 +62,27 @@ it("downloads with redirects refused and the 30 s timeout by default", async () const fetched = vi .spyOn(globalThis, "fetch") .mockResolvedValue(new Response("{}")); + const files = new ArtifactResourceScope(); + const responses = new ResponseBodyCleanup("state response body"); try { - const url = new URL("https://state.example.test/state.json"); - const signal = AbortSignal.timeout(STATE_FETCH_TIMEOUT_MS); - await fetchStateDocument(url, signal); - expect(fetched).toHaveBeenCalledExactlyOnceWith(url, { - signal, + const uri = "https://state.example.test/state.json"; + await expect( + partDocument( + { kind: "file", file: { uri } }, + DEFAULT_STATE_INTAKE, + new AbortController().signal, + files, + responses, + ), + ).resolves.toEqual({ ok: true, value: {} }); + expect(fetched).toHaveBeenCalledExactlyOnceWith(new URL(uri), { + signal: expect.any(AbortSignal), redirect: "error", }); - expect(DEFAULT_STATE_INTAKE).toEqual({ - fetchState: fetchStateDocument, - timeoutMs: 30_000, - }); + expect(DEFAULT_STATE_INTAKE.timeoutMs).toBe(30_000); } finally { + await responses.close(); + await files.close(); fetched.mockRestore(); } }); diff --git a/src/server/environment/stateIntake.ts b/src/server/environment/stateIntake.ts index d4de76c47a..b8b31823da 100644 --- a/src/server/environment/stateIntake.ts +++ b/src/server/environment/stateIntake.ts @@ -54,13 +54,13 @@ export const CANCELLED: RpcError = { }; /** An https state download must finish within this time. */ -export const STATE_FETCH_TIMEOUT_MS = 30_000; +const STATE_FETCH_TIMEOUT_MS = 30_000; /** Fetches one https state document; injectable so tests make no external requests. */ export type StateFetcher = (url: URL, signal: AbortSignal) => Promise; /** Redirects are refused so a download never leaves the https origin the caller named. */ -export const fetchStateDocument: StateFetcher = (url, signal) => +const fetchStateDocument: StateFetcher = (url, signal) => fetch(url, { signal, redirect: "error" }); /** How `data/add` reaches documents a uri names; injectable so tests make no external requests and need not wait. */ diff --git a/src/server/registerProcessComparisonTool.ts b/src/server/registerProcessComparisonTool.ts index 68f5bf35ef..2310cedb36 100644 --- a/src/server/registerProcessComparisonTool.ts +++ b/src/server/registerProcessComparisonTool.ts @@ -17,7 +17,6 @@ export const registerProcessComparisonTool = ( server: EvidenceMcpServer, session: BinarySessionPort, contract: ReturnType>, - now: () => number = Date.now, ): void => { const admission = withAdmittedAnalysis({ kind: "session", session }); server.registerTool( @@ -37,7 +36,7 @@ export const registerProcessComparisonTool = ( const result = compareProcessEvidence( input, session.evidenceForAnalysis.bind(session), - { signal: context.mcpReq.signal, now }, + { signal: context.mcpReq.signal }, ); if (!result.ok) throw result.error; return result.value; diff --git a/tests/acceptance/peCertificatesCli.test.ts b/tests/acceptance/peCertificatesCli.test.ts index 0846b214fb..18531c68e5 100644 --- a/tests/acceptance/peCertificatesCli.test.ts +++ b/tests/acceptance/peCertificatesCli.test.ts @@ -2,7 +2,6 @@ import { createHash } from "node:crypto"; import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; -import { UNSIGNED_ATTRIBUTES_LIMITATION } from "../../src/native/pe/PeCertificateParser.js"; import { toolContract } from "../../src/contracts/toolContracts.js"; import { peCertificateFixture, @@ -156,7 +155,9 @@ cliTest("reports unsigned attributes through CLI and MCP", async ({ cli }) => { expect( report.entries[0]?.pkcs7?.signers[0]?.unsigned_attribute_oids, ).toEqual(["1.3.6.1.4.1.311.3.3.1"]); - expect(report.limitations).toContain(UNSIGNED_ATTRIBUTES_LIMITATION); + expect(report.limitations).toContain( + "Signer unsigned attribute contents are not decoded; nested signatures and timestamp token certificates and times may be omitted.", + ); } }); diff --git a/tests/boundary/android/androidJniLibrary.test.ts b/tests/boundary/android/androidJniLibrary.test.ts index cdd7a16c43..a1431ffd6b 100644 --- a/tests/boundary/android/androidJniLibrary.test.ts +++ b/tests/boundary/android/androidJniLibrary.test.ts @@ -1,8 +1,5 @@ import { expect, it, vi } from "vitest"; -import { - decodeJniModifiedUtf8, - inspectAndroidJniLibrary, -} from "../../../src/native/android/AndroidJniLibrary.js"; +import { inspectAndroidJniLibrary } from "../../../src/native/android/AndroidJniLibrary.js"; import { androidJniLibraryFactsSchema } from "../../../src/domain/android/androidJniLibraryFacts.js"; import { projectAndroidJniBindings } from "../../../src/domain/android/androidJniBindings.js"; import { jniElfFixture } from "../../fixtures/android/jniElf.js"; @@ -260,19 +257,42 @@ it("preserves later candidates after malformed Modified UTF-8 and a malformed JN }); it.each([ - [[], ""], - [[0xc0, 0x80], "\0"], - [[0xed, 0xa0, 0x81, 0xed, 0xb0, 0x80], "𐐀"], - [[0xed, 0xa0, 0x80], "\ud800"], - [[0], undefined], + [[], "X"], + [[0xc0, 0x80], undefined], + [[0xed, 0xa0, 0x81, 0xed, 0xb0, 0x80], "X𐐀"], + [[0xed, 0xa0, 0x80], "X\ud800"], [[0xc0, 0x81], undefined], [[0xc1, 0xbf], undefined], [[0xe0, 0x80, 0x80], undefined], [[0xf0, 0x90, 0x90, 0x80], undefined], [[0xe7, 0x8c], undefined], [[0x80], undefined], -] as const)("decodes exact Modified UTF-8 bytes %o", (bytes, value) => { - expect(decodeJniModifiedUtf8(Uint8Array.from(bytes))).toBe(value); +] as const)( + "admits JNI names with Modified UTF-8 bytes %o", + async (encoded, name) => { + const { bytes } = jniElfFixture(); + bytes.fill(0, 0x400, 0x420); + bytes.set([0x58, ...encoded, 0], 0x400); + const result = await inspectAndroidJniLibrary(bytes); + if (!result.ok) throw new Error(result.error.message); + expect( + result.value.tables + .flatMap((table) => table.entries) + .map((entry) => entry.name), + ).toEqual([...(name === undefined ? [] : [name]), "overload", "𐐀"]); + }, +); + +it("rejects an empty JNI method name while preserving later candidates", async () => { + const { bytes } = jniElfFixture(); + bytes[0x400] = 0; + const result = await inspectAndroidJniLibrary(bytes); + if (!result.ok) throw new Error(result.error.message); + expect( + result.value.tables + .flatMap((table) => table.entries) + .map((entry) => entry.name), + ).toEqual(["overload", "𐐀"]); }); it("returns explicit cancellation before decoding and never runs a target", async () => { diff --git a/tests/boundary/android/androidTargetSnapshot.test.ts b/tests/boundary/android/androidTargetSnapshot.test.ts index 45d70569fb..1280fbfa31 100644 --- a/tests/boundary/android/androidTargetSnapshot.test.ts +++ b/tests/boundary/android/androidTargetSnapshot.test.ts @@ -1,11 +1,158 @@ +import { createHash } from "node:crypto"; import { stat, truncate, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import { expect, it } from "vitest"; +import { expect, it, vi } from "vitest"; import { snapshotAndroidTarget } from "../../../src/android/AndroidTargetSnapshot.js"; +import { OwnedFileHandle } from "../../../src/filesystem/OwnedFileHandle.js"; +import { + RegularFileCleanupFailure, + regularFileCancellation, +} from "../../../src/filesystem/OwnedRegularFile.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +it.each(["close-failure", "final-cancellation", "retention-throws"] as const)( + "retains the exact failed snapshot owner for retry: %s", + async (scenario) => { + const root = await createTestTempDirectory("rea-android-snapshot-close-"); + const source = join(root, "source.apk"); + const bytes = Buffer.from("selected APK bytes"); + await writeFile(source, bytes); + const controller = new AbortController(); + const reason = new Error("cancelled during final close"); + const closeFailure = new Error("snapshot descriptor close denied"); + const retentionFailure = new Error("retention callback failed"); + const retained = new Map(); + const originalClose = OwnedFileHandle.prototype.close; + let failedOwner: OwnedFileHandle | undefined; + const close = vi + .spyOn(OwnedFileHandle.prototype, "close") + .mockImplementation(function (this: OwnedFileHandle) { + if (failedOwner === undefined) { + failedOwner = this; + if (scenario === "final-cancellation") controller.abort(reason); + } + if (this === failedOwner) return Promise.reject(closeFailure); + return originalClose.call(this); + }); + try { + const failure = await snapshotAndroidTarget( + source, + createHash("sha256").update(bytes).digest("hex"), + root, + "inspect_android_package", + controller.signal, + (path, owner) => { + retained.set(owner, path); + if (scenario === "retention-throws") throw retentionFailure; + }, + ).catch((cause: unknown) => cause); + expect(failure).toBeInstanceOf(RegularFileCleanupFailure); + if (!(failure instanceof RegularFileCleanupFailure)) + throw new Error("Expected a failed snapshot descriptor close"); + expect(failure.cleanupCause).toBe(closeFailure); + expect(failure.owner).toBe(failedOwner); + expect(failure.retention).toEqual( + scenario === "retention-throws" + ? { kind: "callback_threw", cause: retentionFailure } + : { kind: "callback_returned" }, + ); + expect(retained.get(failure.owner)).toBe(failure.path); + if (scenario === "final-cancellation") + expect(regularFileCancellation(failure)?.reason).toBe(reason); + else expect(regularFileCancellation(failure)).toBeUndefined(); + expect(failure.owner.handle.fd).toBeGreaterThanOrEqual(0); + close.mockRestore(); + await failure.owner.close(); + expect(failure.owner.handle.fd).toBe(-1); + } finally { + close.mockRestore(); + await Promise.all([...retained.keys()].map((owner) => owner.close())); + } + }, +); + +it("retains both snapshot owners when the copy fails and both closes fail", async () => { + const root = await createTestTempDirectory( + "rea-android-snapshot-mixed-close-", + ); + const source = join(root, "source.apk"); + const snapshot = join(root, "target.apk"); + await writeFile(source, "selected APK bytes"); + const retained = new Map(); + const attempted = new Set(); + const closeFailure = new Error("snapshot descriptor close denied"); + const close = vi + .spyOn(OwnedFileHandle.prototype, "close") + .mockImplementation(function (this: OwnedFileHandle) { + attempted.add(this); + return Promise.reject(closeFailure); + }); + try { + const failure = await snapshotAndroidTarget( + source, + "0".repeat(64), + root, + "inspect_android_package", + undefined, + (path, owner) => retained.set(owner, path), + ).catch((cause: unknown) => cause); + + expect(failure).toBeInstanceOf(RegularFileCleanupFailure); + if (!(failure instanceof RegularFileCleanupFailure)) + throw new Error("Expected source cleanup failure"); + expect(failure.path).toBe(source); + expect(failure.cleanupCause).toBe(closeFailure); + expect(failure.retention).toEqual({ kind: "callback_returned" }); + expect(failure.outcome.kind).toBe("failed"); + if (failure.outcome.kind !== "failed") + throw new Error("Expected nested failure"); + const destinationFailure = failure.outcome.cause; + expect(destinationFailure).toBeInstanceOf(RegularFileCleanupFailure); + if (!(destinationFailure instanceof RegularFileCleanupFailure)) + throw new Error("Expected destination cleanup failure"); + expect(destinationFailure.path).toBe(snapshot); + expect(destinationFailure.cleanupCause).toBe(closeFailure); + expect(destinationFailure.retention).toEqual({ kind: "callback_returned" }); + expect(destinationFailure.outcome).toMatchObject({ + kind: "failed", + cause: { + _tag: "AnalysisInputError", + issues: [ + { + path: ["path"], + reason: "invalid_value", + message: expect.stringContaining( + `APK bytes changed after admission at ${source}`, + ), + }, + ], + }, + }); + expect(retained.size).toBe(2); + expect(retained.get(failure.owner)).toBe(source); + expect(retained.get(destinationFailure.owner)).toBe(snapshot); + expect(failure.owner).not.toBe(destinationFailure.owner); + expect([...retained.keys()].every((owner) => owner.handle.fd >= 0)).toBe( + true, + ); + + close.mockRestore(); + await failure.owner.close(); + expect(failure.owner.handle.fd).toBe(-1); + expect(destinationFailure.owner.handle.fd).toBeGreaterThanOrEqual(0); + expect(retained.get(destinationFailure.owner)).toBe(snapshot); + await destinationFailure.owner.close(); + expect([...retained.keys()].every((owner) => owner.handle.fd === -1)).toBe( + true, + ); + } finally { + close.mockRestore(); + await Promise.all([...attempted].map((owner) => owner.close())); + } +}); + it.skipIf(process.platform === "win32")( "cancels an in-flight APK snapshot and removes its partial output", async () => { @@ -18,12 +165,14 @@ it.skipIf(process.platform === "win32")( await truncate(source, 64 * 1024 * 1024 * 1024); const controller = new AbortController(); + const retained = new Set(); const pending = snapshotAndroidTarget( source, "0".repeat(64), root, "inspect_android_package", controller.signal, + (_path, owner) => retained.add(owner), ); try { await expect @@ -42,6 +191,7 @@ it.skipIf(process.platform === "win32")( } finally { controller.abort(); await pending.catch(() => undefined); + await Promise.all([...retained].map((owner) => owner.close())); } }, ); diff --git a/tests/boundary/browser/webSourceMapFetcher.test.ts b/tests/boundary/browser/webSourceMapFetcher.test.ts index fe163b952d..2cf8991d7d 100644 --- a/tests/boundary/browser/webSourceMapFetcher.test.ts +++ b/tests/boundary/browser/webSourceMapFetcher.test.ts @@ -678,9 +678,12 @@ describe("web source-map fetching and validation: enforcing fetch limits and val const url = `${localOrigin}/map?n=0`; const result = await fetchWebSourceMapsOk( [{ ...request, fetchUrl: url, declaredUrl: url }], - input({ allowed_origins: [localOrigin] }), + input({ + allowed_origins: [localOrigin], + source_map_fetch_timeout_ms: 100, + }), undefined, - { fetch, timeoutMs: 100 }, + { fetch }, ); expect(calls).toBeGreaterThan(1); expect(result).toMatchObject({ @@ -733,9 +736,12 @@ describe("web source-map fetching and validation: enforcing fetch limits and val const url = `${localOrigin}/map`; const result = await fetchWebSourceMapsOk( [{ ...request, fetchUrl: url, declaredUrl: url }], - input({ allowed_origins: [localOrigin] }), + input({ + allowed_origins: [localOrigin], + source_map_limits: { fetch_bytes: 512 }, + }), undefined, - { fetch, maxResponseBytes: 512 }, + { fetch }, ); let timeout: ReturnType | undefined; const closeConfirmed = await Promise.race([ @@ -1046,20 +1052,24 @@ describe("source-map operation deadline observations", () => { request, { ...request, scriptKey: `scr_${"2".repeat(64)}` }, ]; - const result = await fetchWebSourceMapsOk(requests, input(), undefined, { - timeoutMs: 100, - fetch: (_url, options) => { - calls += 1; - return calls === 1 - ? Promise.resolve(validMapResponse()) - : new Promise((_resolve, reject) => { - const signal = options?.signal; - signal?.addEventListener("abort", () => reject(signal.reason), { - once: true, + const result = await fetchWebSourceMapsOk( + requests, + input({ source_map_fetch_timeout_ms: 100 }), + undefined, + { + fetch: (_url, options) => { + calls += 1; + return calls === 1 + ? Promise.resolve(validMapResponse()) + : new Promise((_resolve, reject) => { + const signal = options?.signal; + signal?.addEventListener("abort", () => reject(signal.reason), { + once: true, + }); }); - }); + }, }, - }); + ); expect(result.status).toBe("partial"); expect(result.items.map(({ status }) => status)).toEqual([ @@ -1070,6 +1080,38 @@ describe("source-map operation deadline observations", () => { }); }); +it("shares the caller's fetch byte budget across completed map responses", async () => { + const body = JSON.stringify({ + version: 3, + sources: [], + names: [], + mappings: "", + }); + const requests = [ + request, + { ...request, scriptKey: `scr_${"2".repeat(64)}` }, + ]; + let calls = 0; + const result = await fetchWebSourceMapsOk( + requests, + input({ source_map_limits: { fetch_bytes: Buffer.byteLength(body) } }), + undefined, + { + fetch: () => { + calls += 1; + return Promise.resolve(new Response(body)); + }, + }, + ); + + expect(calls).toBe(2); + expect(result.items.map(({ status }) => status)).toEqual([ + "included", + "fetch_failed", + ]); + expect(result.items[1]?.limitation).toContain("retained budget"); +}); + describe("indexed source-map offsets", () => { it.each([ { line: 0, column: -1 }, @@ -1712,10 +1754,12 @@ describe("source-map set output admission", () => { }); const result = await fetchWebSourceMapsOk( requests, - input({ allowed_origins: [localOrigin] }), + input({ + allowed_origins: [localOrigin], + source_map_fetch_timeout_ms: 120_000, + }), undefined, { - timeoutMs: 120_000, fetch: () => Promise.resolve(new Response(map, { status: 200 })), }, ); diff --git a/tests/boundary/browser/webSourceMapHttpStatus.test.ts b/tests/boundary/browser/webSourceMapHttpStatus.test.ts index ded438f4c5..13ec2a408e 100644 --- a/tests/boundary/browser/webSourceMapHttpStatus.test.ts +++ b/tests/boundary/browser/webSourceMapHttpStatus.test.ts @@ -147,10 +147,12 @@ it.each(["invalid JSON", "invalid UTF-8", "HTTP failure", "response limit"])( target_id: "page", allowed_origins: [origin], fetch_source_maps: true, + ...(failure === "response limit" + ? { source_map_limits: { fetch_bytes: 1 } } + : {}), }), undefined, { - ...(failure === "response limit" ? { maxResponseBytes: 1 } : {}), fetch: (url) => Promise.resolve( String(url) === declaredUrl diff --git a/tests/boundary/browser/webSourceMapResponseIdentity.test.ts b/tests/boundary/browser/webSourceMapResponseIdentity.test.ts index 9314b866a5..31348e93f6 100644 --- a/tests/boundary/browser/webSourceMapResponseIdentity.test.ts +++ b/tests/boundary/browser/webSourceMapResponseIdentity.test.ts @@ -115,10 +115,12 @@ it("retains completed body identity when map interpretation reaches its deadline return text; }); try { - const result = await fetchWebSourceMaps([request(1)], input, undefined, { - timeoutMs: 1_000, - fetch: () => Promise.resolve(response), - }); + const result = await fetchWebSourceMaps( + [request(1)], + { ...input, source_map_fetch_timeout_ms: 1_000 }, + undefined, + { fetch: () => Promise.resolve(response) }, + ); if (!result.ok) throw result.error; expect(result.value).toMatchObject({ status: "unavailable", diff --git a/tests/boundary/cli/traceNativeValuesCompletenessCli.test.ts b/tests/boundary/cli/traceNativeValuesCompletenessCli.test.ts index bf6c881504..f2cf696b80 100644 --- a/tests/boundary/cli/traceNativeValuesCompletenessCli.test.ts +++ b/tests/boundary/cli/traceNativeValuesCompletenessCli.test.ts @@ -13,8 +13,8 @@ import { } from "../../../src/application/AnalysisProvider.js"; import { runDirectAnalysis } from "../../../src/application/DirectAnalysis.js"; import type { DirectAnalysisDependencies } from "../../../src/application/DirectAnalysisDependencies.js"; -import { CLEANUP_STOPPED_VALUE_TRACE } from "../../../src/application/native/NativeValueTrace.js"; import { + EXPECTED_CLEANUP_STOPPED_TRACE, failingValueTraceReads, valueTraceDiamond, valueTraceProvider, @@ -254,7 +254,7 @@ it.each([ expect(admitted.unknowns).toContainEqual({ procedure: "0x2000", address: null, - reason: CLEANUP_STOPPED_VALUE_TRACE, + reason: EXPECTED_CLEANUP_STOPPED_TRACE, }); }, ); diff --git a/tests/boundary/filesystem/binaryTarget.test.ts b/tests/boundary/filesystem/binaryTarget.test.ts index 305a6ceee1..1797e34900 100644 --- a/tests/boundary/filesystem/binaryTarget.test.ts +++ b/tests/boundary/filesystem/binaryTarget.test.ts @@ -396,31 +396,6 @@ describe("binary target I/O: app plist permissions and decoding", () => { }); }); - it("does not label binary plist decoder I/O failures as malformed data", async () => { - const directory = await createTestTempDirectory("rea-app-decode-error-"); - const app = join(directory, "DecodeError.app"); - await mkdir(join(app, "Contents"), { recursive: true }); - await writeFile(join(app, "Contents", "Info.plist"), "bplist00"); - const decodeError = Object.assign(new Error("I/O failure"), { - code: "EIO", - }); - const result = await resolveAppBundleExecutable(app, { - readFile: async () => Buffer.from("bplist00"), - realpath, - decodeBinaryPlist: async () => { - throw decodeError; - }, - }); - expect(result).toMatchObject({ - ok: false, - error: { - path: join(app, "Contents", "Info.plist"), - reason: expect.stringContaining("could not decode app Info.plist"), - cause: decodeError, - }, - }); - }); - it.each(["EACCES", "EPERM", "ENOENT", "ENOTDIR"])( "preserves %s while resolving an app executable", async (code) => { diff --git a/tests/boundary/filesystem/bunMetadataReadBudget.test.ts b/tests/boundary/filesystem/bunMetadataReadBudget.test.ts index e79ab0342d..b75ea79327 100644 --- a/tests/boundary/filesystem/bunMetadataReadBudget.test.ts +++ b/tests/boundary/filesystem/bunMetadataReadBudget.test.ts @@ -1,11 +1,11 @@ import { open, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it } from "vitest"; -import { BUN_TRAILER } from "../../../src/artifacts/bun/BunModuleGraphLayout.js"; import { decodeBunModuleGraph } from "../../../src/artifacts/bun/BunModuleGraphMembers.js"; import { elfAppendedBunPayload, locateBunGraphFixture, + BUN_FIXTURE_TRAILER, } from "../../../src/artifacts/bun/BunStandalone.fixture.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; @@ -33,7 +33,7 @@ it("keeps scattered tiny metadata reads proportional to the graph instead of cac offsets.writeUInt32LE(tableAt, 8); offsets.writeUInt32LE(count * 52, 12); const bytes = elfAppendedBunPayload( - Buffer.concat([data, offsets, BUN_TRAILER]), + Buffer.concat([data, offsets, BUN_FIXTURE_TRAILER]), ); const path = join( await createTestTempDirectory("rea-bun-scattered-metadata-"), @@ -79,7 +79,7 @@ it("refuses cumulative uncached metadata work before the next file read", async const offsets = Buffer.alloc(32); offsets.writeBigUInt64LE(BigInt(data.length)); const bytes = elfAppendedBunPayload( - Buffer.concat([data, offsets, BUN_TRAILER]), + Buffer.concat([data, offsets, BUN_FIXTURE_TRAILER]), ); const path = join( await createTestTempDirectory("rea-bun-metadata-work-"), diff --git a/tests/boundary/filesystem/hopperFatCoordinates.test.ts b/tests/boundary/filesystem/hopperFatCoordinates.test.ts index 87ea3e1ff7..ce5c14e3a0 100644 --- a/tests/boundary/filesystem/hopperFatCoordinates.test.ts +++ b/tests/boundary/filesystem/hopperFatCoordinates.test.ts @@ -10,6 +10,11 @@ import { machoImage, } from "../../../src/artifacts/apple/MachoImage.fixture.js"; import type { BinaryTarget } from "../../../src/domain/binaryTargetTypes.js"; +import type { JsonValue } from "../../../src/domain/jsonValue.js"; +import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; +import { AnalysisCleanupLease } from "../../../src/artifacts/AnalysisCleanupLease.js"; +import { RegularFileCleanupFailure } from "../../../src/filesystem/OwnedRegularFile.js"; +import type { OwnedFileHandle } from "../../../src/filesystem/OwnedFileHandle.js"; import { mapHopperFileOffset } from "../../../src/hopper/HopperFileOffset.js"; import { resolveHopperMachOImage } from "../../../src/hopper/HopperMachOImage.js"; import { createInspectionResources } from "../../fixtures/inspectionResources.js"; @@ -40,6 +45,23 @@ const resolveImage = ( return resolveHopperMachOImage(target, { resourceScope: resources.owner }); }; +const mapOffset = (target: BinaryTarget, observation: JsonValue) => { + const scope = new ArtifactResourceScope(); + const leases = new WeakMap(); + onTestFinished(() => scope.close()); + return mapHopperFileOffset(target, observation, { + retainFailedClose: (owner) => { + const lease = AnalysisCleanupLease.forCloseOwner(owner, target.path); + leases.set(owner, lease); + scope.retain({ kind: "lease", resource: target.path, lease }); + }, + onCleanupMarker: (marker) => { + if (marker.cause instanceof RegularFileCleanupFailure) + leases.get(marker.cause.owner)?.bindCleanupError(marker); + }, + }); +}; + const loadedObservation = (header: Uint8Array) => ({ address: "0x1000", provider_file_offset: 8, @@ -83,7 +105,7 @@ it.each([ cpu_subtype: CPU.arm64.subtype, }, }); - const mapped = await mapHopperFileOffset(target, loadedObservation(arm64e)); + const mapped = await mapOffset(target, loadedObservation(arm64e)); expect(mapped).toEqual({ ok: true, value: { @@ -117,9 +139,7 @@ it.each([false, true])( ok: false, error: { _tag: "BinaryTargetError", reason }, }); - expect( - await mapHopperFileOffset(target, loadedObservation(thin)), - ).toMatchObject({ + expect(await mapOffset(target, loadedObservation(thin))).toMatchObject({ ok: false, error: { _tag: "AnalysisCapabilityUnavailableError", @@ -165,9 +185,7 @@ it.each([ ok: false, error: { _tag: "BinaryTargetError", reason: "FAT slices overlap" }, }); - expect( - await mapHopperFileOffset(target, loadedObservation(arm64e)), - ).toMatchObject({ + expect(await mapOffset(target, loadedObservation(arm64e))).toMatchObject({ ok: false, error: { _tag: "AnalysisCapabilityUnavailableError", @@ -184,9 +202,7 @@ it("requires a unique exact raw CPU identity for loaded-image attribution", asyn { cpu: CPU.arm64e, bytes: arm64e }, ]); const target = await sourceTarget(bytes, arm64e.byteLength); - expect( - await mapHopperFileOffset(target, loadedObservation(arm64e)), - ).toMatchObject({ + expect(await mapOffset(target, loadedObservation(arm64e))).toMatchObject({ ok: false, error: { _tag: "AnalysisCapabilityUnavailableError", @@ -196,7 +212,7 @@ it("requires a unique exact raw CPU identity for loaded-image attribution", asyn const withoutCapabilities = Buffer.from(arm64e); withoutCapabilities.writeUInt32LE(2, 8); expect( - await mapHopperFileOffset(target, loadedObservation(withoutCapabilities)), + await mapOffset(target, loadedObservation(withoutCapabilities)), ).toMatchObject({ ok: false, error: { @@ -218,7 +234,7 @@ it("checks all 12 loaded-header bytes after matching its CPU identity", async () loadedHeader.writeUInt32BE(CPU.arm64.type, 4); loadedHeader.writeUInt32BE(CPU.arm64.subtype, 8); expect( - await mapHopperFileOffset(target, loadedObservation(loadedHeader)), + await mapOffset(target, loadedObservation(loadedHeader)), ).toMatchObject({ ok: false, error: { diff --git a/tests/boundary/filesystem/javascript-applications/javascriptArtifactReconstruction.test.ts b/tests/boundary/filesystem/javascript-applications/javascriptArtifactReconstruction.test.ts index 21f47ab6dc..01109d8c3d 100644 --- a/tests/boundary/filesystem/javascript-applications/javascriptArtifactReconstruction.test.ts +++ b/tests/boundary/filesystem/javascript-applications/javascriptArtifactReconstruction.test.ts @@ -27,7 +27,7 @@ import { immutableArtifactGraph } from "../../../fixtures/javascriptArtifactGrap import { createJavaScriptSemanticGraphProjection } from "../../../../src/application/javascript/JavaScriptSemanticGraphBuilder.js"; import type { JavaScriptModuleSemanticIr } from "../../../../src/domain/javascript/javascriptModuleSemanticIr.js"; import type { JavaScriptStaticAnalysis } from "../../../../src/domain/javascript/javascriptStaticAnalysisTypes.js"; -import { analyzeJavaScriptStaticSource } from "../../../../src/domain/javascript/javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../../fixtures/javascriptStaticAnalysis.js"; import type { JavaScriptArtifactFile } from "../../../../src/domain/javascript/javascriptArtifactFiles.js"; import { completeJavaScriptAnalysisSteps } from "../../../../src/application/javascript/JavaScriptAnalysisControl.js"; import { buildImmutableJavaScriptArtifactGraphSteps } from "../../../../src/application/javascript/JavaScriptArtifactGraphBuilder.js"; diff --git a/tests/boundary/filesystem/javascript-applications/javascriptFileUrlPaths.test.ts b/tests/boundary/filesystem/javascript-applications/javascriptFileUrlPaths.test.ts index d0959c26a2..2c5148e00d 100644 --- a/tests/boundary/filesystem/javascript-applications/javascriptFileUrlPaths.test.ts +++ b/tests/boundary/filesystem/javascript-applications/javascriptFileUrlPaths.test.ts @@ -9,7 +9,7 @@ import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js import { javascriptApplicationAnalysisResultSchema } from "../../../../src/domain/javascript/javascriptApplicationAnalysis.js"; import { parseJavaScriptSource } from "../../../../src/domain/javascript/javascriptSourceParser.js"; import { staticPath } from "../../../../src/domain/javascript/javascriptStaticAnalysisHelpers.js"; -import { analyzeParsedJavaScriptStaticSource } from "../../../../src/domain/javascript/javascriptStaticAnalysis.js"; +import { analyzeParsedJavaScriptStaticSource } from "../../../fixtures/javascriptStaticAnalysis.js"; import { analyzeJavaScriptApplication } from "../../../support/javascriptApplicationScope.js"; const urlCases = [ diff --git a/tests/boundary/filesystem/referenceSourceCleanupOwnership.test.ts b/tests/boundary/filesystem/referenceSourceCleanupOwnership.test.ts index ec0e0d0182..19868a2d27 100644 --- a/tests/boundary/filesystem/referenceSourceCleanupOwnership.test.ts +++ b/tests/boundary/filesystem/referenceSourceCleanupOwnership.test.ts @@ -13,7 +13,8 @@ import { afterEach, beforeEach, expect, it, onTestFinished, vi } from "vitest"; import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; import { importReferenceSource } from "../../../src/application/ReferenceSourceImport.js"; -import { projectReferenceSourceImportError } from "../../../src/application/ReferenceSourceImportTypes.js"; +import { referenceSourceImportAnalysisError } from "../../../src/application/ReferenceSourceImportTypes.js"; +import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; import { readReferenceSource } from "../../../src/reference/ReferenceSourceReader.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { err, ok } from "../../../src/domain/result.js"; @@ -451,7 +452,9 @@ it.each([ surface === "import" && result.error.tag === "reference-source-import" ) { - const projected = projectReferenceSourceImportError(result.error); + const projected = projectAnalysisError( + referenceSourceImportAnalysisError(result.error), + ); expect(projected.details?.secondary_failures).toEqual([ expect.objectContaining({ category: "cancelled" }), ]); diff --git a/tests/boundary/filesystem/referenceSourceImport.test.ts b/tests/boundary/filesystem/referenceSourceImport.test.ts index 825d38a2c6..d35c9e176e 100644 --- a/tests/boundary/filesystem/referenceSourceImport.test.ts +++ b/tests/boundary/filesystem/referenceSourceImport.test.ts @@ -24,10 +24,7 @@ import { RegularFileReadFailure, } from "../../../src/application/RegularFileRead.js"; import { importReferenceSource } from "../../support/referenceSourceResourceScope.js"; -import { - parseReferenceSourceEntries, - projectReferenceSourceEntryFailure, -} from "../../../src/application/ReferenceSourceImportEntries.js"; +import { parseReferenceSourceEntries } from "../../../src/application/ReferenceSourceImportEntries.js"; import type { ReferenceSourceRead } from "../../../src/reference/ReferenceSourceReaderTypes.js"; import { createHistoricalSourceManifest } from "../../../src/domain/referenceSourceGraph.js"; import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; @@ -117,13 +114,24 @@ describe("reference source import projections", () => { ["file", "cancelled"], ["unknown", "unsupported"], ] as const) { - const message = projectReferenceSourceEntryFailure({ - status: "failed", - path: "safe/path", - kind, - code, - message: "Observed entry failure at /owned/fixture/path", - }); + const parsed = parseReferenceSourceEntries( + { + root: "/owned/fixture", + entries: [ + { + status: "failed", + path: "safe/path", + kind, + code, + message: "Observed entry failure at /owned/fixture/path", + }, + ], + bytesRead: 0, + limitations: [], + }, + [], + ); + const message = parsed.entries[0]?.limitations[0]; expect(message).toContain( "Observed entry failure at /owned/fixture/path", ); diff --git a/tests/boundary/filesystem/sourcePathFailures.test.ts b/tests/boundary/filesystem/sourcePathFailures.test.ts index beb66c4e41..8c5795ecfd 100644 --- a/tests/boundary/filesystem/sourcePathFailures.test.ts +++ b/tests/boundary/filesystem/sourcePathFailures.test.ts @@ -3,7 +3,7 @@ import { join } from "node:path"; import { expect, it } from "vitest"; import { captureProcessScenarioFile } from "../../../src/application/process/ProcessCli.js"; -import { projectReferenceSourceImportError } from "../../../src/application/ReferenceSourceImportTypes.js"; +import { referenceSourceImportAnalysisError } from "../../../src/application/ReferenceSourceImportTypes.js"; import { readJsonFile } from "../../../src/application/JsonFiles.js"; import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; @@ -27,7 +27,9 @@ it.skipIf(process.platform === "win32")( policy: { secretPatterns: [] }, }); if (result.ok) throw new Error("expected invalid root"); - expect(projectReferenceSourceImportError(result.error)).toMatchObject({ + expect( + projectAnalysisError(referenceSourceImportAnalysisError(result.error)), + ).toMatchObject({ code: "invalid_request", category: "invalid_input", message: expect.stringContaining(errno), diff --git a/tests/boundary/javascript/javascriptApplicationProjectionEstimate.test.ts b/tests/boundary/javascript/javascriptApplicationProjectionEstimate.test.ts index e816b5daf1..e4bb972965 100644 --- a/tests/boundary/javascript/javascriptApplicationProjectionEstimate.test.ts +++ b/tests/boundary/javascript/javascriptApplicationProjectionEstimate.test.ts @@ -3,7 +3,7 @@ import { join } from "node:path"; import { expect, it } from "vitest"; import { estimateJavaScriptStaticApplicationProjection } from "../../../src/domain/javascript/javascriptApplicationProjectionResources.js"; -import { analyzeJavaScriptStaticSource } from "../../../src/domain/javascript/javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../fixtures/javascriptStaticAnalysis.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { reconstructJavaScriptFixture, diff --git a/tests/boundary/javascript/javascriptApplicationProjectionSharedBudget.test.ts b/tests/boundary/javascript/javascriptApplicationProjectionSharedBudget.test.ts index f0792c2630..a8ca423cca 100644 --- a/tests/boundary/javascript/javascriptApplicationProjectionSharedBudget.test.ts +++ b/tests/boundary/javascript/javascriptApplicationProjectionSharedBudget.test.ts @@ -28,7 +28,7 @@ vi.mock("node:v8", async (importOriginal) => { import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; import { estimateJavaScriptStaticApplicationProjection } from "../../../src/domain/javascript/javascriptApplicationProjectionResources.js"; import type { JavaScriptArtifactFile } from "../../../src/domain/javascript/javascriptArtifactFiles.js"; -import { analyzeJavaScriptStaticSource } from "../../../src/domain/javascript/javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../fixtures/javascriptStaticAnalysis.js"; import { JavaScriptAnalysisWorker } from "../../../src/javascript/analysis/JavaScriptAnalysisWorker.js"; import { javaScriptWorkerStaticAnalysisSchema } from "../../../src/javascript/analysis/JavaScriptAnalysisWorkerSchemas.js"; import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js"; diff --git a/tests/boundary/javascript/javascriptPreloadDiscovery.test.ts b/tests/boundary/javascript/javascriptPreloadDiscovery.test.ts index 2947dab782..2201eac40e 100644 --- a/tests/boundary/javascript/javascriptPreloadDiscovery.test.ts +++ b/tests/boundary/javascript/javascriptPreloadDiscovery.test.ts @@ -8,7 +8,7 @@ import type { JavaScriptArtifactFile, JavaScriptArtifactFileSet, } from "../../../src/domain/javascript/javascriptArtifactFiles.js"; -import { analyzeParsedJavaScriptStaticSource } from "../../../src/domain/javascript/javascriptStaticAnalysis.js"; +import { analyzeParsedJavaScriptStaticSource } from "../../fixtures/javascriptStaticAnalysis.js"; import { parseJavaScriptSource } from "../../../src/domain/javascript/javascriptSourceParser.js"; import { ok, err } from "../../../src/domain/result.js"; import { discoverJavaScriptPreloadPaths } from "../../../src/application/javascript/JavaScriptPreloadDiscovery.js"; diff --git a/tests/boundary/javascript/loaderInvocationMethodAuthority.test.ts b/tests/boundary/javascript/loaderInvocationMethodAuthority.test.ts index 216f57fbab..8a7371500b 100644 --- a/tests/boundary/javascript/loaderInvocationMethodAuthority.test.ts +++ b/tests/boundary/javascript/loaderInvocationMethodAuthority.test.ts @@ -1,7 +1,7 @@ import { execFileSync } from "node:child_process"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "../../../src/domain/javascript/javascriptStaticAnalysis.js"; +import { analyzeJavaScriptStaticSource } from "../../fixtures/javascriptStaticAnalysis.js"; it.each( (["call", "apply"] as const).flatMap((method) => diff --git a/tests/boundary/mcp/batchDecompileViewsMcp.test.ts b/tests/boundary/mcp/batchDecompileViewsMcp.test.ts index 4ee5ca96de..810d9fccdd 100644 --- a/tests/boundary/mcp/batchDecompileViewsMcp.test.ts +++ b/tests/boundary/mcp/batchDecompileViewsMcp.test.ts @@ -5,7 +5,6 @@ import type { CallToolResult } from "@modelcontextprotocol/server"; import { afterAll, expect, it, onTestFinished, vi } from "vitest"; import { z } from "zod"; import { ANALYSIS_VIEW_TOOL_CONTRACTS } from "../../../src/contracts/analysisViewToolContracts.js"; -import { batchUnknowns } from "../../../src/domain/analysisView/batchDecompileView.js"; import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; import { AnalysisOutputError } from "../../../src/domain/analysisErrorCore.js"; import { @@ -266,11 +265,10 @@ it.each(["full", "compact"] as const)( }, }, }); - expect(summary.unknowns).toEqual( - batchUnknowns( - batchDecompileResultSchema.parse(parent.normalized_result).items, - ), - ); + expect(summary.unknowns).toEqual([ + expect.stringContaining("1 items have unknown procedure identity"), + expect.stringContaining("Evidence limitations aggregate"), + ]); const callsAfterBatch = harness.calls.count; // The same summary is reachable later through the retained parent. diff --git a/tests/boundary/mcp/buildCaptureMcp.test.ts b/tests/boundary/mcp/buildCaptureMcp.test.ts index 5a6432f5b0..4cc853767b 100644 --- a/tests/boundary/mcp/buildCaptureMcp.test.ts +++ b/tests/boundary/mcp/buildCaptureMcp.test.ts @@ -11,7 +11,6 @@ import { } from "../../../src/application/desktop/desktopCapture.fixture.js"; import { toolContract } from "../../../src/contracts/toolContracts.js"; import type { DesktopApplicationCapture } from "../../../src/domain/desktop/desktopCapture.js"; -import { seedWording } from "../../../src/domain/desktop/appExport.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { LINUX_ARGUMENT_VECTOR_BYTES } from "../../../src/desktop/BuildComputerController.js"; import { @@ -191,11 +190,7 @@ it("captures the app inside the computer, acknowledges checkpoints, drafts the t .parameters, ).toMatchObject({ synthetic_seed: true }); expect(await readFile(join(directory, "README.md"), "utf8")).toContain( - seedWording({ - name: exported.environment, - seedCheckpoint: "seed", - syntheticSeed: true, - }).notice, + `The seed was made for this task, not taken from a user's own data; read \`envs/${exported.environment}/seed/\` before publishing the bundle.`, ); expectNoToken(computer, [started, seed, stopped, exported]); }); diff --git a/tests/boundary/mcp/buildComputerMcp.test.ts b/tests/boundary/mcp/buildComputerMcp.test.ts index 1787ecf63b..121a3a893d 100644 --- a/tests/boundary/mcp/buildComputerMcp.test.ts +++ b/tests/boundary/mcp/buildComputerMcp.test.ts @@ -14,7 +14,6 @@ import type { CallToolResult } from "@modelcontextprotocol/server"; import { expect, it, vi } from "vitest"; import { z } from "zod"; -import { seededLockStartNotice } from "../../../src/application/desktop/DesktopBuildComputers.js"; import { toolContract } from "../../../src/contracts/toolContracts.js"; import { buildComputerFakes as fakes, @@ -333,7 +332,12 @@ it("points a bundle's start that never got ready at the lock files its seed hold "envs/calc/seed/Documents/q1.ods.LCK", ]; expect(pointed).toContain(notReady); - expect(pointed).toContain(seededLockStartNotice(locks)); + expect(pointed).toContain( + "The bundle's seed holds lock files, which can keep the app from starting", + ); + expect(pointed).toContain( + "Delete them from the bundle, or export it again, which leaves them out, and start again.", + ); for (const lock of locks) expect(pointed).toContain(lock); expect(pointed).not.toContain("Documents/notes.lck"); expect(unlocked).not.toContain(locks[0]); diff --git a/tests/boundary/mcp/ghidraDecompileDeadlineLifetime.test.ts b/tests/boundary/mcp/ghidraDecompileDeadlineLifetime.test.ts index 2631660d4e..59104ae2ed 100644 --- a/tests/boundary/mcp/ghidraDecompileDeadlineLifetime.test.ts +++ b/tests/boundary/mcp/ghidraDecompileDeadlineLifetime.test.ts @@ -14,10 +14,7 @@ import { parseEvidence } from "../../../src/domain/evidence.js"; import { GhidraClient } from "../../../src/ghidra/GhidraClient.js"; import { ghidraProfileInstallationHost } from "../../../src/ghidra/GhidraInstallation.fixture.js"; import { GhidraProvider } from "../../../src/ghidra/GhidraProvider.js"; -import { - GHIDRA_SESSION_RETAINED, - ghidraDecompileTimeoutRecovery, -} from "../../../src/ghidra/GhidraProviderClient.js"; +import { ghidraDecompileTimeoutRecovery } from "../../../src/ghidra/GhidraProviderClient.js"; import { silentLogger } from "../../../src/logger.js"; import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; @@ -32,6 +29,7 @@ const environment = { }; const BRIDGE_MESSAGE = "Ghidra decompilation of 0x1000 exceeded the 1-second configured decompilation limit (ghidra.decompile_timeout_seconds; launch seed REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS)"; +const GHIDRA_SESSION_RETAINED = "The Ghidra session remains usable."; const ghidraSession = (launcher: GhidraFixtureLauncher) => { const config = parseConfig(environment); diff --git a/tests/boundary/mcp/nativeValueTraceCleanupIncomplete.test.ts b/tests/boundary/mcp/nativeValueTraceCleanupIncomplete.test.ts index b2793892a7..af3a76ba64 100644 --- a/tests/boundary/mcp/nativeValueTraceCleanupIncomplete.test.ts +++ b/tests/boundary/mcp/nativeValueTraceCleanupIncomplete.test.ts @@ -6,8 +6,8 @@ import { afterEach, describe, expect, it } from "vitest"; import { z } from "zod"; import type { AnalysisOperationPort } from "../../../src/application/AnalysisProvider.js"; -import { CLEANUP_STOPPED_VALUE_TRACE } from "../../../src/application/native/NativeValueTrace.js"; import { + EXPECTED_CLEANUP_STOPPED_TRACE, failingValueTraceReads, valueTraceDiamond, valueTraceProvider as provider, @@ -148,7 +148,7 @@ describe("trace_native_values after a validated seed", () => { }); expect(admitted.unknowns).toContainEqual({ ...stop, - reason: CLEANUP_STOPPED_VALUE_TRACE, + reason: EXPECTED_CLEANUP_STOPPED_TRACE, }); // The stopping failure is the outcome, not one of the trace's unknowns. expect(JSON.stringify(admitted.unknowns)).not.toContain(CHILD_FAILURE); @@ -214,7 +214,7 @@ describe("trace_native_values after a validated seed", () => { { procedure: "0x1000", address: "0x1008", - reason: CLEANUP_STOPPED_VALUE_TRACE, + reason: EXPECTED_CLEANUP_STOPPED_TRACE, }, ]); }); diff --git a/tests/boundary/mcp/providerSelectionMcp.test.ts b/tests/boundary/mcp/providerSelectionMcp.test.ts index 9f09ae44f5..03d2e603e3 100644 --- a/tests/boundary/mcp/providerSelectionMcp.test.ts +++ b/tests/boundary/mcp/providerSelectionMcp.test.ts @@ -24,7 +24,6 @@ import { } from "../../../src/application/AnalysisProvider.js"; import { AnalysisProviderRegistry } from "../../../src/application/binary/AnalysisProviderRegistry.js"; import { composeBinarySession } from "../../../src/application/binary/BinarySessionComposition.js"; -import { undeclaredOperationGuidance } from "../../../src/application/binary/BinarySessionExecution.js"; import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js"; import { analysisErrorRemediationAction } from "../../../src/domain/analysisErrorPresentation.js"; import { AnalysisSnapshotMismatchError } from "../../../src/domain/evidenceErrors.js"; @@ -275,16 +274,15 @@ describe("provider selection over MCP", () => { message: expect.stringContaining("read_bytes"), details: { provider_id: "beta", operation: "read_bytes" }, remediation: { - action: undeclaredOperationGuidance("read_bytes"), + action: expect.stringMatching( + /binary_session.*complete.*open_binary.*provider_id/u, + ), }, }, }); - const remediation = undeclaredOperationGuidance("read_bytes"); - expect(parseMcpToolError(unsupported).error.message).not.toContain( - remediation, - ); - expect(remediation).toMatch( - /binary_session.*complete.*open_binary.*provider_id/u, + const unsupportedError = parseMcpToolError(unsupported).error; + expect(unsupportedError.message).not.toContain( + unsupportedError.remediation.action, ); expect(starts).toEqual(["beta"]); @@ -935,8 +933,8 @@ it.each([true, false])( const error = parseMcpToolError(reply).error; expect(error.code).toBe("capability_unavailable"); expect(error.details).toMatchObject({ provider_id: "beta", operation }); - expect(error.remediation.action).toBe( - undeclaredOperationGuidance(operation, entryCommentAvailable), + expect(error.remediation.action).toMatch( + /binary_session.*complete.*open_binary.*provider_id/u, ); if (entryCommentAvailable) { expect(error.remediation.action).toContain("annotate_native_function"); diff --git a/tests/boundary/mcp/sessionAvailabilityRecovery.test.ts b/tests/boundary/mcp/sessionAvailabilityRecovery.test.ts index 6e639680b1..0122663abf 100644 --- a/tests/boundary/mcp/sessionAvailabilityRecovery.test.ts +++ b/tests/boundary/mcp/sessionAvailabilityRecovery.test.ts @@ -17,10 +17,6 @@ import { AnalysisTimeoutError, } from "../../../src/domain/analysisErrorCore.js"; import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js"; -import { - inferredUnavailabilityReason, - runtimeFailureReason, -} from "../../../src/application/binary/SessionRuntimeAvailability.js"; import { ProviderAdapterError } from "../../../src/domain/providerAdapterError.js"; import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js"; import { err, ok, type Result } from "../../../src/domain/result.js"; @@ -368,22 +364,17 @@ const liveCapability = ( const adapterFailure = (provider: ProviderIdentity, operation: string) => Promise.resolve(err(new ProviderAdapterError(provider.id, operation))); -const recordedReason = (failure: AnalysisError): string => { - const reason = runtimeFailureReason(failure); - if (reason === undefined) throw new Error(`${failure._tag} is not recorded`); - return reason; -}; -// Compare remediation with its owning formatters; the facts are their inputs. const observedFailure = (operation: string, provider: string = FIXTURE.id) => - recordedReason(new ProviderAdapterError(provider, operation)); + expect.stringContaining( + `Provider ${provider} adapter failed during ${operation}`, + ); const inferredFailure = (operation: string, provider: string = FIXTURE.id) => - inferredUnavailabilityReason(provider, { - operation, - reason: observedFailure(operation, provider), - }); + expect.stringContaining( + `Inferred from the ${operation} failure (ProviderAdapterError: Provider ${provider} adapter failed during ${operation}). Retry after repairing the provider; a later successful ${provider} request clears this.`, + ); const refusal = (operation: string, reason: string) => - recordedReason( - new AnalysisCapabilityUnavailableError(FIXTURE.id, operation, reason), + expect.stringContaining( + `Provider ${FIXTURE.id} cannot execute ${operation}: ${reason}`, ); /** Hold provider replies; release and join every launched call. */ diff --git a/tests/boundary/native/PwndbgCoreContext.test.ts b/tests/boundary/native/PwndbgCoreContext.test.ts index f91ea2c8a5..1c89134fc0 100644 --- a/tests/boundary/native/PwndbgCoreContext.test.ts +++ b/tests/boundary/native/PwndbgCoreContext.test.ts @@ -1,8 +1,8 @@ -import { access, readFile } from "node:fs/promises"; +import { access, chmod, mkdir, readFile, writeFile } from "node:fs/promises"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { join } from "node:path"; -import { expect, it, onTestFinished } from "vitest"; +import { expect, it, onTestFinished, vi } from "vitest"; import { OwnedCommandFailure } from "../../../src/process/OwnedCommand.js"; import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; import { @@ -18,6 +18,36 @@ import { inspectPwndbgCore } from "../../../src/native/pwndbg/PwndbgCoreContext. const diagnostics = { stdout: "", stderr: "", truncated: false }; +const accessFault = vi.hoisted(() => ({ + handler: undefined as (() => Promise) | undefined, +})); + +vi.mock("node:fs/promises", async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + access: async (...args: Parameters) => { + if (accessFault.handler !== undefined) return accessFault.handler(); + return original.access(...args); + }, + }; +}); + +const prepareDebuggerPaths = async (root: string) => { + const gdb = join(root, "gdb"); + const init = join(root, "gdbinit.py"); + const venv = join(root, "venv"); + await writeFile(gdb, ""); + await chmod(gdb, 0o700); + await writeFile(init, ""); + await mkdir(venv); + return { + REA_PWNDBG_GDB: gdb, + REA_PWNDBG_GDBINIT: init, + REA_PWNDBG_VENV_PATH: venv, + }; +}; + it.each(["pre-aborted", "aborted-during-probe"] as const)( "observes cancellation at the debugger admission checkpoint: %s", async (scenario) => { @@ -27,7 +57,7 @@ it.each(["pre-aborted", "aborted-during-probe"] as const)( const reason = { cancelled: scenario }; const gdb = join(workspace.root, "missing-gdb"); let probes = 0; - const probePath = async () => { + accessFault.handler = async () => { probes++; if (scenario === "aborted-during-probe") { await new Promise((resolve) => { @@ -40,6 +70,9 @@ it.each(["pre-aborted", "aborted-during-probe"] as const)( } throw new Error("path is unavailable"); }; + onTestFinished(() => { + accessFault.handler = undefined; + }); if (scenario === "pre-aborted") controller.abort(reason); const inspection = inspectPwndbgCore({ environment: { @@ -52,7 +85,6 @@ it.each(["pre-aborted", "aborted-during-probe"] as const)( resources: new ArtifactResourceScope(), diagnostics, options: { signal: controller.signal }, - probePath, timeoutMs: 30_000, runCommand: () => { throw new Error("No process should exist before cancellation"); @@ -65,6 +97,7 @@ it.each(["pre-aborted", "aborted-during-probe"] as const)( ); expect(failure.cause).toBe(reason); expect(probes).toBe(scenario === "pre-aborted" ? 0 : 1); + accessFault.handler = undefined; await expect( access(join(workspace.root, "debugger")), ).rejects.toMatchObject({ @@ -75,6 +108,42 @@ it.each(["pre-aborted", "aborted-during-probe"] as const)( const execute = promisify(execFile); +it.each([ + ["REA_PWNDBG_GDBINIT", "directory", "file"], + ["REA_PWNDBG_VENV_PATH", "file", "directory"], +] as const)( + "rejects a %s path with the wrong kind", + async (setting, given, expected) => { + const workspace = await createTestWorkspace("rea-pwndbg-path-kind-"); + onTestFinished(() => removeTestWorkspace(workspace.root)); + const environment = await prepareDebuggerPaths(workspace.root); + const wrong = join(workspace.root, "wrong-kind"); + if (given === "directory") await mkdir(wrong); + else await writeFile(wrong, ""); + const failure = await inspectPwndbgCore({ + environment: { ...environment, [setting]: wrong }, + architecture: "x86_64-little-endian", + rootPath: workspace.root, + resources: new ArtifactResourceScope(), + diagnostics, + options: undefined, + timeoutMs: 30_000, + runCommand: () => { + throw new Error("No command may start after path refusal"); + }, + }).catch((cause: unknown) => cause); + expect(failure).toMatchObject({ + _tag: "AnalysisCapabilityUnavailableError", + message: expect.stringContaining( + `${setting} is unavailable at ${wrong}: Expected a ${expected}.`, + ), + }); + await expect( + access(join(workspace.root, "debugger")), + ).rejects.toMatchObject({ code: "ENOENT" }); + }, +); + it("establishes core-only debugger state before querying recorded mappings", async () => { await execute( "python3", @@ -91,20 +160,16 @@ it.each(["x86_64-little-endian", "aarch64-little-endian"] as const)( async (architecture) => { const workspace = await createTestWorkspace("rea-pwndbg-profile-"); onTestFinished(() => removeTestWorkspace(workspace.root)); + const environment = await prepareDebuggerPaths(workspace.root); let requested = false; await expect( inspectPwndbgCore({ architecture, - environment: { - REA_PWNDBG_GDB: join(workspace.root, "gdb"), - REA_PWNDBG_GDBINIT: join(workspace.root, "gdbinit.py"), - REA_PWNDBG_VENV_PATH: join(workspace.root, "venv"), - }, + environment, rootPath: workspace.root, resources: new ArtifactResourceScope(), diagnostics, options: undefined, - probePath: async () => {}, timeoutMs: 30_000, runCommand: async () => { const request: unknown = JSON.parse( @@ -131,6 +196,7 @@ it.each(["cancelled", "physical-name", "physical-identity"])( async (outcome) => { const workspace = await createTestWorkspace("rea-pwndbg-command-outcome-"); onTestFinished(() => removeTestWorkspace(workspace.root)); + const environment = await prepareDebuggerPaths(workspace.root); const controller = new AbortController(); const reason = { stage: "command handoff" }; const failure = new OwnedCommandFailure( @@ -142,17 +208,12 @@ it.each(["cancelled", "physical-name", "physical-identity"])( ); if (outcome === "physical-name") failure.name = "AbortError"; const inspection = inspectPwndbgCore({ - environment: { - REA_PWNDBG_GDB: workspace.path("gdb"), - REA_PWNDBG_GDBINIT: workspace.path("gdbinit.py"), - REA_PWNDBG_VENV_PATH: workspace.path("venv"), - }, + environment, architecture: "x86_64-little-endian", rootPath: workspace.root, resources: new ArtifactResourceScope(), diagnostics: { stdout: "basic evidence", stderr: "", truncated: false }, options: { signal: controller.signal }, - probePath: async () => {}, timeoutMs: 30_000, runCommand: async () => { controller.abort(outcome === "physical-identity" ? failure : reason); @@ -175,18 +236,14 @@ it.each(["cancelled", "physical-name", "physical-identity"])( it("reports a non-default debugger deadline instead of 30000 ms", async () => { const workspace = await createTestWorkspace("rea-pwndbg-deadline-"); onTestFinished(() => removeTestWorkspace(workspace.root)); + const environment = await prepareDebuggerPaths(workspace.root); const failure = await inspectPwndbgCore({ - environment: { - REA_PWNDBG_GDB: workspace.path("gdb"), - REA_PWNDBG_GDBINIT: workspace.path("gdbinit.py"), - REA_PWNDBG_VENV_PATH: workspace.path("venv"), - }, + environment, architecture: "x86_64-little-endian", rootPath: workspace.root, resources: new ArtifactResourceScope(), diagnostics, options: undefined, - probePath: async () => {}, timeoutMs: 60_000, runCommand: () => { throw new OwnedCommandFailure("timeout", "deadline expired"); @@ -206,6 +263,13 @@ it("retains a physical debugger path failure when cancellation arrives in the pr onTestFinished(() => removeTestWorkspace(workspace.root)); const controller = new AbortController(); const physical = Object.assign(new Error("path denied"), { code: "EACCES" }); + accessFault.handler = async () => { + controller.abort({ late: true }); + throw physical; + }; + onTestFinished(() => { + accessFault.handler = undefined; + }); const inspection = inspectPwndbgCore({ environment: { REA_PWNDBG_GDB: workspace.path("gdb"), @@ -218,10 +282,6 @@ it("retains a physical debugger path failure when cancellation arrives in the pr diagnostics, options: { signal: controller.signal }, timeoutMs: 30_000, - probePath: async () => { - controller.abort({ late: true }); - throw physical; - }, runCommand: () => { throw new Error("No command may start after probe failure"); }, diff --git a/tests/boundary/process/jadxIntegration.test.ts b/tests/boundary/process/jadxIntegration.test.ts index 1acfd3d810..7917ab291e 100644 --- a/tests/boundary/process/jadxIntegration.test.ts +++ b/tests/boundary/process/jadxIntegration.test.ts @@ -366,12 +366,15 @@ it("rejects a copied JAR that differs from its admitted session identity", async admitted, root.path, "inspect_android_package", + undefined, + (_path, owner) => hashOwners.push(owner), ), ).rejects.toMatchObject({ _tag: "AnalysisCapabilityUnavailableError", reason: expect.stringContaining("bytes changed"), }); } finally { + await Promise.all(hashOwners.map((owner) => owner.close())); await root.close(); } }); diff --git a/tests/boundary/providers/desktopExportPython.test.ts b/tests/boundary/providers/desktopExportPython.test.ts index c892821daa..70cbf4ee27 100644 --- a/tests/boundary/providers/desktopExportPython.test.ts +++ b/tests/boundary/providers/desktopExportPython.test.ts @@ -30,8 +30,7 @@ import { import { detectStateFormat, keyfileView, - isJsonText, - jsonView, + textSyntaxFormat, textualView, } from "../../../src/domain/desktop/stateFormats.js"; import { safeParseJson } from "../../../src/domain/safeJson.js"; @@ -539,7 +538,7 @@ it("refuses the JSON numbers REA's state view refuses, as JSON with an error rat const python = pythonJson(texts); texts.forEach((text, index) => { - const typescript = jsonView(text); + const typescript = textualView("json", new TextEncoder().encode(text)); const verdict = python[index]; expect(verdict?.error === undefined).toBe(typescript.ok); if (typescript.ok) expect(verdict?.view).toEqual(typescript.value); @@ -733,7 +732,9 @@ it("checks text for JSON the way REA does, token by token, without parsing it", ) as { syntax: boolean[]; built: boolean[] }; const parsed = texts.map((text) => safeParseJson(text).ok); - expect(texts.map((text) => isJsonText(text))).toEqual(parsed); + expect( + texts.map((text) => textSyntaxFormat("fixture.json", text) === "json"), + ).toEqual(parsed); expect(python.syntax).toEqual(parsed); expect(python.built).toEqual(parsed); expect(parsed.filter(Boolean)).toHaveLength(10); diff --git a/tests/boundary/providers/ghidra/ghidraProjectSnapshot.test.ts b/tests/boundary/providers/ghidra/ghidraProjectSnapshot.test.ts index effb563d4a..3da07249c0 100644 --- a/tests/boundary/providers/ghidra/ghidraProjectSnapshot.test.ts +++ b/tests/boundary/providers/ghidra/ghidraProjectSnapshot.test.ts @@ -15,13 +15,14 @@ import { ArtifactResourceScope } from "../../../../src/artifacts/ArtifactResourc import { copyExistingGhidraProject, fingerprintGhidraProject, - GHIDRA_PROJECT_PROFILE_MISMATCH, resolveGhidraProject, } from "../../../../src/ghidra/GhidraProjectSnapshot.js"; import { parseConfig } from "../../../../src/config/parseConfig.js"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; let resources: ArtifactResourceScope; +const GHIDRA_PROJECT_PROFILE_MISMATCH = + "Selected Ghidra project content no longer matches the profile committed by open_binary; when it changed is not established"; beforeEach(() => { resources = new ArtifactResourceScope(); }); diff --git a/tests/boundary/providers/native/artifactProvider.extractionAdmission.test.ts b/tests/boundary/providers/native/artifactProvider.extractionAdmission.test.ts index e92dcc5b48..3d3fd3c6ff 100644 --- a/tests/boundary/providers/native/artifactProvider.extractionAdmission.test.ts +++ b/tests/boundary/providers/native/artifactProvider.extractionAdmission.test.ts @@ -18,7 +18,10 @@ import * as pckLocation from "../../../../src/artifacts/GodotPckLocation.js"; import * as seaLocation from "../../../../src/artifacts/nodeSea/NodeSeaLocation.js"; import * as pyInstallerLocation from "../../../../src/artifacts/pyinstaller/CArchiveLayout.js"; import { nodeSeaElf } from "../../../../src/artifacts/nodeSea/NodeSea.fixture.js"; -import { cArchiveExecutable } from "../../../../src/artifacts/pyinstaller/CArchive.fixture.js"; +import { + CARCHIVE_COOKIE_MAGIC, + cArchiveExecutable, +} from "../../../../src/artifacts/pyinstaller/CArchive.fixture.js"; import { artifactExtractionResultSchema } from "../../../../src/domain/artifactGraph.js"; import { OwnedFileHandle } from "../../../../src/filesystem/OwnedFileHandle.js"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; @@ -395,7 +398,7 @@ it("keeps incidental rejected cookie signatures unsupported during native extrac pythonVersion: 314, members: [], }).subarray(0, 64), - pyInstallerLocation.PYINSTALLER_COOKIE_MAGIC, + CARCHIVE_COOKIE_MAGIC, Buffer.alloc(88), ]); await writeFile(source, bytes); diff --git a/tests/boundary/providers/native/lldbRetainedObservations.test.ts b/tests/boundary/providers/native/lldbRetainedObservations.test.ts index 4f921759f7..1404c19d22 100644 --- a/tests/boundary/providers/native/lldbRetainedObservations.test.ts +++ b/tests/boundary/providers/native/lldbRetainedObservations.test.ts @@ -1,16 +1,17 @@ import { describe, expect, it } from "vitest"; import type { FileHandle } from "node:fs/promises"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; import { err, ok, type Result } from "../../../../src/domain/result.js"; +import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; import { nativeCallPartialObservationSchema } from "../../../../src/domain/native/nativeCallPartialObservation.js"; import { - parseLldbObservationJournal, projectLldbPartialObservation, - readBoundedPrefix, readLldbObservationJournal, readPartialCapture, } from "../../../../src/native/LldbRetainedObservations.js"; -import type { LldbReadbackFiles } from "../../../../src/native/LldbReadbackFiles.js"; +import { LldbReadbackFiles } from "../../../../src/native/LldbReadbackFiles.js"; const event = { sequence: 0, @@ -28,11 +29,24 @@ const event = { backtrace: [], }; -const observation = ( +const readJournal = async (bytes: Buffer) => { + const root = await createTestTempDirectory("rea-lldb-journal-"); + const path = join(root, "journal"); + await writeFile(path, bytes); + const result = await readLldbObservationJournal( + path, + new LldbReadbackFiles(), + bytes.length, + ); + if (!result.ok) throw result.error; + return result.value.parsed; +}; + +const observation = async ( reason: "cancelled" | "tracer-failure" | "cleanup-failure", journalText: string, ) => { - const parsed = parseLldbObservationJournal(Buffer.from(journalText)); + const parsed = await readJournal(Buffer.from(journalText)); return nativeCallPartialObservationSchema.parse( projectLldbPartialObservation({ target: { @@ -54,7 +68,7 @@ const observation = ( }; describe("LLDB retained observations", () => { - it("preserves partial source coordinates while accepting historical omission", () => { + it("preserves partial source coordinates while accepting historical omission", async () => { const attributed = { ...event, source_location: { @@ -67,7 +81,7 @@ describe("LLDB retained observations", () => { [event, attributed] .map((entry) => JSON.stringify({ kind: "event", event: entry })) .join("\n") + "\n"; - const partial = observation("cancelled", journal); + const partial = await observation("cancelled", journal); expect(partial.events).toEqual([event, attributed]); expect(partial.limitations).toContain( "Source paths and lines are LLDB line-table attribution; REA did not read or verify the referenced source bytes.", @@ -78,9 +92,9 @@ describe("LLDB retained observations", () => { ["cancellation", "cancelled"], ["bridge failure", "tracer-failure"], ["cleanup failure", "cleanup-failure"], - ] as const)("retains admitted events after %s", (_case, reason) => { + ] as const)("retains admitted events after %s", async (_case, reason) => { const journal = `${JSON.stringify({ kind: "event", event })}\n`; - const partial = observation(reason, journal); + const partial = await observation(reason, journal); expect(partial.events).toEqual([event]); expect(partial.coverage).toEqual({ status: "partial", reason }); expect(partial.process.stdout).toMatchObject({ @@ -99,14 +113,14 @@ describe("LLDB retained observations", () => { ); }); - it("keeps valid rows around malformed data and ignores a torn final row", () => { + it("keeps valid rows around malformed data and ignores a torn final row", async () => { const journal = [ JSON.stringify({ kind: "event", event }), "{bad-json}", JSON.stringify({ kind: "other-stop", reason: "signal SIGTERM" }), '{"kind":"event","event":', ].join("\n"); - const partial = observation("tracer-failure", journal); + const partial = await observation("tracer-failure", journal); expect(partial.events).toEqual([event]); expect(partial.process.other_stops).toEqual(["signal SIGTERM"]); expect(partial.limitations).toContain( @@ -117,7 +131,7 @@ describe("LLDB retained observations", () => { ); }); - it("rejects invalid UTF-8 rows while retaining valid neighbors and literal replacement characters", () => { + it("rejects invalid UTF-8 rows while retaining valid neighbors and literal replacement characters", async () => { const before = Buffer.from(`${JSON.stringify({ kind: "event", event })}\n`); const invalid = Buffer.concat([ Buffer.from('{"kind":"other-stop","reason":"before'), @@ -130,7 +144,7 @@ describe("LLDB retained observations", () => { const after = Buffer.from( `${JSON.stringify({ kind: "event", event: { ...event, sequence: 1 } })}\n`, ); - const parsed = parseLldbObservationJournal( + const parsed = await readJournal( Buffer.concat([before, invalid, literalReplacement, after]), ); @@ -141,7 +155,7 @@ describe("LLDB retained observations", () => { ); }); - it("ignores a torn non-ASCII tail without discarding completed rows", () => { + it("ignores a torn non-ASCII tail without discarding completed rows", async () => { const complete = Buffer.from( `${JSON.stringify({ kind: "event", event })}\n`, ); @@ -149,7 +163,7 @@ describe("LLDB retained observations", () => { Buffer.from('{"kind":"other-stop","reason":"'), Buffer.from([0xe2, 0x82]), ]); - const parsed = parseLldbObservationJournal(Buffer.concat([complete, torn])); + const parsed = await readJournal(Buffer.concat([complete, torn])); expect(parsed.events).toEqual([event]); expect(parsed.otherStops).toEqual([]); @@ -163,7 +177,7 @@ describe("LLDB retained observations", () => { }); describe("LLDB retained observation read failures", () => { - it("bounds malformed-row diagnostics for an 8 MiB journal", () => { + it("bounds malformed-row diagnostics for an 8 MiB journal", async () => { const valid = JSON.stringify({ kind: "event", event }); const malformed = '{"kind":"unknown"}\n'; const malformedRows = Math.floor( @@ -171,7 +185,7 @@ describe("LLDB retained observation read failures", () => { ); const journal = `${valid}\n${malformed.repeat(malformedRows)}${valid}\n`; expect(Buffer.byteLength(journal)).toBeLessThanOrEqual(8 * 1024 * 1024); - const partial = observation("tracer-failure", journal); + const partial = await observation("tracer-failure", journal); expect(partial.events).toEqual([event, event]); expect(partial.limitations.length).toBeLessThanOrEqual(6); expect( @@ -184,22 +198,40 @@ describe("LLDB retained observation read failures", () => { }); it("continues a bounded prefix read after short reads", async () => { - const source = Buffer.from("retained prefix"); + const source = Buffer.from(`${JSON.stringify({ kind: "event", event })}\n`); let position = 0; - const result = await readBoundedPrefix( - source.length, - async (buffer, offset, length, requestedPosition) => { - expect(requestedPosition).toBe(position); - const count = Math.min(2, length, source.length - position); - source.copy(buffer, offset, position, position + count); - position += count; - return count; + const files = { + read: async ( + _path: string, + read: (file: FileHandle) => Promise, + ): Promise> => { + const handle = { + stat: async () => ({ size: source.length }), + read: async ( + buffer: Buffer, + offset: number, + length: number, + requestedPosition: number, + ) => { + expect(requestedPosition).toBe(position); + const count = Math.min(2, length, source.length - position); + source.copy(buffer, offset, position, position + count); + position += count; + return { bytesRead: count, buffer }; + }, + } as unknown as FileHandle; + try { + return ok(await read(handle)); + } catch (cause) { + return err(cause); + } }, - ); - expect(result.bytesRead).toBe(source.length); - expect(result.buffer.toString("utf8", 0, result.bytesRead)).toBe( - "retained prefix", - ); + } as LldbReadbackFiles; + const result = await readLldbObservationJournal("journal", files); + if (!result.ok) throw result.error; + expect(position).toBe(source.length); + expect(result.value.status).toBe("complete"); + expect(result.value.parsed.events).toEqual([event]); }); it("reads the selected partial capture and journal budgets instead of fixed defaults", async () => { diff --git a/tests/boundary/providers/native/nativeSelectedArchitecture.test.ts b/tests/boundary/providers/native/nativeSelectedArchitecture.test.ts index 5bd4889ec0..6abfb67353 100644 --- a/tests/boundary/providers/native/nativeSelectedArchitecture.test.ts +++ b/tests/boundary/providers/native/nativeSelectedArchitecture.test.ts @@ -325,6 +325,30 @@ it("uses the uniquely matching FAT entry when lipo omits the selected slice offs }); }); +it.each([ + ["offset 8192\n", "offset 12288\n"], + ["offset 8192\n size 4096\n", "offset 8192\n size 4095\n"], +] as const)( + "refuses lipo slice facts that disagree with the selected FAT entry", + async (original, replacement) => { + const fixture = await makeFat(["x86_64", "arm64"]); + const lipo = fixture.lipo.replace(original, replacement); + expect(lipo).not.toBe(fixture.lipo); + const result = await inspect( + new NativeMacOSProvider( + {}, + new NativeFixtureRunner({ lipo }), + "darwin", + ).createClient(fixture.target), + ); + expect(result.ok).toBe(false); + if (result.ok) throw new Error("Mismatched FAT slice was admitted"); + expect(result.error.message).toContain( + "Selected lipo slice does not identify exactly one FAT entry", + ); + }, +); + it("reports the selected 32-bit slice's word size and byte order", async () => { const fixture = await makeFat(["x86_64", "i386"]); const runner = sliceSelectingRunner( diff --git a/tests/conformance/artifactComparisonDocs.test.ts b/tests/conformance/artifactComparisonDocs.test.ts index f1d02c9d06..c1a74ad785 100644 --- a/tests/conformance/artifactComparisonDocs.test.ts +++ b/tests/conformance/artifactComparisonDocs.test.ts @@ -2,14 +2,8 @@ import { readFile } from "node:fs/promises"; import { resolve } from "node:path"; import { expect, it } from "vitest"; -import { - artifactFormatMetadataSchema, - COMPARED_FORMAT_METADATA_KINDS, -} from "../../src/domain/artifactFormatMetadata.js"; +import { COMPARED_FORMAT_METADATA_KINDS } from "../../src/domain/artifactFormatMetadata.js"; -const FORMAT_METADATA_KINDS = new Set( - artifactFormatMetadataSchema.options.map((option) => option.shape.kind.value), -); // Markers, not the paragraph's wording, delimit the contract's compared kinds. const BEGIN = ""; const END = ""; @@ -27,8 +21,6 @@ it("names exactly the format metadata kinds that artifact comparison compares", expect(contracts.indexOf(END, end + 1)).toBe(-1); const named = [ ...contracts.slice(start + BEGIN.length, end).matchAll(/`([^`]+)`/gu), - ] - .map((match) => match[1] ?? "") - .filter((token) => FORMAT_METADATA_KINDS.has(token)); + ].map((match) => match[1] ?? ""); expect(new Set(named)).toEqual(new Set(COMPARED_FORMAT_METADATA_KINDS)); }); diff --git a/tests/fixtures/browserScenarioCapture.ts b/tests/fixtures/browserScenarioCapture.ts index 14af97c523..550f2c51bb 100644 --- a/tests/fixtures/browserScenarioCapture.ts +++ b/tests/fixtures/browserScenarioCapture.ts @@ -1,5 +1,5 @@ import { jsonValueSchema } from "../../src/domain/jsonValue.js"; -import { analysisViewRequestSchema } from "../../src/domain/analysisView/analysisView.js"; +import { analysisViewRequestSchema } from "../../src/domain/analysisView/analysisViewRequest.js"; import { createHash } from "node:crypto"; import { inspectAnalysisViewValidated } from "../../src/application/analysisView/AnalysisViewService.js"; diff --git a/tests/fixtures/environmentServe.ts b/tests/fixtures/environmentServe.ts index 354a9efddb..761e19a46d 100644 --- a/tests/fixtures/environmentServe.ts +++ b/tests/fixtures/environmentServe.ts @@ -1,4 +1,4 @@ -import { analysisViewRequestSchema } from "../../src/domain/analysisView/analysisView.js"; +import { analysisViewRequestSchema } from "../../src/domain/analysisView/analysisViewRequest.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; diff --git a/tests/fixtures/hopperBookmarkMutationProbe.py b/tests/fixtures/hopperBookmarkMutationProbe.py index 7f8c8fb67d..cf3e7f994d 100644 --- a/tests/fixtures/hopperBookmarkMutationProbe.py +++ b/tests/fixtures/hopperBookmarkMutationProbe.py @@ -88,7 +88,7 @@ def main(): source = Path(bridge_path).read_text(encoding="utf-8") exec(compile(source, bridge_path, "exec"), namespace) namespace["REA_TARGET_PATH"] = FakeDocumentProvider.document.getExecutableFilePath() - namespace["_configure_hopper_api"](FakeDocumentProvider) + namespace["Document"] = FakeDocumentProvider namespace["_bind_session_document"]() dispatch = namespace["_dispatch"] address = "0x401000" diff --git a/tests/fixtures/hopperBridgeFacadeProbe.py b/tests/fixtures/hopperBridgeFacadeProbe.py index 26b70a8264..840429c1b2 100644 --- a/tests/fixtures/hopperBridgeFacadeProbe.py +++ b/tests/fixtures/hopperBridgeFacadeProbe.py @@ -162,7 +162,7 @@ def main(): bridge["REA_TARGET_PATH"] = "/tmp/rea-hopper-facade-fixture" bridge["REA_OWNS_PROCESS_LIFETIME"] = False - bridge["_configure_hopper_api"](FakeDocumentProvider) + bridge["Document"] = FakeDocumentProvider bridge["_bind_session_document"]() current = bridge["_dispatch"]("current_document", {}) current_address = bridge["_dispatch"]("current_address", {}) diff --git a/tests/fixtures/hopperImageCopyFileSystem.ts b/tests/fixtures/hopperImageCopyFileSystem.ts new file mode 100644 index 0000000000..f9d761c781 --- /dev/null +++ b/tests/fixtures/hopperImageCopyFileSystem.ts @@ -0,0 +1,24 @@ +import { open } from "node:fs/promises"; + +/** Observe completed writes to a real prepared-image file. */ +export const hopperImageCopyFileSystem = ( + destination: string, + afterWrite: (bytes: number) => Promise, + openFile: typeof open = open, +) => ({ + open: (async (...args: Parameters) => { + const handle = await openFile(...args); + if (args[0] === destination) { + const writeFile = handle.writeFile.bind(handle); + handle.writeFile = (async ( + ...writeArgs: Parameters + ) => { + await writeFile(...writeArgs); + const data = writeArgs[0]; + if (!Buffer.isBuffer(data)) throw new Error("Expected a buffer write"); + await afterWrite(data.length); + }) as typeof handle.writeFile; + } + return handle; + }) as typeof open, +}); diff --git a/tests/fixtures/javascriptCommonJsAnalysis.ts b/tests/fixtures/javascriptCommonJsAnalysis.ts index b300b406cb..de43f49d3d 100644 --- a/tests/fixtures/javascriptCommonJsAnalysis.ts +++ b/tests/fixtures/javascriptCommonJsAnalysis.ts @@ -1,5 +1,5 @@ import { analyzeParsedJavaScriptSemantics } from "../../src/domain/javascript/javascriptSemanticAnalysis.js"; -import { analyzeParsedJavaScriptStaticSource } from "../../src/domain/javascript/javascriptStaticAnalysis.js"; +import { analyzeParsedJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; import { parseJavaScriptSource } from "../../src/domain/javascript/javascriptSourceParser.js"; // These fixtures model CommonJS TypeScript assets. The filename establishes diff --git a/tests/fixtures/javascriptSemanticIndex.ts b/tests/fixtures/javascriptSemanticIndex.ts new file mode 100644 index 0000000000..1abca17e2e --- /dev/null +++ b/tests/fixtures/javascriptSemanticIndex.ts @@ -0,0 +1,17 @@ +import type * as t from "@babel/types"; + +import { indexJavaScriptBindingsWithReceiversSteps } from "../../src/domain/javascript/javascriptSemanticAnalysis.js"; +import type { JavaScriptSemanticPrecisionInput } from "../../src/domain/javascript/javascriptSemanticPrecision.js"; + +export function* indexJavaScriptBindingsSteps( + program: t.Program, + precision?: JavaScriptSemanticPrecisionInput, +) { + const index = yield* indexJavaScriptBindingsWithReceiversSteps( + program, + undefined, + precision, + ); + yield* index.indexAmbientWritesSteps(); + return index.bindings; +} diff --git a/tests/fixtures/javascriptStaticAnalysis.ts b/tests/fixtures/javascriptStaticAnalysis.ts new file mode 100644 index 0000000000..72ce0a628b --- /dev/null +++ b/tests/fixtures/javascriptStaticAnalysis.ts @@ -0,0 +1,27 @@ +import { analyzeParsedJavaScriptStaticSourceSteps } from "../../src/domain/javascript/javascriptStaticAnalysis.js"; +import { failedJavaScriptStaticAnalysis } from "../../src/domain/javascript/javascriptStaticAnalysisHelpers.js"; +import type { JavaScriptSemanticPrecisionInput } from "../../src/domain/javascript/javascriptSemanticPrecision.js"; +import { completeSemanticSteps } from "../../src/domain/javascript/javascriptSemanticTraversal.js"; +import { + parseJavaScriptSource, + type ParsedJavaScriptSource, +} from "../../src/domain/javascript/javascriptSourceParser.js"; + +export const analyzeParsedJavaScriptStaticSource = ( + source: string, + file: ParsedJavaScriptSource, + precision?: JavaScriptSemanticPrecisionInput, +) => + completeSemanticSteps( + analyzeParsedJavaScriptStaticSourceSteps(source, file, precision), + ); + +export const analyzeJavaScriptStaticSource = ( + source: string, + precision?: JavaScriptSemanticPrecisionInput, +) => { + const file = parseJavaScriptSource(source); + return file === null + ? failedJavaScriptStaticAnalysis() + : analyzeParsedJavaScriptStaticSource(source, file, precision); +}; diff --git a/tests/fixtures/managedSignature.ts b/tests/fixtures/managedSignature.ts new file mode 100644 index 0000000000..eec08b9f2d --- /dev/null +++ b/tests/fixtures/managedSignature.ts @@ -0,0 +1,43 @@ +import { createHash } from "node:crypto"; + +import { + metadataSignature, + type SignatureDisplayTypeName, + type SignatureTypeName, +} from "../../src/dotnet/ManagedMemberInspectorCore.js"; +import { buildManagedPeFixture } from "../../src/dotnet/ManagedPe.fixture.js"; +import { readManagedMetadataLayout } from "../../src/dotnet/ManagedMetadataLayout.js"; +import { readManagedPeLayout } from "../../src/dotnet/ManagedPeReader.js"; + +// Supply real metadata rows for the TypeDefOrRef coordinates used by the +// signature grammar and identity cases. +const bytes = buildManagedPeFixture({ + typeSpecSignatures: [Buffer.from([0x08])], + typeReferences: Array.from({ length: 9 }, (_, index) => ({ + namespace: "Fixture", + name: `Type${String(index + 2)}`, + })), +}); +const pe = readManagedPeLayout(bytes); +if (pe.cli === null) throw new Error("Fixture has no CLI metadata"); +const layout = readManagedMetadataLayout( + bytes, + pe.rvaToOffset(pe.cli.metadata.rva, pe.cli.metadata.size, "cli.metadata"), + pe.cli.metadata.size, +); + +export const signature = ( + blob: Buffer, + rawSha256?: string, + typeName: SignatureTypeName = () => null, + displayTypeName?: SignatureDisplayTypeName, +) => + metadataSignature(blob, { + rawSha256: rawSha256 ?? createHash("sha256").update(blob).digest("hex"), + typeName, + displayTypeName, + layout, + issues: [], + scope: "fixture.signature", + blobOffset: 0, + });