Files
reverse-engineering-anything/tests/boundary/sqlite/sqliteDatabaseBusy.test.ts
T
46728ca41d test: remove per-test deadlines owned by project policy
Explicit deadlines at or below the project deadline only shortened it:
a literal 30-second value also disabled the coverage scaling, which timed
out wide-capacity regressions on main. Workloads above the built
deadline use the long-running tag. Deadlines derived from an operation
budget remain.

Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 05:27:07 +08:00

163 lines
5.9 KiB
TypeScript

import { spawn, type ChildProcess } from "node:child_process";
import { access } from "node:fs/promises";
import { expect, it, onTestFinished, vi } from "vitest";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js";
import { SqliteDatabaseProvider } from "../../../src/sqlite/SqliteDatabaseProvider.js";
import { createSqliteDatabaseFixture } from "../../fixtures/sqlite/database.js";
import {
createTestWorkspace,
removeTestWorkspace,
} from "../../support/workspace/workspaceFixture.js";
const busy = vi.hoisted(() => ({ path: "" }));
// Node opens files with full sharing, so a test process cannot hold one without read sharing
// itself. This injects the error Windows returns for that sharing violation.
vi.mock("node:fs/promises", async (importOriginal) => {
const filesystem = await importOriginal<typeof import("node:fs/promises")>();
return {
...filesystem,
open: async (...args: Parameters<typeof filesystem.open>) => {
if (busy.path !== "" && args[0] === busy.path)
throw Object.assign(
new Error(`EBUSY: resource busy or locked, open '${busy.path}'`),
{ code: "EBUSY", errno: -4082, syscall: "open", path: busy.path },
);
return filesystem.open(...args);
},
};
});
// Private roots go under the test's own workspace, so the cleanup check sees only this
// provider's root and not those of SQLite tests running in parallel.
const createProvider = (parent: string, roots: string[]) =>
new SqliteDatabaseProvider(process.env, undefined, async () => {
const root = await PrivateRuntimeRoot.create({ parent });
roots.push(root.path);
return root;
});
const expectRetryableBusy = (
error: Parameters<typeof projectAnalysisError>[0],
path: string,
) => {
const projected = projectAnalysisError(error);
expect(projected).toMatchObject({
code: "capability_unavailable",
category: "unavailable",
retryable: true,
message: expect.stringMatching(
/held open.*without read sharing.*close.*copy.*retry/i,
),
details: {
provider_id: "sqlite",
operation: "inspect_sqlite_database",
reason: expect.stringContaining(path),
},
});
expect(projected.details?.reason).toEqual(expect.stringContaining("EBUSY"));
};
const expectRootsRemoved = async (roots: string[]) => {
expect(roots).toHaveLength(1);
await expect(access(roots[0]!)).rejects.toMatchObject({ code: "ENOENT" });
};
const stopHolder = async (holder: ChildProcess) => {
// A holder that never started has no pid and emits only "error", never "exit".
if (
holder.pid === undefined ||
holder.exitCode !== null ||
holder.signalCode !== null
)
return;
const exited = new Promise<void>((resolve) =>
holder.once("exit", () => resolve()),
);
holder.kill();
await exited;
};
it("reports a selected database held by another process as retryable and unavailable", async () => {
const workspace = await createTestWorkspace("rea-sqlite-busy-");
const database = createSqliteDatabaseFixture(workspace.root);
database.close();
const roots: string[] = [];
const provider = createProvider(workspace.root, roots);
onTestFinished(async () => {
busy.path = "";
await provider.close();
await removeTestWorkspace(workspace.root);
});
busy.path = database.path;
const result = await provider.inspect({ path: database.path });
if (result.ok) throw new Error("Expected a busy database rejection");
expectRetryableBusy(result.error, database.path);
await expectRootsRemoved(roots);
});
it("names the WAL when another process holds that member of the file set", async () => {
const workspace = await createTestWorkspace("rea-sqlite-busy-wal-");
const database = createSqliteDatabaseFixture(workspace.root, true);
const roots: string[] = [];
const provider = createProvider(workspace.root, roots);
onTestFinished(async () => {
busy.path = "";
await provider.close();
database.close();
await removeTestWorkspace(workspace.root);
});
busy.path = database.walPath;
const result = await provider.inspect({ path: database.path });
if (result.ok) throw new Error("Expected a busy WAL rejection");
expectRetryableBusy(result.error, database.walPath);
await expectRootsRemoved(roots);
});
it.runIf(process.platform === "win32")(
"reports a database that another Windows process holds without sharing as retryable",
async () => {
const workspace = await createTestWorkspace("rea-sqlite-busy-native-");
const database = createSqliteDatabaseFixture(workspace.root);
database.close();
const roots: string[] = [];
const provider = createProvider(workspace.root, roots);
const literal = database.path.replaceAll("'", "''");
const holder = spawn(
"powershell.exe",
[
"-NoProfile",
"-Command",
`$f = [System.IO.File]::Open('${literal}', 'Open', 'ReadWrite', 'None'); Write-Output ready; Start-Sleep -Seconds 60; $f.Close()`,
],
{ stdio: ["ignore", "pipe", "ignore"], windowsHide: true },
);
// One ordered cleanup: the holder must release the file before the workspace is removed.
onTestFinished(async () => {
await stopHolder(holder);
await provider.close();
await removeTestWorkspace(workspace.root);
});
await new Promise<void>((resolve, reject) => {
holder.stdout?.once("data", () => resolve());
holder.once("error", (error) =>
reject(new Error("Holder did not start", { cause: error })),
);
holder.once("exit", (code) =>
reject(
new Error(`Holder exited before opening the file (${String(code)})`),
),
);
});
const result = await provider.inspect({ path: database.path });
if (result.ok) throw new Error("Expected a busy database rejection");
expectRetryableBusy(result.error, database.path);
await expectRootsRemoved(roots);
},
);