test(sqlite): preserve worker failures through cleanup retry

This commit is contained in:
morluto committed 2026-10-24 12:34:10 +08:00
1 parent f774dc420b
commit 21a20306b4
2 files changed
+81

No files matched your search

+5
View File
@@ -184,6 +184,11 @@ retains the completed Evidence and the resource owner for cleanup retry. MCP
records that Evidence before delivering the error, which carries its retained records that Evidence before delivering the error, which carries its retained
evidence ID for later retrieval or export. evidence ID for later retrieval or export.
If a worker fails and cleanup remains incomplete, the error retains the original
cancellation, timeout, diagnostic overflow or memory failure. Successful cleanup
retry restores that failure's classification; it does not make the inspection
successful.
Changed input identities, special files, malformed databases, unsupported Changed input identities, special files, malformed databases, unsupported
formats and resource failures are reported as errors with the selected path and formats and resource failures are reported as errors with the selected path and
meaningful reason. Worker resource refusals expose the caller-selected file in meaningful reason. Worker resource refusals expose the caller-selected file in
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
import { expect, it as nativeTest, onTestFinished } from "vitest"; import { expect, it as nativeTest, onTestFinished } from "vitest";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js"; import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
import { SQLITE_DATABASE_LIMITS } from "../../../src/sqlite/SqliteDatabaseLimits.js";
import { SqliteDatabaseProvider } from "../../../src/sqlite/SqliteDatabaseProvider.js"; import { SqliteDatabaseProvider } from "../../../src/sqlite/SqliteDatabaseProvider.js";
import { inspectSqliteDatabaseSnapshot } from "../../../src/sqlite/SqliteDatabaseInspection.js"; import { inspectSqliteDatabaseSnapshot } from "../../../src/sqlite/SqliteDatabaseInspection.js";
import { cleanupOwnedProcessGroup } from "../../../src/process/ProcessOwnership.js"; import { cleanupOwnedProcessGroup } from "../../../src/process/ProcessOwnership.js";
@@ -1269,3 +1270,78 @@ nativeTest(
await expectPrivateRootRemoved(ownedRoot); await expectPrivateRootRemoved(ownedRoot);
}, },
); );
nativeTest.each([
["cancelled", "cancelled"],
["output-limit", "unreadable_output"],
["heap-diagnostic", "resource_constraint"],
] as const)(
"preserves the typed %s worker failure through retained cleanup retry",
async (scenario, code) => {
const { database } = await createFixture();
const controller = new AbortController();
const reason = { requested: "cancel retained SQLite worker" };
let cleanupAllowed = false;
let ownedRoot = "";
let launches = 0;
const provider = new SqliteDatabaseProvider({}, async (spawn) => {
launches += 1;
ownedRoot = spawn.cwd ?? "";
const script =
scenario === "heap-diagnostic"
? "process.stderr.write('JavaScript heap out of memory'); process.exitCode = 3"
: scenario === "output-limit"
? `process.stdout.write('x'.repeat(${String(SQLITE_DATABASE_LIMITS.diagnosticBytes + 1)})); setInterval(() => {}, 1000)`
: "process.stdout.write('ready\\n'); setInterval(() => {}, 1000)";
const launched = await spawnOwnedProviderProcess({
...spawn,
arguments: ["-e", script],
});
const cleanupOwned =
launched.cleanup ??
(() => cleanupOwnedProcessGroup(launched.ownership));
if (scenario === "cancelled") {
await waitForProviderProcessReady(launched.process);
setImmediate(() => controller.abort(reason));
}
return {
...launched,
cleanup: async () => {
const cleaned = await cleanupOwned();
if (!cleanupAllowed)
throw new Error("controlled SQLite worker cleanup refusal");
return cleaned;
},
};
});
onTestFinished(async () => {
cleanupAllowed = true;
await provider.close();
});
const result = await provider.inspect(
{ path: database.path },
{ signal: controller.signal },
);
if (result.ok) throw new Error("Expected retained worker cleanup");
expect(projectAnalysisError(result.error).code).toBe("cleanup_incomplete");
const primary = result.error.cleanupPrimary;
if (primary === undefined) throw new Error("Missing typed worker failure");
expect(projectAnalysisError(primary).code).toBe(code);
if (scenario === "cancelled")
expect(result.error.cleanupPrimary?.cause).toBe(reason);
expect(await provider.inspect({ path: database.path })).toMatchObject({
ok: false,
error: { cleanupIncomplete: true },
});
expect(launches).toBe(1);
await access(ownedRoot);
cleanupAllowed = true;
await provider.close();
expect(result.error.cleanupIncomplete).toBe(false);
expect(result.error.cleanupResources).toEqual([]);
expect(projectAnalysisError(result.error).code).toBe(code);
if (scenario === "cancelled")
expect(result.error.cleanupPrimary?.cause).toBe(reason);
await expectPrivateRootRemoved(ownedRoot);
},
);