diff --git a/src/sqlite/SqliteDatabaseProvider.ts b/src/sqlite/SqliteDatabaseProvider.ts index 612d97afef..a4ed9aeb1b 100644 --- a/src/sqlite/SqliteDatabaseProvider.ts +++ b/src/sqlite/SqliteDatabaseProvider.ts @@ -13,7 +13,6 @@ import type { } from "../domain/analysisErrorBase.js"; import { AnalysisCancelledError, - AnalysisInputError, AnalysisOutputError, AnalysisResourceConstraintError, } from "../domain/analysisErrorCore.js"; @@ -30,7 +29,7 @@ import { DEFAULT_SQLITE_WORKER_TIMEOUT_MS, sqliteInspectionParameters, } from "../domain/sqlite/sqliteDatabase.js"; -import { isAbsolute } from "node:path"; +import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"; import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js"; import { OwnedCommandFailure, @@ -533,14 +532,8 @@ const completedSqliteEvidence = ( const validateSqliteInput = (input: InspectSqliteDatabaseInput): void => { const parsed = inspectSqliteDatabaseInputSchema.safeParse(input); - if (!parsed.success || !isAbsolute(input.path)) - throw new AnalysisInputError(operation, undefined, [ - { - path: ["path"], - reason: "invalid_format", - message: parsed.success - ? "path must be an absolute local filesystem path" - : parsed.error.message, - }, - ]); + if (!parsed.success) + throw analysisInputErrorFromIssues(operation, parsed.error.issues, input, { + cause: parsed.error, + }); }; diff --git a/tests/boundary/sqlite/sqliteDatabaseProvider.test.ts b/tests/boundary/sqlite/sqliteDatabaseProvider.test.ts index cd561c3c02..c2140ba382 100644 --- a/tests/boundary/sqlite/sqliteDatabaseProvider.test.ts +++ b/tests/boundary/sqlite/sqliteDatabaseProvider.test.ts @@ -4,7 +4,8 @@ import { access, lstat, readFile, symlink, writeFile } from "node:fs/promises"; import { DatabaseSync } from "node:sqlite"; import { promisify } from "node:util"; import { fileURLToPath } from "node:url"; -import { expect, it as nativeTest, onTestFinished } from "vitest"; +import { expect, it as nativeTest, onTestFinished, vi } from "vitest"; +import { ZodError } from "zod"; import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js"; import { SQLITE_DATABASE_LIMITS } from "../../../src/sqlite/SqliteDatabaseLimits.js"; @@ -54,6 +55,32 @@ const createFixture = async (wal = false) => { const expectPrivateRootRemoved = (path: string) => expect(access(path)).rejects.toMatchObject({ code: "ENOENT" }); +nativeTest.each([ + { input: { path: "relative.db" }, field: "path" }, + { + input: { path: "/selected.db", table: "records", row_limit: 0 }, + field: "row_limit", + }, + { input: { path: "/selected.db", columns: ["id"] }, field: "columns" }, +])( + "attributes invalid provider input to its argument before allocating resources: %j", + async ({ input, field }) => { + const createRuntime = vi.fn(async () => { + throw new Error("Invalid input must not allocate a SQLite runtime"); + }); + const provider = new SqliteDatabaseProvider({}, undefined, createRuntime); + onTestFinished(() => provider.close()); + const result = await provider.inspect({ ...input }); + if (result.ok) throw new Error("Expected an input rejection"); + expect(result.error.cause).toBeInstanceOf(ZodError); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "invalid_request", + details: { issues: [{ path: [field] }] }, + }); + expect(createRuntime).not.toHaveBeenCalled(); + }, +); + it("enforces a selected worker budget and completes the corrected same-database retry", async () => { const { database } = await createFixture(); const original = await readFile(database.path);