@@ -5,7 +5,12 @@ import { describe, expect, it } from 'vitest'
55import { OrchestrationError } from '@/lib/core/orchestration/types'
66import { TableRowLimitError } from '@/lib/table/billing'
77import type { ColumnDefinition } from '@/lib/table/types'
8- import { rootErrorMessage , rowWriteErrorResponse , tableFilterError } from '@/app/api/table/utils'
8+ import {
9+ orchestrationErrorResponse ,
10+ orchestrationOutcomeErrorResponse ,
11+ rootErrorMessage ,
12+ tableFilterError ,
13+ } from '@/app/api/table/utils'
914
1015/** Mimics drizzle's DrizzleQueryError: message is the failed SQL, real error on `cause`. */
1116function wrapLikeDrizzle ( cause : Error ) : Error {
@@ -29,9 +34,9 @@ describe('rootErrorMessage', () => {
2934 } )
3035} )
3136
32- describe ( 'rowWriteErrorResponse ' , ( ) => {
37+ describe ( 'orchestrationErrorResponse ' , ( ) => {
3338 it ( 'passes the plan row-limit error through as a 400' , async ( ) => {
34- const response = rowWriteErrorResponse ( new TableRowLimitError ( 10000 ) )
39+ const response = orchestrationErrorResponse ( new TableRowLimitError ( 10000 ) )
3540 expect ( response ?. status ) . toBe ( 400 )
3641 const body = await response ?. json ( )
3742 expect ( body . error ) . toBe (
@@ -40,7 +45,7 @@ describe('rowWriteErrorResponse', () => {
4045 } )
4146
4247 it ( 'passes a classified validation failure through as 400' , async ( ) => {
43- const response = rowWriteErrorResponse (
48+ const response = orchestrationErrorResponse (
4449 new OrchestrationError ( 'validation' , 'Value for column "email" must be unique' )
4550 )
4651 expect ( response ?. status ) . toBe ( 400 )
@@ -50,24 +55,26 @@ describe('rowWriteErrorResponse', () => {
5055
5156 it ( 'answers the code the failure carries, not one derived from its wording' , ( ) => {
5257 expect (
53- rowWriteErrorResponse ( new OrchestrationError ( 'not_found' , 'Row not found' ) ) ?. status
58+ orchestrationErrorResponse ( new OrchestrationError ( 'not_found' , 'Row not found' ) ) ?. status
5459 ) . toBe ( 404 )
5560 // The phrase that used to force a 400 no longer decides anything.
5661 expect (
57- rowWriteErrorResponse ( new OrchestrationError ( 'conflict' , 'Row 3: must be unique' ) ) ?. status
62+ orchestrationErrorResponse ( new OrchestrationError ( 'conflict' , 'Row 3: must be unique' ) )
63+ ?. status
5864 ) . toBe ( 409 )
5965 } )
6066
6167 it ( 'unwraps a classified failure drizzle wrapped in a query error' , ( ) => {
6268 expect (
63- rowWriteErrorResponse ( wrapLikeDrizzle ( new OrchestrationError ( 'validation' , 'Row 3: bad' ) ) )
64- ?. status
69+ orchestrationErrorResponse (
70+ wrapLikeDrizzle ( new OrchestrationError ( 'validation' , 'Row 3: bad' ) )
71+ ) ?. status
6572 ) . toBe ( 400 )
6673 } )
6774
6875 it ( 'returns null for unknown errors so callers keep their generic 500' , ( ) => {
69- expect ( rowWriteErrorResponse ( new Error ( 'connection refused' ) ) ) . toBeNull ( )
70- expect ( rowWriteErrorResponse ( wrapLikeDrizzle ( new Error ( 'deadlock detected' ) ) ) ) . toBeNull ( )
76+ expect ( orchestrationErrorResponse ( new Error ( 'connection refused' ) ) ) . toBeNull ( )
77+ expect ( orchestrationErrorResponse ( wrapLikeDrizzle ( new Error ( 'deadlock detected' ) ) ) ) . toBeNull ( )
7178 } )
7279} )
7380
@@ -117,3 +124,60 @@ describe('tableFilterError', () => {
117124 expect ( tableFilterError ( { col_status : { $regex : 'x' } } as never , columns ) ?. status ) . toBe ( 400 )
118125 } )
119126} )
127+
128+ describe ( 'orchestrationOutcomeErrorResponse' , ( ) => {
129+ /**
130+ * Shaped like a driver fault surfacing verbatim — a statement plus its bound
131+ * parameters — so the assertion proves none of it reaches the response body.
132+ */
133+ const leakyMessage =
134+ 'Failed query: delete from "user_table" where "user_table"."id" = $1 params: tbl-1'
135+
136+ it ( 'replaces an unclassified failure message with the fallback' , async ( ) => {
137+ const response = orchestrationOutcomeErrorResponse (
138+ { success : false , error : leakyMessage , errorCode : 'internal' } ,
139+ 'Failed to delete table'
140+ )
141+
142+ expect ( response . status ) . toBe ( 500 )
143+ const body = await response . json ( )
144+ expect ( body ) . toEqual ( { error : 'Failed to delete table' } )
145+ expect ( JSON . stringify ( body ) ) . not . toContain ( 'Failed query' )
146+ expect ( JSON . stringify ( body ) ) . not . toContain ( 'params:' )
147+ } )
148+
149+ it ( 'replaces an unclassified failure with no error code too' , async ( ) => {
150+ const response = orchestrationOutcomeErrorResponse (
151+ { error : leakyMessage } ,
152+ 'Failed to delete table'
153+ )
154+
155+ expect ( response . status ) . toBe ( 500 )
156+ expect ( await response . json ( ) ) . toEqual ( { error : 'Failed to delete table' } )
157+ } )
158+
159+ it ( 'keeps the message of a classified failure' , async ( ) => {
160+ const response = orchestrationOutcomeErrorResponse (
161+ { error : 'A table named "Orders" already exists in this workspace' , errorCode : 'conflict' } ,
162+ 'Failed to rename table'
163+ )
164+
165+ expect ( response . status ) . toBe ( 409 )
166+ expect ( await response . json ( ) ) . toEqual ( {
167+ error : 'A table named "Orders" already exists in this workspace' ,
168+ } )
169+ } )
170+
171+ it ( 'carries the rejecting lock kind on a 423' , async ( ) => {
172+ const response = orchestrationOutcomeErrorResponse (
173+ { error : 'Table is locked against deletion' , errorCode : 'locked' , lock : 'delete' } ,
174+ 'Failed to delete table'
175+ )
176+
177+ expect ( response . status ) . toBe ( 423 )
178+ expect ( await response . json ( ) ) . toEqual ( {
179+ error : 'Table is locked against deletion' ,
180+ lock : 'delete' ,
181+ } )
182+ } )
183+ } )
0 commit comments