fix(go): retain completed artifact facts through finalization

This commit is contained in:
morluto
2026-10-24 03:19:34 +08:00
parent 782c6c6636
commit 3f519c5587
3 changed files with 105 additions and 1 deletions
+6
View File
@@ -5,6 +5,12 @@ modules, replacements and build settings are embedded in one selected local
executable or library. It reads the file without running it and requires no
Go installation, active binary target, Hopper, Ghidra or IDA.
If a stable read completes but descriptor finalization fails or receives
cancellation, the error retains the selected path, SHA-256 and byte length as
a `file-snapshot-hash` partial observation. It does not claim that Go metadata
was decoded. Failed cleanup stays incomplete until its exact owner confirms
release; cancellation keeps the caller's reason.
```bash
rea inspect-go-binary /absolute/path/to/application --json
```
+20 -1
View File
@@ -1,7 +1,11 @@
import { isSelectedRegularFileError } from "../filesystem/RegularFile.js";
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
import {
readStableArtifact,
type StableArtifactSnapshot,
} from "../artifacts/readStableArtifact.js";
import {
artifactFilePrimaryFailure,
artifactFileCompletion,
assertArtifactReadActive,
bindArtifactFileCleanup,
artifactFileCancellationCause,
@@ -38,6 +42,7 @@ import {
BinaryResourceFailure,
} from "../native/metadata/BinaryContainer.js";
import { parseGoModuleBytes, parseGoModuleText } from "./GoModuleText.js";
import { fileSnapshotPartialObservationSchema } from "../domain/fileSnapshotObservation.js";
/** Bundled format reader identity; embedded compiler versions are separate observed facts. */
export const GO_BINARY_PROVIDER_IDENTITY = {
@@ -131,6 +136,20 @@ const failureForPrimary = (cause: unknown, path: string): AnalysisError => {
const failureFor = (cause: unknown, path: string): AnalysisError => {
const primary = failureForPrimary(artifactFilePrimaryFailure(cause), path);
const completed = artifactFileCompletion(cause);
if (completed !== undefined) {
// This inspect boundary acquires exactly one StableArtifactSnapshot before
// metadata decoding. Completion describes that read, not a decoded report.
const snapshot = completed.value as StableArtifactSnapshot;
primary.retainPartialObservation(
fileSnapshotPartialObservationSchema.parse({
kind: "file-snapshot-hash",
path,
sha256: snapshot.sha256,
bytes: snapshot.bytes.length,
}),
);
}
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
return primary;
const cleanup = new ProviderCleanupError(
@@ -0,0 +1,79 @@
import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it, vi } from "vitest";
import { GoBinaryProvider } from "../../../src/go/GoBinaryProvider.js";
import { GoBinaryService } from "../../../src/application/go/GoBinaryService.js";
import { OwnedFileHandle } from "../../../src/filesystem/OwnedFileHandle.js";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import { createGoBinaryFixture } from "../../fixtures/go/image.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
it.each(["control", "close-failure", "close-cancel", "close-both"] as const)(
"retains completed Go acquisition through %s",
async (mode) => {
const root = await createTestTempDirectory("rea-go-finalization-");
const path = join(root, "selected.elf");
const bytes = createGoBinaryFixture().bytes;
await writeFile(path, bytes);
const controller = new AbortController();
const reason = { request: "cancel during Go close" };
const closeFailure = Object.assign(new Error("controlled Go close EIO"), {
code: "EIO",
});
const original = OwnedFileHandle.prototype.close;
let owner: OwnedFileHandle | undefined;
const close = vi
.spyOn(OwnedFileHandle.prototype, "close")
.mockImplementationOnce(async function (this: OwnedFileHandle) {
owner = this;
if (mode === "close-cancel" || mode === "close-both")
controller.abort(reason);
if (mode === "close-failure" || mode === "close-both")
throw closeFailure;
await original.call(this);
});
const service = new GoBinaryService(new GoBinaryProvider());
try {
const result = await service.inspect(
{ path },
{ signal: controller.signal },
);
if (mode === "control") {
if (!result.ok) throw result.error;
expect(result.value.normalized_result).toMatchObject({
build_info: { go_version: "go1.26.0" },
});
} else {
if (result.ok) throw new Error("Expected finalization failure");
const acquired = {
kind: "file-snapshot-hash",
path,
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
};
expect(result.error.partialObservation).toEqual(acquired);
expect(
projectAnalysisError(result.error).details?.partial_observation,
).toEqual(acquired);
const failedClose = mode === "close-failure" || mode === "close-both";
expect(result.error.cleanupIncomplete).toBe(failedClose);
if (failedClose) expect(owner?.handle.fd).toBeGreaterThanOrEqual(0);
if (mode !== "close-failure") {
const primary = result.error.cleanupPrimary ?? result.error;
expect(primary).toMatchObject({
_tag: "AnalysisCancelledError",
cause: reason,
});
}
close.mockRestore();
await service.close();
expect(result.error.cleanupIncomplete).toBe(false);
}
expect(owner?.handle.fd).toBe(-1);
} finally {
close.mockRestore();
await service.close();
}
},
);