1import { assert, it } from "@effect/vitest";
2import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
3import * as Effect from "effect/Effect";
4import * as Schema from "effect/Schema";
5import * as SqlClient from "effect/sql/SqlClient";
6
7import { PersistenceDecodeError, PersistenceSqlError, toPersistenceSqlError } from "./Errors.ts";
8
9const decodeRuntimePayload = Schema.decodeUnknownEffect(
10 Schema.Struct({
11 runtimePayload: Schema.Struct({
12 attempt: Schema.Number,
13 }),
14 }),
15);
16
17it("keeps SQL operation context without a tautological detail", () => {
18 const cause = new Error("database unavailable");
19 const error = new PersistenceSqlError({
20 operation: "AuthSessionRepository.list:query",
21 cause,
22 });
23
24 assert.equal(error.operation, "AuthSessionRepository.list:query");
25 assert.equal(error.detail, undefined);
26 assert.equal(error.cause, cause);
27 assert.equal(error.message, "SQL error in AuthSessionRepository.list:query");
28});
29
30it.effect("names a real SQLite condition without copying query data", () =>
31 Effect.gen(function* () {
32 const sql = yield* SqlClient.SqlClient;
33 const payload = "sql-private-sentinel";
34 yield* sql`CREATE TABLE error_test (private_column TEXT PRIMARY KEY)`;
35 yield* sql`INSERT INTO error_test VALUES (${payload})`;
36 const cause = yield* Effect.flip(sql`INSERT INTO error_test VALUES (${payload})`);
37 const error = toPersistenceSqlError("OrchestrationCommandReceiptRepository.upsert:query")(
38 cause,
39 );
40
41 assert.equal(error.detail, "SQLITE(1555) constraint failed");
42 assert.equal(error.cause, cause);
43 assert.notInclude(error.message, payload);
44 assert.notInclude(error.message, "private_column");
45 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: ":memory:" }))),
46);
47
48it("reads the condition through a wrapping driver error", () => {
49 const driver = Object.assign(new Error("locked"), { errcode: 5, errstr: "database is locked" });
50 const error = toPersistenceSqlError("AuthSessionRepository.list:query")(
51 new Error("Failed to prepare statement", { cause: driver }),
52 );
53
54 assert.equal(error.detail, "SQLITE(5) database is locked");
55});
56
57it.each([
58 new Error("unhelpful"),
59 Object.assign(new Error("file not found"), { errno: -2, code: "ENOENT" }),
60])("omits a detail for a cause it cannot categorize (%#)", (cause) => {
61 const error = toPersistenceSqlError("AuthSessionRepository.list:query")(cause);
62
63 assert.equal(error.detail, undefined);
64 assert.equal(error.message, "SQL error in AuthSessionRepository.list:query");
65});
66
67it.effect("summarizes a schema cause by issue tag instead of by rejected value", () =>
68 Effect.gen(function* () {
69 const rejectedPayload = "sql-mapper-secret-sentinel";
70 const cause = yield* Effect.flip(
71 decodeRuntimePayload({ runtimePayload: { attempt: rejectedPayload } }),
72 );
73 const error = toPersistenceSqlError("ProviderSessionRuntimeRepository.list:query")(cause);
74
75 assert.ok(error.detail !== undefined);
76 assert.ok(!error.message.includes(rejectedPayload));
77 }),
78);
79
80it.effect("maps schema errors without copying rejected payloads into diagnostics", () =>
81 Effect.gen(function* () {
82 const rejectedPayload = "runtime-payload-secret-sentinel";
83 const cause = yield* Effect.flip(
84 decodeRuntimePayload({
85 runtimePayload: {
86 attempt: rejectedPayload,
87 },
88 }),
89 );
90 const error = PersistenceDecodeError.fromSchemaError(
91 "ProviderSessionRuntimeRepository.list:decodeRows",
92 cause,
93 );
94
95 assert.equal(error.operation, "ProviderSessionRuntimeRepository.list:decodeRows");
96 assert.equal(error.cause, cause);
97 assert.notInclude(error.issue, rejectedPayload);
98 assert.notInclude(error.message, rejectedPayload);
99 assert.include(error.issue, "InvalidType");
100 }),
101);