rea/tests/boundary/javascript/javascriptSourceAnalysisWor...

149 lines
5.4 KiB
TypeScript

import { createHash } from "node:crypto";
import { expect, it } from "vitest";
import { JavaScriptAnalysisWorker } from "../../../src/javascript/analysis/JavaScriptAnalysisWorker.js";
import { cleanupOwnedProcessGroup } from "../../../src/process/ProcessOwnership.js";
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js";
import type { JavaScriptArtifactFile } from "../../../src/domain/javascript/javascriptArtifactFiles.js";
const source = (path: string, value: string): JavaScriptArtifactFile => ({
path,
sha256: createHash("sha256").update(value).digest("hex"),
container_sha256: "1".repeat(64),
bytes: Buffer.byteLength(value),
inventory_artifact_id: "fixture-source",
kind: "javascript",
unpacked: false,
text: { included: true, value },
});
it("reuses one real owned analyzer while retaining distinct source identities and exported returns", async () => {
const launches: number[] = [];
const worker = new JavaScriptAnalysisWorker(
{ heapMb: 128, timeoutMs: 30_000 },
async (request) => {
const spawned = await spawnOwnedProviderProcess(request);
const pid = spawned.process.pid;
if (pid === undefined)
throw new Error("Owned analyzer has no observed PID");
launches.push(pid);
return spawned;
},
);
try {
const first = source(
"first.mjs",
"export function create() { return { observed: 7 }; }",
);
const second = source(
"second.mjs",
"export function next() { return { observed: 8 }; }",
);
for (const [index, file] of [first, second].entries()) {
const result = await worker.analyze(file, 100);
if (!result.ok) throw result.error.error;
expect(result.value.javascript.parse_status).toBe("complete");
expect(
result.value.module?.callables[0]?.returnSites[0]?.value,
).toMatchObject({
status: "object",
properties: [
{ name: "observed", value: { status: "literal", value: index + 7 } },
],
});
expect(
result.value.projection?.nodes.every(
({ identity }) => identity.artifact_sha256 === file.sha256,
),
).toBe(true);
expect(
result.value.projection?.nodes.some(
({ identity }) => identity.module_path === file.path,
),
).toBe(true);
}
expect(launches).toHaveLength(1);
} finally {
await worker.close();
}
});
it("reports an actual startup deadline and closes its owned resources before subsequent analysis", async () => {
const file = source("app.mjs", "export const observed = 7;");
const expired = new JavaScriptAnalysisWorker({ heapMb: 128, timeoutMs: 1 });
try {
const result = await expired.analyze(file, 100);
if (result.ok)
throw new Error(
"A one-millisecond fresh process deadline unexpectedly completed",
);
expect(result.error.error).toMatchObject({
_tag: "AnalysisTimeoutError",
timeoutMs: 1,
});
} finally {
await expired.close();
}
const next = new JavaScriptAnalysisWorker({ heapMb: 128, timeoutMs: 30_000 });
try {
const result = await next.analyze(file, 100);
if (!result.ok) throw result.error.error;
expect(result.value.javascript.parse_status).toBe("complete");
} finally {
await next.close();
}
});
it("preserves a failed owned cleanup retry when the next caller cancels before acquisition", async () => {
const controller = new AbortController();
let releaseAllowed = false;
let cleanupAttempts = 0;
const worker = new JavaScriptAnalysisWorker(
{ heapMb: 128, timeoutMs: 30_000 },
async (request) => {
const launched = await spawnOwnedProviderProcess(request);
return {
...launched,
async cleanup() {
cleanupAttempts += 1;
if (!releaseAllowed) {
if (cleanupAttempts > 1) controller.abort();
throw new Error("Fixture owned cleanup is unavailable");
}
return launched.cleanup === undefined
? cleanupOwnedProcessGroup(launched.ownership)
: launched.cleanup();
},
};
},
);
const next = new JavaScriptAnalysisWorker({ heapMb: 128, timeoutMs: 30_000 });
try {
const file = source("app.mjs", "export const observed = 7;");
const first = await worker.analyze(file, 100);
if (!first.ok) throw first.error.error;
const closing = worker.close();
await expect(closing).rejects.toBeInstanceOf(ProviderCleanupError);
await expect(closing).rejects.toMatchObject({
cause: { message: "Fixture owned cleanup is unavailable" },
});
const interrupted = await next.analyze(file, 100, controller.signal);
if (interrupted.ok)
throw new Error("Unreleased ownership must block subsequent analysis");
expect(controller.signal.aborted).toBe(true);
expect(projectAnalysisError(interrupted.error.error)).toMatchObject({
code: "cleanup_incomplete",
});
releaseAllowed = true;
await worker.close();
const recovered = await next.analyze(file, 100);
if (!recovered.ok) throw recovered.error.error;
expect(recovered.value.javascript.parse_status).toBe("complete");
} finally {
releaseAllowed = true;
await worker.close();
await next.close();
}
});