44import { createMockRequest } from '@sim/testing'
55import { describe , expect , it , vi } from 'vitest'
66
7- const mocks = vi . hoisted ( ( ) => ( { status : 200 } ) )
8-
9- vi . mock ( '@/lib/api/server/routes' , ( ) => ( {
10- defineInternalJsonRoute : vi . fn (
11- ( options : { staticResponseHeaders ?: HeadersInit } ) => async ( ) =>
12- new Response ( JSON . stringify ( { ok : mocks . status < 400 } ) , {
13- status : mocks . status ,
14- headers : options . staticResponseHeaders ,
15- } )
16- ) ,
17- extendInternalErrorPolicy : vi . fn ( ( ) => ( { } ) ) ,
18- internalErrorResponse : vi . fn ( ) ,
19- internalOrchestrationErrorPolicy : { } ,
20- internalRateLimits : { none : vi . fn ( ( ) => ( { kind : 'none' } ) ) } ,
21- internalSessionAuth : { } ,
7+ const mocks = vi . hoisted ( ( ) => ( {
8+ status : 200 ,
9+ errorPolicy : undefined as
10+ | {
11+ project ( error : unknown ) : { body : unknown ; status : number ; headers ?: HeadersInit } | null
12+ unhandled ?( ) : { body : unknown ; status : number ; headers ?: HeadersInit }
13+ }
14+ | undefined ,
2215} ) )
2316
17+ vi . mock ( '@/lib/api/server/routes' , ( ) => {
18+ const internalErrorResponse = vi . fn ( ( status : number , body : unknown , headers ?: HeadersInit ) => ( {
19+ body,
20+ status,
21+ headers,
22+ } ) )
23+ const internalOrchestrationErrorPolicy = {
24+ project ( error : unknown ) {
25+ if ( ! ( error instanceof Error ) || ! ( 'code' in error ) ) return null
26+ const code = ( error as Error & { code : string } ) . code
27+ const status =
28+ code === 'validation'
29+ ? 400
30+ : code === 'unauthorized'
31+ ? 401
32+ : code === 'forbidden'
33+ ? 403
34+ : code === 'not_found'
35+ ? 404
36+ : code === 'conflict'
37+ ? 409
38+ : 500
39+ return internalErrorResponse ( status , { error : error . message } )
40+ } ,
41+ unhandled : ( ) => internalErrorResponse ( 500 , { error : 'Internal server error' } ) ,
42+ }
43+
44+ return {
45+ defineInternalJsonRoute : vi . fn (
46+ ( options : { errorPolicy : typeof mocks . errorPolicy ; staticResponseHeaders ?: HeadersInit } ) => {
47+ mocks . errorPolicy = options . errorPolicy
48+ return async ( ) =>
49+ new Response ( JSON . stringify ( { ok : mocks . status < 400 } ) , {
50+ status : mocks . status ,
51+ headers : options . staticResponseHeaders ,
52+ } )
53+ }
54+ ) ,
55+ extendInternalErrorPolicy : vi . fn (
56+ (
57+ base : typeof internalOrchestrationErrorPolicy ,
58+ project : ( error : unknown ) => ReturnType < typeof internalErrorResponse > | null
59+ ) => ( {
60+ project : ( error : unknown ) => project ( error ) ?? base . project ( error ) ,
61+ unhandled : base . unhandled ,
62+ } )
63+ ) ,
64+ internalErrorResponse,
65+ internalOrchestrationErrorPolicy,
66+ internalRateLimits : { none : vi . fn ( ( ) => ( { kind : 'none' } ) ) } ,
67+ internalSessionAuth : { } ,
68+ }
69+ } )
70+
71+ import { NoWorkspaceAccessError } from '@/lib/core/application/workspace-authorization'
72+ import { OrchestrationError } from '@/lib/core/orchestration/types'
73+ import {
74+ SelectorConnectionUnavailableError ,
75+ SelectorContextUnavailableError ,
76+ SelectorOptionsUnavailableError ,
77+ } from '@/lib/selectors/server/errors'
2478import { POST } from '@/app/api/selectors/execute/route'
2579
80+ function project ( error : unknown ) {
81+ const result = mocks . errorPolicy ?. project ( error )
82+ if ( ! result ) throw new Error ( 'Expected route error policy to project the error' )
83+ return result
84+ }
85+
2686describe ( 'POST /api/selectors/execute' , ( ) => {
2787 it ( 'marks success, authentication, parse, and unhandled responses private and non-cacheable' , async ( ) => {
2888 for ( const status of [ 200 , 400 , 401 , 500 ] ) {
@@ -33,4 +93,39 @@ describe('POST /api/selectors/execute', () => {
3393 expect ( response . headers . get ( 'Cache-Control' ) ) . toBe ( 'private, no-store' )
3494 }
3595 } )
96+
97+ it . each ( [
98+ [ 'missing workflow' , new OrchestrationError ( 'not_found' , 'Workflow not found' ) ] ,
99+ [ 'missing workspace' , new OrchestrationError ( 'not_found' , 'Workspace not found' ) ] ,
100+ [ 'asserted workspace mismatch' , new OrchestrationError ( 'not_found' , 'Workflow not found' ) ] ,
101+ [ 'cross-tenant workspace' , new NoWorkspaceAccessError ( ) ] ,
102+ ] ) ( 'conceals %s as the same selector-scope absence' , ( _case , error ) => {
103+ expect ( project ( error ) ) . toEqual ( {
104+ status : 404 ,
105+ body : { error : 'Selector scope not found' } ,
106+ headers : { 'Cache-Control' : 'private, no-store' } ,
107+ } )
108+ } )
109+
110+ it . each ( [
111+ [ new SelectorContextUnavailableError ( ) , 400 , 'Context unavailable' ] ,
112+ [ new SelectorConnectionUnavailableError ( ) , 403 , 'Connection unavailable' ] ,
113+ [ new SelectorOptionsUnavailableError ( ) , 502 , 'Options unavailable' ] ,
114+ ] ) ( 'preserves selector error projection for %s' , ( error , status , message ) => {
115+ expect ( project ( error ) ) . toEqual ( {
116+ status,
117+ body : { error : message } ,
118+ headers : { 'Cache-Control' : 'private, no-store' } ,
119+ } )
120+ } )
121+
122+ it ( 'preserves same-workspace forbidden errors' , ( ) => {
123+ expect (
124+ project ( new OrchestrationError ( 'forbidden' , 'Insufficient workspace permissions' ) )
125+ ) . toEqual ( {
126+ status : 403 ,
127+ body : { error : 'Insufficient workspace permissions' } ,
128+ headers : undefined ,
129+ } )
130+ } )
36131} )
0 commit comments