From a0317cf732b3addefad5ac3b234db1e044e3351d Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:16:31 +0800 Subject: [PATCH] fix(ci): align FAT fixtures and consolidate duplicated workflow helpers Declare the synthetic 4 KiB Mach-O slice alignment accurately while preserving the IDA thinning scenario. Share web cleanup aggregation and process-trace fixture facts instead of duplicating them across consumers and tests; keep the 3% duplication threshold unchanged. Validation: npm run check:ci, test discovery, and 42 focused IDA, process-trace, web workflow and cleanup boundary tests. Duplication falls from 3.01% to 2.99%. --- src/application/WebModuleTraceService.ts | 16 +---- src/application/WebSourceLocationService.ts | 15 +--- src/application/closeOwnedWebResources.ts | 15 ++++ src/domain/process/processCapture.fixture.ts | 54 +++++++++++++++ .../process/processTraceAdapters.test.ts | 69 ++----------------- src/ida/IdaWorkspaceThinning.test.ts | 12 +++- .../mcp/processTraceAdaptersParity.test.ts | 64 +---------------- 7 files changed, 87 insertions(+), 158 deletions(-) create mode 100644 src/application/closeOwnedWebResources.ts diff --git a/src/application/WebModuleTraceService.ts b/src/application/WebModuleTraceService.ts index 7913158f66..be0936e1c4 100644 --- a/src/application/WebModuleTraceService.ts +++ b/src/application/WebModuleTraceService.ts @@ -1,3 +1,4 @@ +import { closeOwnedWebResources } from "./closeOwnedWebResources.js"; import { isAbsolute } from "node:path"; import { z } from "zod"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; @@ -271,21 +272,6 @@ export class WebModuleTraceService { } } -const closeOwnedWebResources = async ( - close: readonly ((() => Promise) | undefined)[], - message: string, -): Promise => { - const results = await Promise.allSettled( - close.flatMap((owner) => - owner === undefined ? [] : [Promise.resolve().then(owner)], - ), - ); - const failures = results.flatMap((result) => - result.status === "rejected" ? [result.reason] : [], - ); - if (failures.length > 0) throw new AggregateError(failures, message); -}; - const admitContext = ( value: string, path: readonly string[], diff --git a/src/application/WebSourceLocationService.ts b/src/application/WebSourceLocationService.ts index 79b7d45fcd..d084bff5ab 100644 --- a/src/application/WebSourceLocationService.ts +++ b/src/application/WebSourceLocationService.ts @@ -1,3 +1,4 @@ +import { closeOwnedWebResources } from "./closeOwnedWebResources.js"; import { createHash } from "node:crypto"; import { isAbsolute } from "node:path"; import { z } from "zod"; @@ -181,20 +182,6 @@ export class WebSourceLocationService { } } -const closeOwnedWebResources = async ( - close: readonly ((() => Promise) | undefined)[], - message: string, -): Promise => { - const results = await Promise.allSettled( - close.flatMap((owner) => - owner === undefined ? [] : [Promise.resolve().then(owner)], - ), - ); - const failures = results.flatMap((result) => - result.status === "rejected" ? [result.reason] : [], - ); - if (failures.length > 0) throw new AggregateError(failures, message); -}; const invalidOutput = (message: string): Result => err(new AnalysisOutputError(OPERATION, message)); const invalid = ( diff --git a/src/application/closeOwnedWebResources.ts b/src/application/closeOwnedWebResources.ts new file mode 100644 index 0000000000..51b3e6f691 --- /dev/null +++ b/src/application/closeOwnedWebResources.ts @@ -0,0 +1,15 @@ +/** Attempt every web workflow cleanup owner and report all failures. */ +export const closeOwnedWebResources = async ( + close: readonly ((() => Promise) | undefined)[], + message: string, +): Promise => { + const results = await Promise.allSettled( + close.flatMap((owner) => + owner === undefined ? [] : [Promise.resolve().then(owner)], + ), + ); + const failures = results.flatMap((result) => + result.status === "rejected" ? [result.reason] : [], + ); + if (failures.length > 0) throw new AggregateError(failures, message); +}; diff --git a/src/domain/process/processCapture.fixture.ts b/src/domain/process/processCapture.fixture.ts index 701e76e2a2..a8f2e669f8 100644 --- a/src/domain/process/processCapture.fixture.ts +++ b/src/domain/process/processCapture.fixture.ts @@ -1,3 +1,4 @@ +import type { ProcessTraceSpecification } from "./processTraceSpecification.js"; import { compareProcessCaptures } from "./processComparison.js"; import { parseProcessCapture } from "./processCaptureParsing.js"; import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./processCaptureExample.js"; @@ -112,3 +113,56 @@ export const compareUnverifiedProcessCaptures = ( parseProcessCapture(right), options, ); + +/** One journal and its matching declared trace, shared by domain and adapter checks. */ +export const processTraceCaptureFixture = () => { + const capture = { + ...EMPTY_PROCESS_CAPTURE_EXAMPLE, + event_journal: [ + { capture_order: 0, collection: "filesystem_checkpoints", index: 0 }, + { capture_order: 1, collection: "lifecycle", index: 0 }, + { capture_order: 2, collection: "lifecycle", index: 1 }, + { capture_order: 3, collection: "filesystem_checkpoints", index: 1 }, + ], + } as const; + const [before, after] = capture.filesystem_checkpoints; + if (before === undefined || after === undefined) + throw new Error( + "Trace fixture requires before and after filesystem checkpoints", + ); + const traceSpecification: ProcessTraceSpecification = { + events: [ + { + id: "before", + source: "filesystem", + exact: before, + cardinality: { kind: "required" }, + }, + { + id: "exit", + source: "lifecycle", + exact: { event: "exit", ...capture.exit }, + cardinality: { kind: "required" }, + }, + { + id: "settlement", + source: "lifecycle", + exact: { event: "settlement", ...capture.settlement }, + cardinality: { kind: "required" }, + }, + { + id: "after", + source: "filesystem", + exact: after, + cardinality: { kind: "required" }, + }, + ], + language: { + kind: "finite_traces", + variants: [ + { id: "normal", trace: ["before", "exit", "settlement", "after"] }, + ], + }, + }; + return { capture, traceSpecification }; +}; diff --git a/src/domain/process/processTraceAdapters.test.ts b/src/domain/process/processTraceAdapters.test.ts index 18cdea5c6c..959ca76c8c 100644 --- a/src/domain/process/processTraceAdapters.test.ts +++ b/src/domain/process/processTraceAdapters.test.ts @@ -1,72 +1,13 @@ import { describe, expect, it } from "vitest"; -import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./processCaptureExample.js"; -import { accountFullyObservedProcessCapture } from "./processCapture.fixture.js"; +import { + processTraceCaptureFixture, + accountFullyObservedProcessCapture, +} from "./processCapture.fixture.js"; import { compareProcessCaptures } from "./processComparison.js"; import { parseProcessCapture } from "./processCaptureParsing.js"; import type { ProcessTraceSpecification } from "./processTraceSpecification.js"; -const beforeCheckpoint = { - name: "before", - at_ms: 0, - files: [], - effects: [], - truncated: false, -}; -const afterCheckpoint = { - name: "after_settlement", - at_ms: 50, - files: [], - effects: [], - truncated: false, -}; - -const capture = { - ...EMPTY_PROCESS_CAPTURE_EXAMPLE, - event_journal: [ - { capture_order: 0, collection: "filesystem_checkpoints", index: 0 }, - { capture_order: 1, collection: "lifecycle", index: 0 }, - { capture_order: 2, collection: "lifecycle", index: 1 }, - { capture_order: 3, collection: "filesystem_checkpoints", index: 1 }, - ], -} as const; - -const traceSpecification: ProcessTraceSpecification = { - events: [ - { - id: "before", - source: "filesystem", - exact: beforeCheckpoint, - cardinality: { kind: "required" }, - }, - { - id: "exit", - source: "lifecycle", - exact: { event: "exit", ...capture.exit }, - cardinality: { kind: "required" }, - }, - { - id: "settlement", - source: "lifecycle", - exact: { event: "settlement", ...capture.settlement }, - cardinality: { kind: "required" }, - }, - { - id: "after", - source: "filesystem", - exact: afterCheckpoint, - cardinality: { kind: "required" }, - }, - ], - language: { - kind: "finite_traces", - variants: [ - { - id: "normal", - trace: ["before", "exit", "settlement", "after"], - }, - ], - }, -}; +const { capture, traceSpecification } = processTraceCaptureFixture(); describe("declared trace comparison nonconformance", () => { it("does not call identical captures changed when both violate the language", () => { diff --git a/src/ida/IdaWorkspaceThinning.test.ts b/src/ida/IdaWorkspaceThinning.test.ts index 7d83f089b2..baba6e1450 100644 --- a/src/ida/IdaWorkspaceThinning.test.ts +++ b/src/ida/IdaWorkspaceThinning.test.ts @@ -40,6 +40,7 @@ interface Layout { readonly cpu: number; readonly offset: number; readonly size: number; + readonly alignmentExponent?: number; } /** @@ -57,12 +58,12 @@ const largeUniversalTarget = async ( const bytes = Buffer.alloc(total); bytes.writeUInt32BE(0xcafebabe, 0); bytes.writeUInt32BE(layout.length, 4); - layout.forEach(({ cpu, offset, size }, index) => { + layout.forEach(({ cpu, offset, size, alignmentExponent = 14 }, index) => { const entry = 8 + index * 20; bytes.writeUInt32BE(cpu, entry); bytes.writeUInt32BE(offset, entry + 8); bytes.writeUInt32BE(size, entry + 12); - bytes.writeUInt32BE(14, entry + 16); + bytes.writeUInt32BE(alignmentExponent, entry + 16); bytes.fill(0x11 * (index + 1), offset, offset + size); thinMach(0xfeedfacf, cpu).copy(bytes, offset); }); @@ -122,7 +123,12 @@ describe("IDA workspace input thinning", () => { async () => { const { bytes, target } = await largeUniversalTarget( [ - { cpu: 0x01000007, offset: 4096, size: 8480 }, + { + cpu: 0x01000007, + offset: 4096, + size: 8480, + alignmentExponent: 12, + }, { cpu: 0x0100000c, offset: 16384, size: 33464 }, { cpu: 0x00000007, offset: 65536, size: 33464 }, ], diff --git a/tests/boundary/mcp/processTraceAdaptersParity.test.ts b/tests/boundary/mcp/processTraceAdaptersParity.test.ts index cb7c1439cc..d8c9b6173c 100644 --- a/tests/boundary/mcp/processTraceAdaptersParity.test.ts +++ b/tests/boundary/mcp/processTraceAdaptersParity.test.ts @@ -9,76 +9,16 @@ import { createTestBinarySession } from "../../fixtures/binarySession.js"; import type { AnalysisClient } from "../../../src/application/AnalysisProvider.js"; import { compareProcessEvidenceFiles } from "../../../src/application/process/ProcessCli.js"; import { PROCESS_PROVIDER } from "../../../src/domain/process/processEvidenceProvider.js"; -import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../../src/domain/process/processCaptureExample.js"; +import { processTraceCaptureFixture } from "../../../src/domain/process/processCapture.fixture.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; -import type { ProcessTraceSpecification } from "../../../src/domain/process/processTraceSpecification.js"; import { createServer } from "../../../src/server/createServer.js"; import { observed as ok } from "../../fixtures/analysisExecution.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const resources: Array<{ close(): Promise }> = []; const temporaryRoots: string[] = []; -const beforeCheckpoint = { - name: "before", - at_ms: 0, - files: [], - effects: [], - truncated: false, -}; -const afterCheckpoint = { - name: "after_settlement", - at_ms: 50, - files: [], - effects: [], - truncated: false, -}; -const capture = { - ...EMPTY_PROCESS_CAPTURE_EXAMPLE, - event_journal: [ - { capture_order: 0, collection: "filesystem_checkpoints", index: 0 }, - { capture_order: 1, collection: "lifecycle", index: 0 }, - { capture_order: 2, collection: "lifecycle", index: 1 }, - { capture_order: 3, collection: "filesystem_checkpoints", index: 1 }, - ], -} as const; -const traceSpecification: ProcessTraceSpecification = { - events: [ - { - id: "before", - source: "filesystem", - exact: beforeCheckpoint, - cardinality: { kind: "required" }, - }, - { - id: "exit", - source: "lifecycle", - exact: { event: "exit", ...capture.exit }, - cardinality: { kind: "required" }, - }, - { - id: "settlement", - source: "lifecycle", - exact: { event: "settlement", ...capture.settlement }, - cardinality: { kind: "required" }, - }, - { - id: "after", - source: "filesystem", - exact: afterCheckpoint, - cardinality: { kind: "required" }, - }, - ], - language: { - kind: "finite_traces", - variants: [ - { - id: "normal", - trace: ["before", "exit", "settlement", "after"], - }, - ], - }, -}; +const { capture, traceSpecification } = processTraceCaptureFixture(); const captureEvidence = (side: "left" | "right") => createEvidence(undefined, PROCESS_PROVIDER, {