import { assert, it } from "@effect/vitest"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; import * as Effect from "effect/Effect"; import * as Schema from "effect/Schema"; import * as SqlClient from "effect/unstable/sql/SqlClient"; import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError"; import { PersistenceDecodeError, PersistenceSqlError, toPersistenceSqlError } from "./Errors.ts"; const decodeRuntimePayload = Schema.decodeUnknownEffect( Schema.Struct({ runtimePayload: Schema.Struct({ attempt: Schema.Number, }), }), ); it("keeps SQL operation context without a tautological detail", () => { const cause = new Error("database unavailable"); const error = new PersistenceSqlError({ operation: "AuthSessionRepository.list:query", cause, }); assert.equal(error.operation, "AuthSessionRepository.list:query"); assert.equal(error.detail, undefined); assert.equal(error.cause, cause); assert.equal(error.message, "SQL error in AuthSessionRepository.list:query"); }); it.effect("names a real SQLite condition without copying query data", () => Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; const payload = "sql-private-sentinel"; yield* sql`CREATE TABLE error_test (private_column TEXT PRIMARY KEY)`; yield* sql`INSERT INTO error_test VALUES (${payload})`; const cause = yield* Effect.flip(sql`INSERT INTO error_test VALUES (${payload})`); const error = toPersistenceSqlError("OrchestrationCommandReceiptRepository.upsert:query")( cause, ); assert.equal(error.detail, "SQLITE(1555) constraint failed"); assert.equal(error.cause, cause); assert.notInclude(error.message, payload); assert.notInclude(error.message, "private_column"); }).pipe(Effect.provide(NodeSqliteClient.layerMemory())), ); it("reads the condition through a wrapping driver error", () => { const driver = Object.assign(new Error("locked"), { errcode: 5, errstr: "database is locked" }); const error = toPersistenceSqlError("AuthSessionRepository.list:query")( new Error("Failed to prepare statement", { cause: driver }), ); assert.equal(error.detail, "SQLITE(5) database is locked"); }); it.each([{ errno: 1555, code: "SQLITE_CONSTRAINT_PRIMARYKEY" }, { errno: 1 }])( "names Bun SQLite condition $errno through the SQL error wrapper", (condition) => { const driver = Object.assign(new Error("bun-sql-private-sentinel"), { name: "SQLiteError", ...condition, }); const cause = new SqlError({ reason: classifySqliteError(driver) }); const error = toPersistenceSqlError("AuthSessionRepository.create:query")(cause); assert.equal( error.message, `SQL error in AuthSessionRepository.create:query: SQLITE(${condition.errno})`, ); assert.equal(error.cause, cause); }, ); it.each([ new Error("unhelpful"), Object.assign(new Error("file not found"), { errno: -2, code: "ENOENT" }), ])("omits a detail for a cause it cannot categorize (%#)", (cause) => { const error = toPersistenceSqlError("AuthSessionRepository.list:query")(cause); assert.equal(error.detail, undefined); assert.equal(error.message, "SQL error in AuthSessionRepository.list:query"); }); it.effect("summarizes a schema cause by issue tag instead of by rejected value", () => Effect.gen(function* () { const rejectedPayload = "sql-mapper-secret-sentinel"; const cause = yield* Effect.flip( decodeRuntimePayload({ runtimePayload: { attempt: rejectedPayload } }), ); const error = toPersistenceSqlError("ProviderSessionRuntimeRepository.list:query")(cause); assert.ok(error.detail !== undefined); assert.ok(!error.message.includes(rejectedPayload)); }), ); it.effect("maps schema errors without copying rejected payloads into diagnostics", () => Effect.gen(function* () { const rejectedPayload = "runtime-payload-secret-sentinel"; const cause = yield* Effect.flip( decodeRuntimePayload({ runtimePayload: { attempt: rejectedPayload, }, }), ); const error = PersistenceDecodeError.fromSchemaError( "ProviderSessionRuntimeRepository.list:decodeRows", cause, ); assert.equal(error.operation, "ProviderSessionRuntimeRepository.list:decodeRows"); assert.equal(error.cause, cause); assert.notInclude(error.issue, rejectedPayload); assert.notInclude(error.message, rejectedPayload); assert.include(error.issue, "InvalidType"); }), );