refactor(tests): keep fixture adapters outside runtime

This commit is contained in:
morluto
2026-10-25 08:28:27 +08:00
parent 42c16ba631
commit fdf5a33fe2
21 changed files with 206 additions and 247 deletions
@@ -13,8 +13,10 @@ import { describe, expect, it } from "vitest";
import { createEvidence } from "../domain/evidence.js";
import { createEvidenceBundle } from "../domain/evidenceBundle.js";
import { emptyUnverifiedProcessCapture } from "../domain/process/processCapture.fixture.js";
import { FINALIZED_PROCESS_CAPTURE_EXAMPLE } from "../domain/process/processCaptureExample.js";
import {
emptyUnverifiedProcessCapture,
FINALIZED_PROCESS_CAPTURE_EXAMPLE,
} from "../domain/process/processCapture.fixture.js";
import { PROCESS_PROVIDER } from "../domain/process/processEvidenceProvider.js";
import { parseProcessCapture } from "../domain/process/processCaptureParsing.js";
import {
-31
View File
@@ -49,37 +49,6 @@ export interface RootInventorySource extends StableRegularFileDescriptor {
readonly owner: OwnedFileHandle;
}
/** Classify and hash one file root through the same stable open descriptor. */
export const classifyAndHashRoot = async (
path: string,
directory: boolean,
expectedMetadata: Stats,
options: Pick<ArtifactInventoryOptions, "resourceScope" | "signal">,
): Promise<RootClassification> => {
return options.resourceScope.run(async () => {
const result = await classifyAndHashRootForInventory(
path,
directory,
expectedMetadata,
options,
);
const rootSource = result.rootSource;
if (rootSource !== undefined) {
const owner: ArtifactResourceOwner = {
kind: "file-handle",
handle: rootSource.owner,
resource: `root artifact descriptor for ${path}`,
};
const cleanupAttempt = await options.resourceScope.release(owner);
if (cleanupAttempt.kind === "failed")
throw options.resourceScope.cleanupFailure({ kind: "completed" }, [
cleanupAttempt,
]);
}
return { format: result.format, digest: result.digest };
});
};
/** Classify a root and retain its admitted ZIP descriptor for child inventory. */
export const classifyAndHashRootForInventory = async (
path: string,
-88
View File
@@ -180,91 +180,3 @@ export const CLI_COMMANDS = Object.freeze({
/** Ordered primary CLI inventory; aliases are intentionally excluded. */
export const CLI_COMMAND_NAMES = Object.freeze(Object.values(CLI_COMMANDS));
/** Canonical MCP operations reached by CLI commands with different names. */
export const CLI_COMMAND_TOOL_ALIASES = Object.freeze({
"environment-build": ["export_environment_bundle"],
analyze: ["binary_overview"],
inspect: ["binary_overview"],
decompile: ["procedure_pseudo_code"],
function: ["analyze_function"],
instructions: ["read_function_instructions"],
trace: ["trace_feature"],
search: ["search_strings", "search_procedures"],
"capture-process": ["capture_process_scenario"],
"compare-bundles": ["compare_bundles"],
"list-objdump-architectures": ["inspect_with_objdump"],
"debug-with-gdb": ["gdb_console"],
"debug-with-rizin": ["rizin_debug_command"],
});
/** MCP operations without a dedicated CLI command or direct command alias. */
export const MCP_TOOLS_WITHOUT_DEDICATED_CLI = Object.freeze([
// MCP session control selects a resource root; CLI resource commands take paths directly.
"open_bundle_resources",
"address_name",
"comment",
"current_address",
"current_procedure",
"current_document",
"goto_address",
"inline_comment",
"list_bookmarks",
"list_documents",
"list_names",
"list_procedures",
"list_segments",
"list_strings",
"next_address",
"prev_address",
"procedure_address",
"procedure_assembly",
"procedure_callees",
"procedure_callers",
"procedure_info",
"procedure_references",
"resolve_containing_procedure",
"set_address_name",
"set_addresses_names",
"set_bookmark",
"set_comment",
"set_inline_comment",
"unset_bookmark",
"get_objc_classes",
"get_objc_protocols",
"batch_decompile",
"get_call_graph",
"analyze_swift_types",
"find_xrefs_to_name",
"trace_call_path",
"open_binary",
"close_binary",
"binary_session",
"export_evidence_bundle",
"import_evidence_bundle",
"compare_artifacts",
"compare_functions",
"find_changed_behavior",
"build_call_path",
"correlate_static_and_runtime",
"verify_reconstruction",
"list_unknowns",
"record_unknown",
"update_unknown",
"verify_unknown_resolution",
"get_evidence_bundle",
"get_navigation_context",
"inspect_address_context",
"start_gdb_session",
"gdb_session_status",
"close_gdb_session",
"start_rizin_debug_session",
"rizin_debug_session_status",
"close_rizin_debug_session",
"start_frida_session",
"load_frida_script",
"resume_frida_session",
"unload_frida_script",
"frida_session_status",
"close_frida_session",
]);
-6
View File
@@ -16,12 +16,6 @@ export const MAX_INSPECT_PE_LOAD_CONFIG_CALLBACKS = 4096;
/** IMAGE_TLS_DIRECTORY byte width for each optional-header magic. */
export const TLS_STRUCT_BYTES = { 32: 24, 64: 40 } as const;
/**
* IMAGE_LOAD_CONFIG_DIRECTORY size through GuardMemcpyFunctionPointer.
* Later fields stay unread; a larger Size does not fail the inspection.
*/
export const LOAD_CONFIG_KNOWN_STRUCT_BYTES = { 32: 192, 64: 320 } as const;
/** Complete bounded inspection of one PE TLS directory and load configuration. */
export const inspectPeLoadConfigInputSchema = z.strictObject({
path: absoluteLocalPathStringSchema.describe(
@@ -3,11 +3,44 @@ import { compareProcessCaptures } from "./processComparison.js";
import { parseProcessCapture } from "./processCaptureParsing.js";
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./processCaptureExample.js";
import { hasCaptureTruncation } from "./processCaptureCoverage.js";
import { digestProcessCommitment } from "./processScenario.js";
import type { JsonValue } from "../jsonValue.js";
import {
processCaptureSchema,
type UnverifiedProcessCapture,
} from "./processCapture.js";
const finalizedScenario = {
...EMPTY_PROCESS_CAPTURE_EXAMPLE.manifest.scenario,
finalization_ms: 1_500,
};
const finalizedComparisonContract = { finalization_ms: 1_500 };
/** Valid capture of a timeout that finalized: SIGTERM delivered, exit observed. */
export const FINALIZED_PROCESS_CAPTURE_EXAMPLE = {
...EMPTY_PROCESS_CAPTURE_EXAMPLE,
manifest: {
...EMPTY_PROCESS_CAPTURE_EXAMPLE.manifest,
scenario: finalizedScenario,
comparison_contract: finalizedComparisonContract,
full_scenario_sha256: digestProcessCommitment(finalizedScenario),
comparison_contract_sha256: digestProcessCommitment(
finalizedComparisonContract,
),
},
exit: {
code: null,
signal: 0,
reason: "timeout",
finalization: {
requested_ms: 1_500,
elapsed_ms: 12,
exit_code: 0,
signals: [{ signal: "SIGTERM", sent_at_ms: 0, delivery: "signaled" }],
},
},
} satisfies JsonValue;
/** Parse a detached valid empty capture for tests that need trusted evidence. */
export const emptyProcessCapture = () =>
parseProcessCapture(EMPTY_PROCESS_CAPTURE_EXAMPLE);
@@ -8,14 +8,14 @@ import {
} from "./processCaptureParsing.js";
import type { UnverifiedProcessCapture } from "./processCapture.js";
import { partialProcessCaptureObservationSchema } from "./processCapture.js";
import { FINALIZED_PROCESS_CAPTURE_EXAMPLE } from "./processCaptureExample.js";
import { finalizationConsistencyIssue } from "./processCaptureValidation.js";
import type { FilesystemCoverage } from "./processCaptureCoverage.js";
import {
FINALIZED_PROCESS_CAPTURE_EXAMPLE,
compareUnverifiedProcessCaptures as compareProcessCaptures,
emptyUnverifiedProcessCapture as emptyCapture,
processCaptureIssues,
} from "./processCapture.fixture.js";
import { finalizationConsistencyIssue } from "./processCaptureValidation.js";
import type { FilesystemCoverage } from "./processCaptureCoverage.js";
expectTypeOf<UnverifiedProcessCapture>().not.toExtend<ProcessCapture>();
@@ -111,31 +111,3 @@ export const EMPTY_PROCESS_CAPTURE_EXAMPLE = {
temporary_root: "removed",
},
} satisfies JsonValue;
const finalizedScenario = { ...scenario, finalization_ms: 1_500 };
const finalizedComparisonContract = { finalization_ms: 1_500 };
/** Valid capture of a timeout that finalized: SIGTERM delivered, exit observed. */
export const FINALIZED_PROCESS_CAPTURE_EXAMPLE = {
...EMPTY_PROCESS_CAPTURE_EXAMPLE,
manifest: {
...EMPTY_PROCESS_CAPTURE_EXAMPLE.manifest,
scenario: finalizedScenario,
comparison_contract: finalizedComparisonContract,
full_scenario_sha256: digestProcessCommitment(finalizedScenario),
comparison_contract_sha256: digestProcessCommitment(
finalizedComparisonContract,
),
},
exit: {
code: null,
signal: 0,
reason: "timeout",
finalization: {
requested_ms: 1_500,
elapsed_ms: 12,
exit_code: 0,
signals: [{ signal: "SIGTERM", sent_at_ms: 0, delivery: "signaled" }],
},
},
} satisfies JsonValue;
+1 -1
View File
@@ -105,7 +105,7 @@ export const locateTrailingZipArchive = async (
* is intentionally absent: the suffix also names HTTP Archive JSON, so a
* Harmony shared archive is only established from verified ZIP bytes. The
* HarmonyOS App Pack suffix `.app` is also absent here: it applies only to
* root inputs (see `classifyAndHashRoot`), because a nested ZIP entry ending
* root inputs (see `classifyAndHashRootForInventory`), because a nested ZIP entry ending
* in `.app` inside another package, such as an IPA, is not an App Pack.
*/
export const zipPackageFormatForPath = (
-21
View File
@@ -268,24 +268,3 @@ function* referencedTypeCount(
/** Rows between cooperative yields while linking. */
const LINK_BATCH = 256;
/** Synchronous linking for byte-level callers. */
export const linkIl2cppBinary = (
bytes: Buffer,
metadata: Il2cppMetadataDecoded,
facts: Il2cppMetadataLinkFacts,
architecture?: string,
options: Il2cppBinaryReadOptions = {},
): Il2cppBinaryLink => {
const steps = linkIl2cppBinarySteps(
bytes,
metadata,
facts,
architecture,
options,
);
for (;;) {
const step = steps.next();
if (step.done) return step.value;
}
};
-20
View File
@@ -969,23 +969,3 @@ function* readMetadataCoreSteps(
link: tables.linkFacts,
};
}
const runSteps = <T>(steps: Generator<void, T>): T => {
for (;;) {
const step = steps.next();
if (step.done) return step.value;
}
};
/** Synchronous consumption of the same decoder for byte-level callers. */
export const readIl2cppMetadata = (
bytes: Buffer,
options: Il2cppMetadataReadOptions = {},
): Il2cppMetadataDecoded => runSteps(readIl2cppMetadataSteps(bytes, options));
/** Synchronous decoding that also retains the typed row facts used for native linking. */
export const readIl2cppMetadataForLinking = (
bytes: Buffer,
options: Il2cppMetadataReadOptions = {},
): Il2cppMetadataForLinking =>
runSteps(readIl2cppMetadataForLinkingSteps(bytes, options));
+4 -4
View File
@@ -1,7 +1,7 @@
import {
LOAD_CONFIG_KNOWN_STRUCT_BYTES,
loadConfigFieldLayout,
} from "../../domain/native/peLoadConfig.js";
import { loadConfigFieldLayout } from "../../domain/native/peLoadConfig.js";
/** Authored fixture directory sizes through GuardMemcpyFunctionPointer. */
const LOAD_CONFIG_KNOWN_STRUCT_BYTES = { 32: 192, 64: 320 } as const;
/** File and RVA layout shared by the synthetic PE32 and PE32+ images. */
export const PE_LOAD_CONFIG_FIXTURE = {
+2 -4
View File
@@ -13,10 +13,8 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";
import { InvestigationRecords } from "../application/investigation/InvestigationRecords.js";
import {
EvidenceMcpServer,
forgetPendingMcpRequest,
} from "./EvidenceMcpServer.js";
import { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import { forgetPendingMcpRequest } from "./EvidenceMcpServerCore.js";
import { createToolResultDelivery } from "./toolResult.js";
const firstText = (result: {
+1 -4
View File
@@ -13,10 +13,7 @@ import {
type CompactInputSchemaPresentation,
} from "./EvidenceMcpServerCore.js";
export {
forgetPendingMcpRequest,
type CompactInputSchemaPresentation,
} from "./EvidenceMcpServerCore.js";
export type { CompactInputSchemaPresentation } from "./EvidenceMcpServerCore.js";
/** Canonical composition retains the constructor's existing identity defaults. */
export class EvidenceMcpServer extends EvidenceMcpServerCore {
+89 -5
View File
@@ -3,11 +3,95 @@ import { describe, expect, it } from "vitest";
import { createCli } from "../../../src/cli.js";
import { createCliInventory } from "../../../scripts/lib/catalog-cli.mjs";
import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js";
import {
CLI_COMMAND_NAMES,
CLI_COMMAND_TOOL_ALIASES,
MCP_TOOLS_WITHOUT_DEDICATED_CLI,
} from "../../../src/cliCommandNames.js";
import { CLI_COMMAND_NAMES } from "../../../src/cliCommandNames.js";
/** Canonical MCP operations reached by CLI commands with different names. */
const CLI_COMMAND_TOOL_ALIASES = Object.freeze({
"environment-build": ["export_environment_bundle"],
analyze: ["binary_overview"],
inspect: ["binary_overview"],
decompile: ["procedure_pseudo_code"],
function: ["analyze_function"],
instructions: ["read_function_instructions"],
trace: ["trace_feature"],
search: ["search_strings", "search_procedures"],
"capture-process": ["capture_process_scenario"],
"compare-bundles": ["compare_bundles"],
"list-objdump-architectures": ["inspect_with_objdump"],
"debug-with-gdb": ["gdb_console"],
"debug-with-rizin": ["rizin_debug_command"],
});
/** MCP operations without a dedicated CLI command or direct command alias. */
const MCP_TOOLS_WITHOUT_DEDICATED_CLI = Object.freeze([
// MCP session control selects a resource root; CLI resource commands take paths directly.
"open_bundle_resources",
"address_name",
"comment",
"current_address",
"current_procedure",
"current_document",
"goto_address",
"inline_comment",
"list_bookmarks",
"list_documents",
"list_names",
"list_procedures",
"list_segments",
"list_strings",
"next_address",
"prev_address",
"procedure_address",
"procedure_assembly",
"procedure_callees",
"procedure_callers",
"procedure_info",
"procedure_references",
"resolve_containing_procedure",
"set_address_name",
"set_addresses_names",
"set_bookmark",
"set_comment",
"set_inline_comment",
"unset_bookmark",
"get_objc_classes",
"get_objc_protocols",
"batch_decompile",
"get_call_graph",
"analyze_swift_types",
"find_xrefs_to_name",
"trace_call_path",
"open_binary",
"close_binary",
"binary_session",
"export_evidence_bundle",
"import_evidence_bundle",
"compare_artifacts",
"compare_functions",
"find_changed_behavior",
"build_call_path",
"correlate_static_and_runtime",
"verify_reconstruction",
"list_unknowns",
"record_unknown",
"update_unknown",
"verify_unknown_resolution",
"get_evidence_bundle",
"get_navigation_context",
"inspect_address_context",
"start_gdb_session",
"gdb_session_status",
"close_gdb_session",
"start_rizin_debug_session",
"rizin_debug_session_status",
"close_rizin_debug_session",
"start_frida_session",
"load_frida_script",
"resume_frida_session",
"unload_frida_script",
"frida_session_status",
"close_frida_session",
]);
describe("CLI canonical tool coverage", () => {
it("accounts for every MCP tool on the CLI or marks it intentionally MCP-only", () => {
+4 -10
View File
@@ -5,11 +5,12 @@ import {
readUInt32,
} from "../../../src/il2cpp/Il2cppBinaryImage.js";
import { expect, it } from "vitest";
import { linkIl2cppBinarySteps } from "../../../src/il2cpp/Il2cppBinaryFile.js";
import {
drain,
linkIl2cppBinary,
linkIl2cppBinarySteps,
} from "../../../src/il2cpp/Il2cppBinaryFile.js";
import { readIl2cppMetadataForLinking } from "../../../src/il2cpp/Il2cppMetadataFile.js";
readIl2cppMetadataForLinking,
} from "../../fixtures/il2cpp/readers.js";
import {
GENERIC_TABLE_LIMIT,
linkGenericMethodsSteps,
@@ -1673,13 +1674,6 @@ it("returns no instances for an empty genericMethodTable without picking a layou
});
});
const drain = <T>(steps: Generator<void, T>, onYield = () => {}): T => {
for (;;) {
const step = steps.next();
if (step.done === true) return step.value;
onYield();
}
};
// Link a low-base image of generic tables only, recording yields and rows.
const linkLowBase = (
options: GenericRegistrationImageOptions = {},
@@ -1,5 +1,4 @@
import { expect, it, vi } from "vitest";
import { linkIl2cppBinary } from "../../../src/il2cpp/Il2cppBinaryFile.js";
import {
isMapped,
openImageSteps,
@@ -7,7 +6,11 @@ import {
readSlot,
readUInt32,
} from "../../../src/il2cpp/Il2cppBinaryImage.js";
import { readIl2cppMetadataForLinking } from "../../../src/il2cpp/Il2cppMetadataFile.js";
import {
drain,
linkIl2cppBinary,
readIl2cppMetadataForLinking,
} from "../../fixtures/il2cpp/readers.js";
import {
createElfFixture,
createIl2cppBinaryFixture,
@@ -20,12 +23,7 @@ const { metadata, link: facts } = readIl2cppMetadataForLinking(
createLinkedIl2cppMetadataFixture().bytes,
);
const link = (bytes: Buffer) => linkIl2cppBinary(bytes, metadata, facts);
const open = (bytes: Buffer) => {
const steps = openImageSteps(bytes, undefined);
let step = steps.next();
while (!step.done) step = steps.next();
return step.value;
};
const open = (bytes: Buffer) => drain(openImageSteps(bytes, undefined));
/** Replace PT_LOAD declarations, keeping the original PT_DYNAMIC and payload. */
const withLoadRanges = (
@@ -6,10 +6,12 @@ import { describe, expect, it } from "vitest";
import {
il2cppIndexWidth,
il2cppMetadataRecordBytes,
readIl2cppMetadata,
readIl2cppMetadataForLinking,
readIl2cppMetadataSteps,
} from "../../../src/il2cpp/Il2cppMetadataFile.js";
import {
readIl2cppMetadata,
readIl2cppMetadataForLinking,
} from "../../fixtures/il2cpp/readers.js";
import {
createIl2cppMetadataFixture,
FIXTURE_IMAGES,
@@ -7,10 +7,8 @@ import { expect, it, onTestFinished } from "vitest";
import { compareProcessEvidenceFiles } from "../../../src/application/process/ProcessCli.js";
import { PROCESS_PROVIDER } from "../../../src/domain/process/processEvidenceProvider.js";
import {
EMPTY_PROCESS_CAPTURE_EXAMPLE,
FINALIZED_PROCESS_CAPTURE_EXAMPLE,
} from "../../../src/domain/process/processCaptureExample.js";
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../../src/domain/process/processCaptureExample.js";
import { FINALIZED_PROCESS_CAPTURE_EXAMPLE } from "../../../src/domain/process/processCapture.fixture.js";
import {
capture,
partialSpecification,
@@ -17,7 +17,7 @@ import { parseEvidence } from "../../../src/domain/evidence.js";
import { compareProcessCaptures } from "../../../src/domain/process/processComparison.js";
import { parseProcessCapture } from "../../../src/domain/process/processCaptureParsing.js";
import { processScenarioSchema } from "../../../src/domain/process/processScenario.js";
import { FINALIZED_PROCESS_CAPTURE_EXAMPLE } from "../../../src/domain/process/processCaptureExample.js";
import { FINALIZED_PROCESS_CAPTURE_EXAMPLE } from "../../../src/domain/process/processCapture.fixture.js";
import { createServer } from "../../../src/server/createServer.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
+26 -5
View File
@@ -1,7 +1,10 @@
import { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js";
import {
ArtifactResourceScope,
type ArtifactResourceOwner,
} from "../../src/artifacts/ArtifactResourceScope.js";
import { onTestFinished } from "vitest";
import { ArtifactReaderFailure } from "../../src/artifacts/ArtifactReader.js";
import { classifyAndHashRoot as classifyAndHashRootOwned } from "../../src/artifacts/inventory/classify.js";
import { classifyAndHashRootForInventory } from "../../src/artifacts/inventory/classify.js";
import {
inventoryArtifact as inventoryArtifactOwned,
scanArtifactInventoryInScope,
@@ -32,9 +35,27 @@ export const classifyAndHashRoot = (
signal?: AbortSignal,
) =>
withArtifactResourceScope((resourceScope) =>
classifyAndHashRootOwned(path, directory, expectedMetadata, {
resourceScope,
...(signal === undefined ? {} : { signal }),
resourceScope.run(async () => {
const result = await classifyAndHashRootForInventory(
path,
directory,
expectedMetadata,
{ resourceScope, ...(signal === undefined ? {} : { signal }) },
);
const rootSource = result.rootSource;
if (rootSource !== undefined) {
const owner: ArtifactResourceOwner = {
kind: "file-handle",
handle: rootSource.owner,
resource: `root artifact descriptor for ${path}`,
};
const cleanupAttempt = await resourceScope.release(owner);
if (cleanupAttempt.kind === "failed")
throw resourceScope.cleanupFailure({ kind: "completed" }, [
cleanupAttempt,
]);
}
return { format: result.format, digest: result.digest };
}),
);
+26
View File
@@ -0,0 +1,26 @@
import { linkIl2cppBinarySteps } from "../../../src/il2cpp/Il2cppBinaryFile.js";
import {
readIl2cppMetadataForLinkingSteps,
readIl2cppMetadataSteps,
} from "../../../src/il2cpp/Il2cppMetadataFile.js";
/** Consume production reader steps synchronously, optionally observing each yield. */
export const drain = <T>(steps: Generator<void, T>, onYield = () => {}): T => {
for (;;) {
const step = steps.next();
if (step.done === true) return step.value;
onYield();
}
};
export const readIl2cppMetadata = (
...parameters: Parameters<typeof readIl2cppMetadataSteps>
) => drain(readIl2cppMetadataSteps(...parameters));
export const readIl2cppMetadataForLinking = (
...parameters: Parameters<typeof readIl2cppMetadataForLinkingSteps>
) => drain(readIl2cppMetadataForLinkingSteps(...parameters));
export const linkIl2cppBinary = (
...parameters: Parameters<typeof linkIl2cppBinarySteps>
) => drain(linkIl2cppBinarySteps(...parameters));