Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma strängvärde byggs med konkatenering i stället för att matcha mönstret.
111 lines
4.5 KiB
TypeScript
111 lines
4.5 KiB
TypeScript
/**
|
|
* The database-error wrapper, and the classification it exists to unlock.
|
|
*
|
|
* The idiom `throw new Error(\`Database error: ${error.message}\`)` kept the
|
|
* prose and dropped `code`. `isTransientFailure()` checks that SQLSTATE FIRST,
|
|
* and 57014 (statement timeout) is already in its transient set, so stripping
|
|
* it turned a retryable timeout into UNKNOWN_ERROR: "Något gick fel. Försök
|
|
* igen." Agents cannot dispatch on that, so they retried: on production over
|
|
* 60 days, 462 wasted repeat calls, with 53.1% of all real-agent error calls
|
|
* sitting inside a repeat streak.
|
|
*
|
|
* The headline test is the last one: it asserts the OLD shape still resolves
|
|
* to UNKNOWN_ERROR and the new one resolves to TRANSIENT_ERROR, so it fails if
|
|
* the wrapper ever stops preserving the code.
|
|
*/
|
|
import { describe, it, expect } from 'vitest'
|
|
import { dbError, errorCauseTag } from '../db-error'
|
|
import { getStructuredError } from '../get-structured-error'
|
|
|
|
/** What supabase-js hands back when Postgres cancels on statement_timeout. */
|
|
const TIMEOUT_ERROR = {
|
|
message: 'canceling statement due to statement timeout',
|
|
code: '57014',
|
|
details: null,
|
|
hint: null,
|
|
}
|
|
|
|
describe('dbError', () => {
|
|
it('preserves the SQLSTATE, which is the whole point', () => {
|
|
const wrapped = dbError(TIMEOUT_ERROR)
|
|
expect(wrapped.code).toBe('57014')
|
|
})
|
|
|
|
it('preserves details and hint for the server log', () => {
|
|
const wrapped = dbError({
|
|
message: 'duplicate key value violates unique constraint',
|
|
code: '23505',
|
|
details: 'Key (company_id, account_number) already exists.',
|
|
hint: 'Use upsert.',
|
|
})
|
|
expect(wrapped.details).toContain('already exists')
|
|
expect(wrapped.hint).toBe('Use upsert.')
|
|
})
|
|
|
|
it('never renders the literal "undefined"', () => {
|
|
// A driver-level failure (aborted fetch, gateway timeout) can arrive with
|
|
// no message at all. "Database error: undefined" is the string that made
|
|
// these unsearchable in production.
|
|
for (const shape of [{}, { message: undefined }, { message: '' }, { message: ' ' }, null]) {
|
|
expect(dbError(shape).message).not.toContain('undefined')
|
|
}
|
|
})
|
|
|
|
it('prefixes with the historical context by default', () => {
|
|
expect(dbError({ message: 'boom' }).message).toBe('Database error: boom')
|
|
})
|
|
|
|
it('keeps the driver message verbatim when context is null', () => {
|
|
// fetchAllRows passes null: callers such as query_journal's
|
|
// sanitizeDbError already match on the exact text, and this change is
|
|
// meant to add the code, not reword anything.
|
|
expect(dbError({ message: 'boom' }, null).message).toBe('boom')
|
|
})
|
|
|
|
it('accepts a custom context', () => {
|
|
expect(dbError({ message: 'boom' }, 'Database error resolving voucher "A-7"').message).toBe(
|
|
'Database error resolving voucher "A-7": boom',
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('errorCauseTag', () => {
|
|
it('returns the SQLSTATE, which carries no tenant data', () => {
|
|
expect(errorCauseTag(dbError(TIMEOUT_ERROR))).toBe('57014')
|
|
})
|
|
|
|
it('falls back to the error name when there is no code', () => {
|
|
expect(errorCauseTag(new TypeError('nope'))).toBe('TypeError')
|
|
})
|
|
|
|
it('returns null rather than inventing a tag', () => {
|
|
expect(errorCauseTag(new Error('plain'))).toBeNull()
|
|
expect(errorCauseTag(null)).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('classification: the regression this prevents', () => {
|
|
it('classifies a wrapped statement timeout as retryable', () => {
|
|
expect(getStructuredError(dbError(TIMEOUT_ERROR)).code).toBe('TRANSIENT_ERROR')
|
|
})
|
|
|
|
it('would classify the OLD bare-Error shape as UNKNOWN_ERROR', () => {
|
|
// The bug, pinned. `new Error(error.message)` is what fetchAllRows threw,
|
|
// and PostgREST does not always put the word "timeout" in the message it
|
|
// returns, so message-pattern matching cannot be relied on. Only the code
|
|
// is durable, which is why the wrapper must carry it.
|
|
const stripped = new Error('some driver text that names no timeout')
|
|
expect(getStructuredError(stripped).code).toBe('UNKNOWN_ERROR')
|
|
|
|
// Same failure, code intact: now dispatchable.
|
|
const preserved = dbError({ message: 'some driver text that names no timeout', code: '57014' })
|
|
expect(getStructuredError(preserved).code).toBe('TRANSIENT_ERROR')
|
|
})
|
|
|
|
it('does not mistake a SQLSTATE for one of our own stable codes', () => {
|
|
// extractCode() only accepts /^[A-Z_]+$/. Every SQLSTATE contains digits,
|
|
// so attaching one cannot hijack the application error registry.
|
|
expect(getStructuredError(dbError({ message: 'x', code: '23505' })).code).not.toBe('23505')
|
|
})
|
|
})
|