refactor(cli): share managed comparison finalization

This commit is contained in:
morluto committed 2026-10-26 13:53:43 +08:00
1 parent 634e566c09
commit 071a637a9f
2 files changed
+131 -48

No files matched your search

+23 -47
View File
@@ -4,13 +4,10 @@ import { compareManagedMemberPaths } from "../application/managed/ManagedMemberC
import { verifyManagedNativeBoundariesEvidence } from "../application/managed/ManagedNativeVerificationService.js";
import { importManagedReconstructionEvidence } from "../application/managed/ManagedReconstructionService.js";
import type { DirectAnalysis } from "../composition/directAnalysis.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { parseCliJsonInput } from "../cliJsonInput.js";
import { MANAGED_STATIC_PROVIDER } from "../domain/investigationProviders.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import type { Logger } from "pino";
@@ -19,6 +16,7 @@ import { registerProjectManagedApplicationGraph } from "./managedProjectGraphCom
import { managedMembersInputSchema } from "../contracts/managed/managedToolContracts.js";
import { localPathStringSchema } from "../domain/localPath.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
export const registerManagedCommands = (
cli: CliInstance,
@@ -135,52 +133,30 @@ const registerCompareManagedMembers = (
"Compare managed PE/CLI member inventories by exact declared type, name, and signature; names alone never form a match",
args: compareManagedMemberArgs,
run: ({ args }) =>
logCliCommand(logger, "compare-managed-members", async () => {
const resources = new ArtifactResourceScope();
const result = await compareManagedMemberPaths(
{
leftPath: args.leftPath,
rightPath: args.rightPath,
},
{},
resources,
);
try {
await resources.close();
} catch (cause: unknown) {
const cleanup = ArtifactReaderFailure.cleanupObservation(
cause,
withCommandCancellation((signal) =>
logCliCommand(logger, "compare-managed-members", async () => {
const resources = new ArtifactResourceScope();
const result = await finishAnalysisCommand(
() =>
compareManagedMemberPaths(
{
leftPath: args.leftPath,
rightPath: args.rightPath,
},
{},
resources,
),
resources,
MANAGED_STATIC_PROVIDER.id,
"compare_managed_members",
signal,
"Managed member comparison resources",
);
const failure = new ProviderCleanupError(
MANAGED_STATIC_PROVIDER.id,
cleanup.resources,
{ reason: cleanup.reason },
{
cause,
operation: "compare_managed_members",
...(result.ok ? { partialObservation: result.value } : {}),
},
);
const error = result.ok
? failure
: analysisErrorWithCleanupFailure(
result.error,
failure,
"compare_managed_members",
);
return {
error: "Managed member comparison failed",
...projectAnalysisError(error),
};
}
return result.ok
? result.value
: {
error: "Managed member comparison failed",
...projectAnalysisError(result.error),
};
}),
return "evidence_id" in result
? result
: { error: "Managed member comparison failed", ...result };
}),
),
});
};
@@ -2,7 +2,15 @@ import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { expect, it, vi } from "vitest";
import * as comparison from "../../../src/application/managed/ManagedMemberComparisonService.js";
import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js";
import { registerManagedCommands } from "../../../src/cli/managedCommands.js";
import { CLI_COMMANDS } from "../../../src/cliCommandNames.js";
import { AnalysisOutputError } from "../../../src/domain/analysisErrorCore.js";
import { err, ok } from "../../../src/domain/result.js";
import { silentLogger } from "../../../src/logger.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { managedMemberInspectionSchema } from "../../../src/domain/managed/managedArtifact.js";
@@ -184,3 +192,102 @@ cliTest(
}
},
);
const managedComparisonEvidence = async () => {
const directory = await createTestTempDirectory("rea-managed-finalization-");
const path = join(directory, "comparison.dll");
await writeFile(path, buildManagedPeFixture());
const result = await comparison.compareManagedMemberPaths({
leftPath: path,
rightPath: path,
});
if (!result.ok) throw result.error;
return result.value;
};
const registeredComparisonCommand = () => {
const commands = new Map<string, { run(input: unknown): Promise<unknown> }>();
const cli = {
command: (
name: string,
definition: { run(input: unknown): Promise<unknown> },
) => {
commands.set(name, definition);
return cli;
},
} as unknown as Parameters<typeof registerManagedCommands>[0];
registerManagedCommands(cli, silentLogger, async () => null);
return commands.get(CLI_COMMANDS.compareManagedMembers)!;
};
it.each([true, false])(
"retains the comparison outcome when final cleanup fails (success: %s)",
async (success) => {
const evidence = await managedComparisonEvidence();
const run = vi
.spyOn(comparison, "compareManagedMemberPaths")
.mockResolvedValue(
success
? ok(evidence)
: err(
new AnalysisOutputError(
"compare_managed_members",
"primary comparison failure",
{
partialObservation: evidence,
},
),
),
);
const close = vi
.spyOn(ArtifactResourceScope.prototype, "close")
.mockRejectedValue(new Error("comparison close denied"));
try {
const result = await registeredComparisonCommand().run({
args: { leftPath: "left.dll", rightPath: "right.dll" },
});
expect(result).toMatchObject({
error: "Managed member comparison failed",
code: "cleanup_incomplete",
details: {
partial_observation: { evidence_id: evidence.evidence_id },
},
});
expect(JSON.stringify(result)).toContain("comparison close denied");
if (!success)
expect(JSON.stringify(result)).toContain("primary comparison failure");
expect(close).toHaveBeenCalledOnce();
} finally {
run.mockRestore();
close.mockRestore();
}
},
);
it("retains completed comparison Evidence when interrupted during final cleanup", async () => {
const evidence = await managedComparisonEvidence();
const run = vi
.spyOn(comparison, "compareManagedMemberPaths")
.mockResolvedValue(ok(evidence));
const close = vi
.spyOn(ArtifactResourceScope.prototype, "close")
.mockImplementation(async () => {
process.emit("SIGINT");
});
const previousExitCode = process.exitCode;
try {
const result = await registeredComparisonCommand().run({
args: { leftPath: "left.dll", rightPath: "right.dll" },
});
expect(result).toMatchObject({
error: "Managed member comparison failed",
code: "cancelled",
details: { partial_observation: { evidence_id: evidence.evidence_id } },
});
expect(process.exitCode).toBe(130);
} finally {
process.exitCode = previousExitCode;
run.mockRestore();
close.mockRestore();
}
});