diff --git a/src/sqlite-state.js b/src/sqlite-state.js index 513aaf7..bda8ab4 100644 --- a/src/sqlite-state.js +++ b/src/sqlite-state.js @@ -197,8 +197,14 @@ export function configureSqliteWriteDurability(db) { } function isSqliteBusyError(error) { - const message = `${error?.code ?? ""} ${error?.message ?? ""}`.toLowerCase(); - return message.includes("database is locked") || message.includes("sqlite_busy") || message.includes("busy"); + const code = String(error?.code ?? "").toUpperCase(); + const message = String(error?.message ?? "").toLowerCase(); + return code === "SQLITE_BUSY" + || code.startsWith("SQLITE_BUSY_") + || code === "SQLITE_LOCKED" + || code.startsWith("SQLITE_LOCKED_") + || message.includes("database is locked") + || message.includes("database table is locked"); } function isSqliteMalformedError(error) { diff --git a/test/sqlite-error-classification.test.js b/test/sqlite-error-classification.test.js new file mode 100644 index 0000000..7bb062b --- /dev/null +++ b/test/sqlite-error-classification.test.js @@ -0,0 +1,33 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { wrapSqliteBusyError } from "../src/sqlite-state.js"; + +test("filesystem EBUSY is not classified as SQLite busy", () => { + const error = Object.assign( + new Error("EBUSY: resource busy or locked, open 'transaction-journal.jsonl'"), + { code: "EBUSY" } + ); + + assert.equal(wrapSqliteBusyError(error, "update session provider metadata"), error); +}); + +test("explicit SQLite busy and locked codes retain the existing guidance", () => { + for (const code of ["SQLITE_BUSY", "SQLITE_BUSY_SNAPSHOT", "SQLITE_LOCKED", "SQLITE_LOCKED_SHAREDCACHE"]) { + const error = Object.assign(new Error("SQLite write failed"), { code }); + const wrapped = wrapSqliteBusyError(error, "update session provider metadata"); + + assert.notEqual(wrapped, error); + assert.match(wrapped.message, /state_5\.sqlite is currently in use/); + } +}); + +test("SQLite lock messages retain the existing guidance when a driver omits the SQLite code", () => { + for (const message of ["database is locked", "database table is locked: threads"]) { + const error = new Error(message); + const wrapped = wrapSqliteBusyError(error, "update session provider metadata"); + + assert.notEqual(wrapped, error); + assert.match(wrapped.message, /state_5\.sqlite is currently in use/); + } +});