162 lines
5.3 KiB
TypeScript
162 lines
5.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
|
|
import { err, ok } from "../../domain/result.js";
|
|
import type {
|
|
JavaScriptRecoveryInput,
|
|
JavaScriptRecoveryResult,
|
|
} from "../../domain/javascript/javascriptRecovery.js";
|
|
import { createAnalysisExecution } from "../AnalysisProvider.js";
|
|
import { JavaScriptRecoveryService } from "./JavaScriptRecoveryService.js";
|
|
|
|
const artifact = { path: "/tmp/bundle.js", sha256: "a".repeat(64), bytes: 20 };
|
|
const recoveryResult = (): JavaScriptRecoveryResult => ({
|
|
source: {
|
|
...artifact,
|
|
snapshot_path: "/tmp/work/inputs/bundle.js",
|
|
published_copy: artifact,
|
|
},
|
|
engine: {
|
|
id: "fixture-recovery",
|
|
version: "1",
|
|
executable: artifact,
|
|
audited_source_revision: "pinned",
|
|
executed_source_revision: null,
|
|
},
|
|
options: { extraction_mode: "structural", rewrite_level: "standard" },
|
|
status: "complete",
|
|
detected_formats: [],
|
|
reported_safety: "normal",
|
|
reported_strategy: "structural",
|
|
reported_format: "unknown",
|
|
total: 0,
|
|
failed: 0,
|
|
modules: [],
|
|
warnings: [],
|
|
report: artifact,
|
|
provenance: artifact,
|
|
manifest: artifact,
|
|
analysis_input: null,
|
|
runtime_equivalence: "unknown",
|
|
limitations: ["Not executed"],
|
|
});
|
|
const providerIdentity = {
|
|
id: "fixture-recovery",
|
|
name: "Fixture recovery",
|
|
version: "1",
|
|
};
|
|
const input = { path: artifact.path, output_directory: "/tmp/output" };
|
|
|
|
describe("JavaScript recovery workflow", () => {
|
|
it("passes normalized requests to a typed port and returns derived Evidence", async () => {
|
|
const received: JavaScriptRecoveryInput[] = [];
|
|
const service = new JavaScriptRecoveryService({
|
|
recover: async (request) => {
|
|
received.push(request);
|
|
return ok(
|
|
createAnalysisExecution(recoveryResult(), providerIdentity, {
|
|
subject: { ...artifact, format: "javascript" },
|
|
limitations: ["Not executed"],
|
|
}),
|
|
);
|
|
},
|
|
});
|
|
const result = await service.recover(input);
|
|
if (!result.ok) throw result.error;
|
|
expect(received).toEqual([
|
|
{ ...input, extraction_mode: "structural", rewrite_level: "standard" },
|
|
]);
|
|
expect(result.value).toMatchObject({
|
|
operation: "recover_javascript_sources",
|
|
confidence: "derived",
|
|
authority: "shipped-artifact",
|
|
provider: providerIdentity,
|
|
});
|
|
});
|
|
it("preserves a missing provider reason", async () => {
|
|
const failure = new AnalysisCapabilityUnavailableError(
|
|
"fixture-recovery",
|
|
"recover_javascript_sources",
|
|
"fixture missing engine",
|
|
);
|
|
const result = await new JavaScriptRecoveryService({
|
|
recover: async () => err(failure),
|
|
}).recover(input);
|
|
expect(result).toEqual(err(failure));
|
|
});
|
|
it.each([
|
|
"invalid-result",
|
|
"missing-subject",
|
|
"subject-mismatch",
|
|
"engine-mismatch",
|
|
])("rejects %s returned across the port", async (mode) => {
|
|
const service = new JavaScriptRecoveryService({
|
|
recover: async () =>
|
|
ok(
|
|
createAnalysisExecution(
|
|
mode === "invalid-result" ? {} : recoveryResult(),
|
|
mode === "engine-mismatch"
|
|
? { ...providerIdentity, id: "another-engine" }
|
|
: providerIdentity,
|
|
mode === "missing-subject"
|
|
? {}
|
|
: {
|
|
subject: {
|
|
path:
|
|
mode === "subject-mismatch" ? "/wrong.js" : artifact.path,
|
|
sha256: artifact.sha256,
|
|
format: "javascript" as const,
|
|
},
|
|
},
|
|
),
|
|
),
|
|
});
|
|
const result = await service.recover(input);
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) throw new Error("expected invalid port output");
|
|
expect(result.error._tag).toBe("AnalysisOutputError");
|
|
});
|
|
it.each(["path", "output_directory"])(
|
|
"rejects relative %s before acquiring the recovery engine",
|
|
async (field) => {
|
|
let invoked = false;
|
|
const service = new JavaScriptRecoveryService({
|
|
recover: async () => {
|
|
invoked = true;
|
|
return err(
|
|
new AnalysisCapabilityUnavailableError(
|
|
"fixture",
|
|
"recover_javascript_sources",
|
|
"engine missing",
|
|
),
|
|
);
|
|
},
|
|
});
|
|
const result = await service.recover({
|
|
...input,
|
|
[field]: "relative.js",
|
|
});
|
|
expect(invoked).toBe(false);
|
|
if (result.ok) throw new Error("expected invalid path");
|
|
expect(result.error._tag).toBe("AnalysisInputError");
|
|
expect(result.error).toMatchObject({
|
|
issues: [{ path: [field], reason: "invalid_format" }],
|
|
});
|
|
},
|
|
);
|
|
it("rejects malformed and pre-cancelled input before acquiring the port", async () => {
|
|
const service = new JavaScriptRecoveryService({
|
|
recover: async () => {
|
|
throw new Error("port must not execute");
|
|
},
|
|
});
|
|
const invalid = await service.recover({});
|
|
if (invalid.ok) throw new Error("expected malformed input");
|
|
expect(invalid.error._tag).toBe("AnalysisInputError");
|
|
const cancelled = await service.recover(input, {
|
|
signal: AbortSignal.abort(),
|
|
});
|
|
if (cancelled.ok) throw new Error("expected cancellation");
|
|
expect(cancelled.error._tag).toBe("AnalysisCancelledError");
|
|
});
|
|
});
|