Open History to see the last commit
1import { assert, describe, it } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
7import * as SqlClient from "effect/sql/SqlClient";
8import { SqlError, classifySqliteError } from "effect/sql/SqlError";
9
10import { toPersistenceSqlError } from "../persistence/Errors.ts";
11import { userFacingDispatchErrorMessage } from "./UserFacingErrors.ts";
12
13describe("userFacingDispatchErrorMessage", () => {
14 it.effect("explains a real database open failure through dispatch and persistence wrappers", () =>
15 Effect.gen(function* () {
16 const fs = yield* FileSystem.FileSystem;
17 const path = yield* Path.Path;
18 const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-database-error-" });
19 const cause = yield* Effect.void.pipe(
20 Effect.provide(
21 NodeSqliteClient.layer({ filename: path.join(directory, "missing", "state.sqlite") }),
22 ),
23 Effect.flip,
24 );
25 const message = userFacingDispatchErrorMessage({
26 message: "Failed to dispatch orchestration command message.dispatch (command-1).",
27 cause: toPersistenceSqlError("OrchestrationEventStore.append:query")(cause),
28 });
29
30 assert.include(message, "server could not open a database file (SQLITE_CANTOPEN)");
31 assert.include(message, "machine running the server");
32 assert.include(message, "free disk space");
33 assert.include(message, "data and temporary directories exist and are writable");
34 assert.include(message, "then retry");
35 assert.notInclude(message, directory);
36 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
37 );
38
39 it.effect("explains a full database and allows a retry after capacity is restored", () =>
40 Effect.gen(function* () {
41 const sql = yield* SqlClient.SqlClient;
42 yield* sql`CREATE TABLE entries (payload BLOB)`;
43 yield* sql`PRAGMA max_page_count = 2`;
44 const cause = yield* sql`INSERT INTO entries VALUES (zeroblob(16384))`.pipe(Effect.flip);
45 const message = userFacingDispatchErrorMessage({ cause });
46
47 assert.include(message, "database or disk is full (SQLITE_FULL)");
48 assert.include(message, "Free space on the machine running the server, then retry");
49 assert.include(message, "database size limit");
50
51 yield* sql`PRAGMA max_page_count = 100`;
52 yield* sql`INSERT INTO entries VALUES (zeroblob(16384))`;
53 const rows = yield* sql<{ count: number }>`SELECT COUNT(*) AS count FROM entries`;
54 assert.equal(rows[0]?.count, 1);
55 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: ":memory:" }))),
56 );
57
58 it.effect("gives server permission guidance for a real read-only failure", () =>
59 Effect.gen(function* () {
60 const sql = yield* SqlClient.SqlClient;
61 yield* sql`CREATE TABLE entries (payload TEXT)`;
62 yield* sql`PRAGMA query_only = ON`;
63 const cause = yield* sql`INSERT INTO entries VALUES ('private-message')`.pipe(Effect.flip);
64 const message = userFacingDispatchErrorMessage(cause);
65
66 assert.include(message, "server database is read-only (SQLITE_READONLY)");
67 assert.include(message, "data directory is writable");
68 assert.notInclude(message, "private-message");
69 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: ":memory:" }))),
70 );
71
72 it("keeps unknown SQL failures contextual without exposing driver text or parameters", () => {
73 const cause = Object.assign(new Error("private-query-data"), { errcode: 1 });
74 const error = new SqlError({ reason: classifySqliteError(cause) });
75 const message = userFacingDispatchErrorMessage({
76 cause: toPersistenceSqlError("OrchestrationEventStore.append:query")(error),
77 });
78
79 assert.include(message, "server could not complete this database request");
80 assert.include(message, "server logs");
81 assert.notInclude(message, "private-query-data");
82 });
83});