diff --git a/apps/server/src/persistence/AuthPairingLinks.ts b/apps/server/src/persistence/AuthPairingLinks.ts index c29b023d1d8..e54c977e7ab 100644 --- a/apps/server/src/persistence/AuthPairingLinks.ts +++ b/apps/server/src/persistence/AuthPairingLinks.ts @@ -11,6 +11,7 @@ import { AuthEnvironmentScopes } from "@t3tools/contracts"; import { type AuthPairingLinkRepositoryError, PersistenceDecodeError, + type PersistenceErrorCorrelation, PersistenceSqlError, } from "./Errors.ts"; @@ -66,6 +67,22 @@ export const GetAuthPairingLinkByCredentialInput = Schema.Struct({ }); export type GetAuthPairingLinkByCredentialInput = typeof GetAuthPairingLinkByCredentialInput.Type; +const AuthPairingLinkRawDbRow = Schema.Struct({ + id: Schema.String, + credential: Schema.Unknown, + method: Schema.Unknown, + scopes: Schema.Unknown, + subject: Schema.Unknown, + label: Schema.Unknown, + proofKeyThumbprint: Schema.Unknown, + createdAt: Schema.Unknown, + expiresAt: Schema.Unknown, + consumedAt: Schema.Unknown, + revokedAt: Schema.Unknown, +}); + +const decodeAuthPairingLinkDbRow = Schema.decodeUnknownEffect(AuthPairingLinkRecord); + export class AuthPairingLinkRepository extends Context.Service< AuthPairingLinkRepository, { @@ -87,11 +104,19 @@ export class AuthPairingLinkRepository extends Context.Service< } >()("t3/persistence/AuthPairingLinks/AuthPairingLinkRepository") {} -function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { +function toPersistenceSqlOrDecodeError( + sqlOperation: string, + decodeOperation: string, + correlation?: PersistenceErrorCorrelation, +) { return (cause: unknown): AuthPairingLinkRepositoryError => Schema.isSchemaError(cause) - ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause) - : new PersistenceSqlError({ operation: sqlOperation, cause }); + ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause, correlation) + : new PersistenceSqlError({ + operation: sqlOperation, + ...(correlation === undefined ? {} : { correlation }), + cause, + }); } export const make = Effect.gen(function* () { @@ -132,7 +157,7 @@ export const make = Effect.gen(function* () { const consumeAvailablePairingLinkRow = SqlSchema.findOneOption({ Request: ConsumeAuthPairingLinkInput, - Result: AuthPairingLinkRecord, + Result: AuthPairingLinkRawDbRow, execute: ({ credential, proofKeyThumbprint, consumedAt, now }) => sql` UPDATE auth_pairing_links @@ -162,7 +187,7 @@ export const make = Effect.gen(function* () { const listActivePairingLinkRows = SqlSchema.findAll({ Request: ListActiveAuthPairingLinksInput, - Result: AuthPairingLinkRecord, + Result: AuthPairingLinkRawDbRow, execute: ({ now }) => sql` SELECT @@ -201,7 +226,7 @@ export const make = Effect.gen(function* () { const getPairingLinkRowByCredential = SqlSchema.findOneOption({ Request: GetAuthPairingLinkByCredentialInput, - Result: AuthPairingLinkRecord, + Result: AuthPairingLinkRawDbRow, execute: ({ credential }) => sql` SELECT @@ -227,6 +252,7 @@ export const make = Effect.gen(function* () { toPersistenceSqlOrDecodeError( "AuthPairingLinkRepository.create:query", "AuthPairingLinkRepository.create:encodeRequest", + { pairingLinkId: input.id }, ), ), ); @@ -239,6 +265,22 @@ export const make = Effect.gen(function* () { "AuthPairingLinkRepository.consumeAvailable:decodeRow", ), ), + Effect.flatMap((rowOption) => + Option.match(rowOption, { + onNone: () => Effect.succeed(Option.none()), + onSome: (row) => + decodeAuthPairingLinkDbRow(row).pipe( + Effect.mapError((cause) => + PersistenceDecodeError.fromSchemaError( + "AuthPairingLinkRepository.consumeAvailable:decodeRow", + cause, + { pairingLinkId: row.id }, + ), + ), + Effect.map(Option.some), + ), + }), + ), ); const listActive: AuthPairingLinkRepository["Service"]["listActive"] = (input) => @@ -249,6 +291,19 @@ export const make = Effect.gen(function* () { "AuthPairingLinkRepository.listActive:decodeRows", ), ), + Effect.flatMap((rows) => + Effect.forEach(rows, (row) => + decodeAuthPairingLinkDbRow(row).pipe( + Effect.mapError((cause) => + PersistenceDecodeError.fromSchemaError( + "AuthPairingLinkRepository.listActive:decodeRows", + cause, + { pairingLinkId: row.id }, + ), + ), + ), + ), + ), ); const revoke: AuthPairingLinkRepository["Service"]["revoke"] = (input) => @@ -257,6 +312,7 @@ export const make = Effect.gen(function* () { toPersistenceSqlOrDecodeError( "AuthPairingLinkRepository.revoke:query", "AuthPairingLinkRepository.revoke:decodeRows", + { pairingLinkId: input.id }, ), ), Effect.map((rows) => rows.length > 0), @@ -270,6 +326,22 @@ export const make = Effect.gen(function* () { "AuthPairingLinkRepository.getByCredential:decodeRow", ), ), + Effect.flatMap((rowOption) => + Option.match(rowOption, { + onNone: () => Effect.succeed(Option.none()), + onSome: (row) => + decodeAuthPairingLinkDbRow(row).pipe( + Effect.mapError((cause) => + PersistenceDecodeError.fromSchemaError( + "AuthPairingLinkRepository.getByCredential:decodeRow", + cause, + { pairingLinkId: row.id }, + ), + ), + Effect.map(Option.some), + ), + }), + ), ); return { diff --git a/apps/server/src/persistence/AuthSessions.ts b/apps/server/src/persistence/AuthSessions.ts index 17f76042d0a..545688e3822 100644 --- a/apps/server/src/persistence/AuthSessions.ts +++ b/apps/server/src/persistence/AuthSessions.ts @@ -16,6 +16,7 @@ import { import { type AuthSessionRepositoryError, PersistenceDecodeError, + type PersistenceErrorCorrelation, PersistenceSqlError, } from "./Errors.ts"; @@ -122,6 +123,25 @@ const AuthSessionDbRow = Schema.Struct({ revokedAt: Schema.NullOr(Schema.DateTimeUtcFromString), }); +const AuthSessionRawDbRow = Schema.Struct({ + sessionId: Schema.String, + subject: Schema.Unknown, + scopes: Schema.Unknown, + method: Schema.Unknown, + clientLabel: Schema.Unknown, + clientIpAddress: Schema.Unknown, + clientUserAgent: Schema.Unknown, + clientDeviceType: Schema.Unknown, + clientOs: Schema.Unknown, + clientBrowser: Schema.Unknown, + issuedAt: Schema.Unknown, + expiresAt: Schema.Unknown, + lastConnectedAt: Schema.Unknown, + revokedAt: Schema.Unknown, +}); + +const decodeAuthSessionDbRow = Schema.decodeUnknownEffect(AuthSessionDbRow); + function toAuthSessionRecord(row: typeof AuthSessionDbRow.Type): AuthSessionRecord { return { sessionId: row.sessionId, @@ -143,11 +163,19 @@ function toAuthSessionRecord(row: typeof AuthSessionDbRow.Type): AuthSessionReco }; } -function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { +function toPersistenceSqlOrDecodeError( + sqlOperation: string, + decodeOperation: string, + correlation?: PersistenceErrorCorrelation, +) { return (cause: unknown): AuthSessionRepositoryError => Schema.isSchemaError(cause) - ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause) - : new PersistenceSqlError({ operation: sqlOperation, cause }); + ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause, correlation) + : new PersistenceSqlError({ + operation: sqlOperation, + ...(correlation === undefined ? {} : { correlation }), + cause, + }); } export const make = Effect.gen(function* () { @@ -192,7 +220,7 @@ export const make = Effect.gen(function* () { const getSessionRowById = SqlSchema.findOneOption({ Request: GetAuthSessionByIdInput, - Result: AuthSessionDbRow, + Result: AuthSessionRawDbRow, execute: ({ sessionId }) => sql` SELECT @@ -217,7 +245,7 @@ export const make = Effect.gen(function* () { const listActiveSessionRows = SqlSchema.findAll({ Request: ListActiveAuthSessionsInput, - Result: AuthSessionDbRow, + Result: AuthSessionRawDbRow, execute: ({ now }) => sql` SELECT @@ -285,6 +313,7 @@ export const make = Effect.gen(function* () { toPersistenceSqlOrDecodeError( "AuthSessionRepository.create:query", "AuthSessionRepository.create:encodeRequest", + { sessionId: input.sessionId }, ), ), ); @@ -295,12 +324,23 @@ export const make = Effect.gen(function* () { toPersistenceSqlOrDecodeError( "AuthSessionRepository.getById:query", "AuthSessionRepository.getById:decodeRow", + { sessionId: input.sessionId }, ), ), Effect.flatMap((rowOption) => Option.match(rowOption, { onNone: () => Effect.succeed(Option.none()), - onSome: (row) => Effect.succeed(Option.some(toAuthSessionRecord(row))), + onSome: (row) => + decodeAuthSessionDbRow(row).pipe( + Effect.mapError((cause) => + PersistenceDecodeError.fromSchemaError( + "AuthSessionRepository.getById:decodeRow", + cause, + { sessionId: input.sessionId }, + ), + ), + Effect.map((decodedRow) => Option.some(toAuthSessionRecord(decodedRow))), + ), }), ), ); @@ -313,7 +353,20 @@ export const make = Effect.gen(function* () { "AuthSessionRepository.listActive:decodeRows", ), ), - Effect.flatMap((rows) => Effect.succeed(rows.map((row) => toAuthSessionRecord(row)))), + Effect.flatMap((rows) => + Effect.forEach(rows, (row) => + decodeAuthSessionDbRow(row).pipe( + Effect.mapError((cause) => + PersistenceDecodeError.fromSchemaError( + "AuthSessionRepository.listActive:decodeRows", + cause, + { sessionId: row.sessionId }, + ), + ), + Effect.map(toAuthSessionRecord), + ), + ), + ), ); const revoke: AuthSessionRepository["Service"]["revoke"] = (input) => @@ -322,6 +375,7 @@ export const make = Effect.gen(function* () { toPersistenceSqlOrDecodeError( "AuthSessionRepository.revoke:query", "AuthSessionRepository.revoke:decodeRows", + { sessionId: input.sessionId }, ), ), Effect.map((rows) => rows.length > 0), @@ -333,6 +387,7 @@ export const make = Effect.gen(function* () { toPersistenceSqlOrDecodeError( "AuthSessionRepository.revokeAllExcept:query", "AuthSessionRepository.revokeAllExcept:decodeRows", + { currentSessionId: input.currentSessionId }, ), ), Effect.map((rows) => rows.map((row) => row.sessionId)), @@ -344,6 +399,7 @@ export const make = Effect.gen(function* () { toPersistenceSqlOrDecodeError( "AuthSessionRepository.setLastConnectedAt:query", "AuthSessionRepository.setLastConnectedAt:encodeRequest", + { sessionId: input.sessionId }, ), ), ); diff --git a/apps/server/src/persistence/Errors.ts b/apps/server/src/persistence/Errors.ts index e7d081c8f72..03edaec77d6 100644 --- a/apps/server/src/persistence/Errors.ts +++ b/apps/server/src/persistence/Errors.ts @@ -19,11 +19,20 @@ function summarizeSchemaIssue(issue: SchemaIssue.Issue): string { // Core Persistence Errors // =============================== +export const PersistenceErrorCorrelation = Schema.Union([ + Schema.Struct({ sessionId: Schema.String }), + Schema.Struct({ currentSessionId: Schema.String }), + Schema.Struct({ pairingLinkId: Schema.String }), + Schema.Struct({ threadId: Schema.String }), +]); +export type PersistenceErrorCorrelation = typeof PersistenceErrorCorrelation.Type; + export class PersistenceSqlError extends Schema.TaggedErrorClass()( "PersistenceSqlError", { operation: Schema.String, detail: Schema.optional(Schema.String), + correlation: Schema.optional(PersistenceErrorCorrelation), cause: Schema.optional(Schema.Defect()), }, ) { @@ -39,13 +48,19 @@ export class PersistenceDecodeError extends Schema.TaggedErrorClass Schema.isSchemaError(cause) - ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause) - : new PersistenceSqlError({ operation: sqlOperation, cause }); + ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause, correlation) + : new PersistenceSqlError({ + operation: sqlOperation, + ...(correlation === undefined ? {} : { correlation }), + cause, + }); } export const make = Effect.gen(function* () { @@ -165,7 +186,7 @@ export const make = Effect.gen(function* () { const getRuntimeRowByThreadId = SqlSchema.findOneOption({ Request: GetRuntimeRequestSchema, - Result: ProviderSessionRuntimeDbRowSchema, + Result: ProviderSessionRuntimeRawDbRowSchema, execute: ({ threadId }) => sql` SELECT @@ -185,7 +206,7 @@ export const make = Effect.gen(function* () { const listRuntimeRows = SqlSchema.findAll({ Request: Schema.Void, - Result: ProviderSessionRuntimeDbRowSchema, + Result: ProviderSessionRuntimeRawDbRowSchema, execute: () => sql` SELECT @@ -218,6 +239,7 @@ export const make = Effect.gen(function* () { toPersistenceSqlOrDecodeError( "ProviderSessionRuntimeRepository.upsert:query", "ProviderSessionRuntimeRepository.upsert:encodeRequest", + { threadId: runtime.threadId }, ), ), ); @@ -228,17 +250,19 @@ export const make = Effect.gen(function* () { toPersistenceSqlOrDecodeError( "ProviderSessionRuntimeRepository.getByThreadId:query", "ProviderSessionRuntimeRepository.getByThreadId:decodeRow", + { threadId: input.threadId }, ), ), Effect.flatMap((runtimeRowOption) => Option.match(runtimeRowOption, { onNone: () => Effect.succeed(Option.none()), onSome: (row) => - decodeRuntime(row).pipe( + decodeRuntimeRow(row).pipe( Effect.mapError((cause) => PersistenceDecodeError.fromSchemaError( - "ProviderSessionRuntimeRepository.getByThreadId:rowToRuntime", + "ProviderSessionRuntimeRepository.getByThreadId:decodeRow", cause, + { threadId: input.threadId }, ), ), Effect.map((runtime) => Option.some(runtime)), @@ -256,18 +280,16 @@ export const make = Effect.gen(function* () { ), ), Effect.flatMap((rows) => - Effect.forEach( - rows, - (row) => - decodeRuntime(row).pipe( - Effect.mapError((cause) => - PersistenceDecodeError.fromSchemaError( - "ProviderSessionRuntimeRepository.list:rowToRuntime", - cause, - ), + Effect.forEach(rows, (row) => + decodeRuntimeRow(row).pipe( + Effect.mapError((cause) => + PersistenceDecodeError.fromSchemaError( + "ProviderSessionRuntimeRepository.list:decodeRows", + cause, + { threadId: row.threadId }, ), ), - { concurrency: "unbounded" }, + ), ), ), ); @@ -280,6 +302,7 @@ export const make = Effect.gen(function* () { (cause) => new PersistenceSqlError({ operation: "ProviderSessionRuntimeRepository.deleteByThreadId:query", + correlation: { threadId: input.threadId }, cause, }), ), diff --git a/apps/server/src/persistence/RepositoryErrorCorrelation.test.ts b/apps/server/src/persistence/RepositoryErrorCorrelation.test.ts new file mode 100644 index 00000000000..f7425200fd1 --- /dev/null +++ b/apps/server/src/persistence/RepositoryErrorCorrelation.test.ts @@ -0,0 +1,253 @@ +import { AuthSessionId, ThreadId, type AuthEnvironmentScope } from "@t3tools/contracts"; +import { assert, describe, it } from "@effect/vitest"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +import * as AuthPairingLinks from "./AuthPairingLinks.ts"; +import * as AuthSessions from "./AuthSessions.ts"; +import * as PersistenceErrors from "./Errors.ts"; +import { SqlitePersistenceMemory } from "./Layers/Sqlite.ts"; +import * as ProviderSessionRuntime from "./ProviderSessionRuntime.ts"; + +const issuedAt = DateTime.makeUnsafe("2026-06-20T00:00:00.000Z"); +const expiresAt = DateTime.makeUnsafe("2027-06-20T00:00:00.000Z"); +const now = DateTime.makeUnsafe("2026-06-21T00:00:00.000Z"); +const scopes: ReadonlyArray = ["access:read"]; + +const authSessionLayer = AuthSessions.layer.pipe(Layer.provideMerge(SqlitePersistenceMemory)); +const authPairingLinkLayer = AuthPairingLinks.layer.pipe( + Layer.provideMerge(SqlitePersistenceMemory), +); +const providerSessionRuntimeLayer = ProviderSessionRuntime.layer.pipe( + Layer.provideMerge(SqlitePersistenceMemory), +); + +describe("persistence error correlation", () => { + it.effect("correlates auth session SQL and row-decode failures without sensitive fields", () => + Effect.gen(function* () { + const sessions = yield* AuthSessions.AuthSessionRepository; + const sql = yield* SqlClient.SqlClient; + const sessionId = AuthSessionId.make("session-correlation"); + const currentSessionId = AuthSessionId.make("current-session-correlation"); + const subject = "session-subject-secret-sentinel"; + + yield* sessions.create({ + sessionId, + subject, + scopes, + method: "browser-session-cookie", + client: { + label: null, + ipAddress: null, + userAgent: null, + deviceType: "desktop", + os: null, + browser: null, + }, + issuedAt, + expiresAt, + }); + yield* sql` + UPDATE auth_sessions + SET scopes = ${"session-scopes-secret-sentinel"} + WHERE session_id = ${sessionId} + `; + + const decodeError = yield* Effect.flip(sessions.listActive({ now })); + assert.instanceOf(decodeError, PersistenceErrors.PersistenceDecodeError); + assert.deepStrictEqual(decodeError.correlation, { sessionId }); + assert.equal( + decodeError.message, + `Decode error in AuthSessionRepository.listActive:decodeRows: ${decodeError.issue}`, + ); + assert.notInclude(decodeError.issue, subject); + assert.notInclude(decodeError.issue, "session-scopes-secret-sentinel"); + assert.notInclude(decodeError.message, subject); + + yield* sql`DROP TABLE auth_sessions`; + const createError = yield* Effect.flip( + sessions.create({ + sessionId, + subject, + scopes, + method: "browser-session-cookie", + client: { + label: null, + ipAddress: null, + userAgent: null, + deviceType: "desktop", + os: null, + browser: null, + }, + issuedAt, + expiresAt, + }), + ); + assert.instanceOf(createError, PersistenceErrors.PersistenceSqlError); + assert.deepStrictEqual(createError.correlation, { sessionId }); + assert.equal(createError.message, "SQL error in AuthSessionRepository.create:query"); + assert.notInclude(createError.message, subject); + assert.notInclude(createError.message, DateTime.formatIso(issuedAt)); + + const revokeOtherError = yield* Effect.flip( + sessions.revokeAllExcept({ currentSessionId, revokedAt: now }), + ); + assert.instanceOf(revokeOtherError, PersistenceErrors.PersistenceSqlError); + assert.deepStrictEqual(revokeOtherError.correlation, { currentSessionId }); + assert.equal( + revokeOtherError.message, + "SQL error in AuthSessionRepository.revokeAllExcept:query", + ); + assert.notInclude(revokeOtherError.message, DateTime.formatIso(now)); + }).pipe(Effect.provide(authSessionLayer)), + ); + + it.effect("correlates pairing-link create and revoke failures by id only", () => + Effect.gen(function* () { + const pairingLinks = yield* AuthPairingLinks.AuthPairingLinkRepository; + const sql = yield* SqlClient.SqlClient; + const id = "pairing-link-correlation"; + const credential = "pairing-credential-secret-sentinel"; + const subject = "pairing-subject-secret-sentinel"; + const scopesPayload = "pairing-scopes-secret-sentinel"; + + yield* sql` + INSERT INTO auth_pairing_links ( + id, + credential, + method, + scopes, + subject, + label, + proof_key_thumbprint, + created_at, + expires_at, + consumed_at, + revoked_at + ) + VALUES ( + ${id}, + ${credential}, + ${"one-time-token"}, + ${scopesPayload}, + ${subject}, + NULL, + NULL, + ${DateTime.formatIso(issuedAt)}, + ${DateTime.formatIso(expiresAt)}, + NULL, + NULL + ) + `; + + const decodeError = yield* Effect.flip(pairingLinks.getByCredential({ credential })); + assert.instanceOf(decodeError, PersistenceErrors.PersistenceDecodeError); + assert.deepStrictEqual(decodeError.correlation, { pairingLinkId: id }); + assert.equal( + decodeError.message, + `Decode error in AuthPairingLinkRepository.getByCredential:decodeRow: ${decodeError.issue}`, + ); + assert.notInclude(decodeError.issue, credential); + assert.notInclude(decodeError.issue, subject); + assert.notInclude(decodeError.issue, scopesPayload); + assert.notInclude(decodeError.message, DateTime.formatIso(issuedAt)); + + yield* sql`DROP TABLE auth_pairing_links`; + const createError = yield* Effect.flip( + pairingLinks.create({ + id, + credential, + method: "one-time-token", + scopes, + subject, + label: null, + proofKeyThumbprint: null, + createdAt: issuedAt, + expiresAt, + }), + ); + assert.instanceOf(createError, PersistenceErrors.PersistenceSqlError); + assert.deepStrictEqual(createError.correlation, { pairingLinkId: id }); + assert.notInclude(createError.message, credential); + assert.notInclude(createError.message, subject); + assert.notInclude(createError.message, DateTime.formatIso(issuedAt)); + + const revokeError = yield* Effect.flip(pairingLinks.revoke({ id, revokedAt: now })); + assert.instanceOf(revokeError, PersistenceErrors.PersistenceSqlError); + assert.deepStrictEqual(revokeError.correlation, { pairingLinkId: id }); + assert.notInclude(revokeError.message, credential); + assert.notInclude(revokeError.message, DateTime.formatIso(now)); + }).pipe(Effect.provide(authPairingLinkLayer)), + ); + + it.effect("correlates provider runtime SQL and per-row decode failures by thread", () => + Effect.gen(function* () { + const runtimes = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; + const sql = yield* SqlClient.SqlClient; + const threadId = ThreadId.make("thread-correlation"); + const runtimePayload = "runtime-payload-secret-sentinel"; + const lastSeenAt = "2026-06-20T00:00:00.000Z"; + + yield* sql` + INSERT INTO provider_session_runtime ( + thread_id, + provider_name, + provider_instance_id, + adapter_key, + runtime_mode, + status, + last_seen_at, + resume_cursor_json, + runtime_payload_json + ) + VALUES ( + ${threadId}, + ${"codex"}, + NULL, + ${"codex"}, + ${"invalid-runtime-mode"}, + ${"running"}, + ${lastSeenAt}, + NULL, + ${`{"secret":"${runtimePayload}"}`} + ) + `; + + const decodeError = yield* Effect.flip(runtimes.list()); + assert.instanceOf(decodeError, PersistenceErrors.PersistenceDecodeError); + assert.deepStrictEqual(decodeError.correlation, { threadId }); + assert.equal( + decodeError.message, + `Decode error in ProviderSessionRuntimeRepository.list:decodeRows: ${decodeError.issue}`, + ); + assert.notInclude(decodeError.issue, runtimePayload); + assert.notInclude(decodeError.message, runtimePayload); + assert.notInclude(decodeError.message, lastSeenAt); + + yield* sql`DROP TABLE provider_session_runtime`; + const sqlFailure = yield* Effect.flip( + runtimes.upsert({ + threadId, + providerName: "codex", + providerInstanceId: null, + adapterKey: "codex", + runtimeMode: "full-access", + status: "running", + lastSeenAt, + resumeCursor: null, + runtimePayload: { secret: runtimePayload }, + }), + ); + assert.instanceOf(sqlFailure, PersistenceErrors.PersistenceSqlError); + assert.deepStrictEqual(sqlFailure.correlation, { threadId }); + assert.equal( + sqlFailure.message, + "SQL error in ProviderSessionRuntimeRepository.upsert:query", + ); + assert.notInclude(sqlFailure.message, runtimePayload); + assert.notInclude(sqlFailure.message, lastSeenAt); + }).pipe(Effect.provide(providerSessionRuntimeLayer)), + ); +});