mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
295 lines
9.4 KiB
TypeScript
295 lines
9.4 KiB
TypeScript
import {
|
|
lstat,
|
|
mkdir,
|
|
mkdtemp,
|
|
open,
|
|
readdir,
|
|
rm,
|
|
writeFile,
|
|
} from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import { expect, it } from "vitest";
|
|
|
|
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
|
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
|
import type { StableArtifactFileSystem } from "../artifacts/readStableArtifact.js";
|
|
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
|
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
|
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
|
import type { AnalysisPartialObservation } from "../domain/analysisErrorBase.js";
|
|
import { captureSqliteDatabaseSnapshot } from "./SqliteDatabaseSnapshot.js";
|
|
import { SQLITE_DATABASE_LIMITS } from "./SqliteDatabaseLimits.js";
|
|
import { sqliteDatabaseFailure } from "./SqliteDatabaseFailures.js";
|
|
|
|
it("tags an exact caller reason at snapshot admission", async () => {
|
|
const workspace = await mkdtemp(join(tmpdir(), "rea-sqlite-snapshot-entry-"));
|
|
const reason = { requested: "cancel at snapshot admission" };
|
|
const controller = new AbortController();
|
|
controller.abort(reason);
|
|
const resourceScope = new ArtifactResourceScope();
|
|
|
|
try {
|
|
const failure = await captureSqliteDatabaseSnapshot(
|
|
join(workspace, "selected.db"),
|
|
join(workspace, "private"),
|
|
resourceScope,
|
|
controller.signal,
|
|
).then(
|
|
() => {
|
|
throw new Error("Expected cancellation at snapshot admission");
|
|
},
|
|
(cause: unknown) => cause,
|
|
);
|
|
expect(failure).toBeInstanceOf(ArtifactReaderFailure);
|
|
expect(failure).toMatchObject({
|
|
reason: "cancelled",
|
|
cancellation: { reason },
|
|
});
|
|
const projected = sqliteDatabaseFailure(
|
|
failure,
|
|
join(workspace, "selected.db"),
|
|
"artifact-read",
|
|
controller.signal,
|
|
);
|
|
expect(projected).toBeInstanceOf(AnalysisCancelledError);
|
|
expect(projected.cause).toBe(reason);
|
|
} finally {
|
|
await resourceScope.close();
|
|
await rm(workspace, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("keeps the exact caller reason when cancellation lands at final snapshot verification", async () => {
|
|
const workspace = await mkdtemp(
|
|
join(tmpdir(), "rea-sqlite-snapshot-cancel-"),
|
|
);
|
|
const databasePath = join(workspace, "selected.db");
|
|
const privateRoot = join(workspace, "private");
|
|
const journalPath = `${databasePath}-journal`;
|
|
const reason = { requested: "cancel after snapshot copy" };
|
|
const controller = new AbortController();
|
|
await writeFile(databasePath, "tiny fixture");
|
|
await mkdir(privateRoot);
|
|
let journalStats = 0;
|
|
const fileSystem: StableArtifactFileSystem = {
|
|
lstat: async (path) => {
|
|
if (path === journalPath) {
|
|
journalStats += 1;
|
|
if (journalStats === 2) controller.abort(reason);
|
|
throw Object.assign(new Error("journal absent"), { code: "ENOENT" });
|
|
}
|
|
return lstat(path, { bigint: true });
|
|
},
|
|
open: async (path, flags) => open(path, flags),
|
|
};
|
|
const resourceScope = new ArtifactResourceScope();
|
|
|
|
try {
|
|
const failure = await captureSqliteDatabaseSnapshot(
|
|
databasePath,
|
|
privateRoot,
|
|
resourceScope,
|
|
controller.signal,
|
|
fileSystem,
|
|
).then(
|
|
() => {
|
|
throw new Error("Expected cancellation during final verification");
|
|
},
|
|
(cause: unknown) => cause,
|
|
);
|
|
const projected = sqliteDatabaseFailure(
|
|
failure,
|
|
databasePath,
|
|
"artifact-read",
|
|
controller.signal,
|
|
);
|
|
expect(journalStats).toBe(2);
|
|
expect(projected).toBeInstanceOf(AnalysisCancelledError);
|
|
expect(projected.cause).toBe(reason);
|
|
} finally {
|
|
await resourceScope.close();
|
|
await rm(workspace, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("reports filesystem sizes above the safe-integer range exactly before opening files", async () => {
|
|
const databaseBytes = BigInt(Number.MAX_SAFE_INTEGER) + 1n;
|
|
const walBytes = 17n;
|
|
const workspace = await mkdtemp(join(tmpdir(), "rea-sqlite-budget-"));
|
|
const databasePath = join(workspace, "selected.db");
|
|
const walPath = `${databasePath}-wal`;
|
|
const journalPath = `${databasePath}-journal`;
|
|
const privateRoot = join(workspace, "private");
|
|
await writeFile(databasePath, "tiny fixture");
|
|
const stat = await lstat(databasePath, { bigint: true });
|
|
const open = async (): Promise<never> => {
|
|
throw new Error("An over-budget source must be refused before open");
|
|
};
|
|
const fileSystem: StableArtifactFileSystem = {
|
|
lstat: async (path) => {
|
|
if (path === journalPath)
|
|
throw Object.assign(new Error("journal absent"), { code: "ENOENT" });
|
|
const size =
|
|
path === databasePath
|
|
? databaseBytes
|
|
: path === walPath
|
|
? walBytes
|
|
: stat.size;
|
|
return Object.assign(
|
|
Object.create(Object.getPrototypeOf(stat)) as typeof stat,
|
|
stat,
|
|
{ size },
|
|
);
|
|
},
|
|
open,
|
|
};
|
|
const resourceScope = new ArtifactResourceScope();
|
|
|
|
try {
|
|
const failure = await captureSqliteDatabaseSnapshot(
|
|
databasePath,
|
|
privateRoot,
|
|
resourceScope,
|
|
undefined,
|
|
fileSystem,
|
|
).then(
|
|
() => {
|
|
throw new Error("Expected the oversized snapshot to be refused");
|
|
},
|
|
(cause: unknown) => cause,
|
|
);
|
|
const projected = projectAnalysisError(
|
|
sqliteDatabaseFailure(failure, databasePath, "artifact-read"),
|
|
);
|
|
expect(failure).toMatchObject({
|
|
_tag: "AnalysisResourceConstraintError",
|
|
operation: "inspect_sqlite_database",
|
|
resource: "file-size",
|
|
reportedLimits: {
|
|
sqlite_snapshot_input_bytes: SQLITE_DATABASE_LIMITS.inputBytes,
|
|
observed_sqlite_snapshot_input_bytes: (
|
|
databaseBytes + walBytes
|
|
).toString(),
|
|
},
|
|
remediationAction: expect.stringContaining(
|
|
"smaller offline SQLite snapshot",
|
|
),
|
|
});
|
|
expect(projected).toMatchObject({
|
|
code: "resource_constraint",
|
|
category: "resource_constraint",
|
|
retryable: false,
|
|
details: {
|
|
reported_limits: {
|
|
sqlite_snapshot_input_bytes: SQLITE_DATABASE_LIMITS.inputBytes,
|
|
observed_sqlite_snapshot_input_bytes: (
|
|
databaseBytes + walBytes
|
|
).toString(),
|
|
},
|
|
},
|
|
remediation: {
|
|
action: expect.stringContaining("supported export workflow"),
|
|
},
|
|
});
|
|
await expect(readdir(privateRoot)).rejects.toMatchObject({
|
|
code: "ENOENT",
|
|
});
|
|
} finally {
|
|
await resourceScope.close();
|
|
await rm(workspace, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
{ label: "incomplete cleanup", retainCleanup: true },
|
|
{ label: "partial observation without cleanup", retainCleanup: false },
|
|
])(
|
|
"preserves $label when a late read-limit failure becomes changed evidence",
|
|
async ({ retainCleanup }) => {
|
|
const workspace = await mkdtemp(join(tmpdir(), "rea-sqlite-read-cleanup-"));
|
|
const databasePath = join(workspace, "selected.db");
|
|
await writeFile(databasePath, "tiny fixture");
|
|
const stat = await lstat(databasePath, { bigint: true });
|
|
const cleanup = {
|
|
reason: "fixture file-handle close failed",
|
|
resources: [databasePath],
|
|
};
|
|
const partialObservation = {
|
|
operation: "address_to_file_offset",
|
|
address: "0x1000",
|
|
provider_file_offset: 16,
|
|
provider_source_path: null,
|
|
provider_image_header_hex: null,
|
|
original_file_offset: {
|
|
available: false,
|
|
reason: "fixture observation",
|
|
},
|
|
} satisfies AnalysisPartialObservation;
|
|
const details = {
|
|
logicalPath: "selected.db",
|
|
declaredSha256: null,
|
|
calculatedSha256: null,
|
|
unpacked: false,
|
|
};
|
|
const primary = new ArtifactReaderFailure(
|
|
"limit",
|
|
"Selected artifact grew beyond its byte limit",
|
|
{ partialObservation },
|
|
details,
|
|
);
|
|
const limitFailure = retainCleanup
|
|
? ArtifactReaderFailure.withCleanup(primary, cleanup)
|
|
: primary;
|
|
const fileSystem: StableArtifactFileSystem = {
|
|
lstat: async (path) => {
|
|
if (path === `${databasePath}-journal`)
|
|
throw Object.assign(new Error("journal absent"), { code: "ENOENT" });
|
|
return stat;
|
|
},
|
|
open: async () => {
|
|
throw limitFailure;
|
|
},
|
|
};
|
|
const resourceScope = new ArtifactResourceScope();
|
|
|
|
try {
|
|
const failure = await captureSqliteDatabaseSnapshot(
|
|
databasePath,
|
|
join(workspace, "private"),
|
|
resourceScope,
|
|
undefined,
|
|
fileSystem,
|
|
).then(
|
|
() => {
|
|
throw new Error("Expected the changed snapshot to be rejected");
|
|
},
|
|
(cause: unknown) => cause,
|
|
);
|
|
expect(failure).toMatchObject({ reason: "integrity" });
|
|
if (retainCleanup)
|
|
expect(failure).toMatchObject({
|
|
cleanup,
|
|
cause: expect.any(ArtifactReaderFailure),
|
|
});
|
|
expect(failure).toMatchObject({ partialObservation, details });
|
|
const projectedFailure = sqliteDatabaseFailure(
|
|
failure,
|
|
databasePath,
|
|
"artifact-read",
|
|
);
|
|
if (retainCleanup) {
|
|
expect(projectedFailure).toBeInstanceOf(ProviderCleanupError);
|
|
expect(projectedFailure).toMatchObject({
|
|
cleanupIncomplete: true,
|
|
cleanupResources: cleanup.resources,
|
|
});
|
|
}
|
|
} finally {
|
|
await resourceScope.close();
|
|
await rm(workspace, { force: true, recursive: true });
|
|
}
|
|
},
|
|
);
|