22 * @vitest -environment node
33 */
44
5- import { beforeEach , describe , expect , it , vi } from 'vitest'
5+ import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
66
77const { getTableById, loadWorkspace } = vi . hoisted ( ( ) => ( {
88 getTableById : vi . fn ( ) ,
@@ -16,6 +16,37 @@ vi.mock('@/lib/workspaces/application/workspace-context', () => ({
1616
1717import { resolveActiveTableContext } from '@/lib/table/application/context'
1818
19+ const WORKSPACE_ONE = {
20+ workspaceId : 'workspace-1' ,
21+ workspaceOrganizationId : 'organization-1' ,
22+ allowPersonalApiKeys : true ,
23+ billedAccountUserId : 'billing-user-1' ,
24+ }
25+
26+ const WORKSPACE_TWO = {
27+ workspaceId : 'workspace-2' ,
28+ workspaceOrganizationId : 'organization-2' ,
29+ allowPersonalApiKeys : false ,
30+ billedAccountUserId : 'billing-user-2' ,
31+ }
32+
33+ /** Runs `body` while capturing any unhandled promise rejection it provokes. */
34+ async function withUnhandledRejectionWatch ( body : ( ) => Promise < void > ) : Promise < unknown [ ] > {
35+ const seen : unknown [ ] = [ ]
36+ const onUnhandled = ( reason : unknown ) => {
37+ seen . push ( reason )
38+ }
39+ process . on ( 'unhandledRejection' , onUnhandled )
40+ try {
41+ await body ( )
42+ await new Promise ( ( resolve ) => setImmediate ( resolve ) )
43+ await new Promise ( ( resolve ) => setImmediate ( resolve ) )
44+ } finally {
45+ process . off ( 'unhandledRejection' , onUnhandled )
46+ }
47+ return seen
48+ }
49+
1950describe ( 'table application context' , ( ) => {
2051 beforeEach ( ( ) => {
2152 vi . clearAllMocks ( )
@@ -24,12 +55,13 @@ describe('table application context', () => {
2455 workspaceId : 'workspace-1' ,
2556 name : 'Contacts' ,
2657 } )
27- loadWorkspace . mockResolvedValue ( {
28- workspaceId : 'workspace-1' ,
29- workspaceOrganizationId : 'organization-1' ,
30- allowPersonalApiKeys : true ,
31- billedAccountUserId : 'billing-user-1' ,
32- } )
58+ loadWorkspace . mockImplementation ( async ( workspaceId : string ) =>
59+ workspaceId === 'workspace-1' ? WORKSPACE_ONE : WORKSPACE_TWO
60+ )
61+ } )
62+
63+ afterEach ( ( ) => {
64+ vi . useRealTimers ( )
3365 } )
3466
3567 it ( 'derives workspace scope from the canonical active table' , async ( ) => {
@@ -44,13 +76,106 @@ describe('table application context', () => {
4476 expect ( loadWorkspace ) . toHaveBeenCalledWith ( 'workspace-1' )
4577 } )
4678
47- it ( 'conceals an asserted cross-workspace table before workspace resolution' , async ( ) => {
79+ it ( 'starts the workspace load without waiting for the table when a workspace is asserted' , async ( ) => {
80+ let releaseTable : ( table : unknown ) => void = ( ) => { }
81+ getTableById . mockImplementationOnce (
82+ ( ) =>
83+ new Promise ( ( resolve ) => {
84+ releaseTable = resolve
85+ } )
86+ )
87+
88+ const pending = resolveActiveTableContext ( {
89+ tableId : 'table-1' ,
90+ assertedWorkspaceId : 'workspace-1' ,
91+ } )
92+ await Promise . resolve ( )
93+ await Promise . resolve ( )
94+
95+ expect ( loadWorkspace ) . toHaveBeenCalledWith ( 'workspace-1' )
96+
97+ releaseTable ( { id : 'table-1' , workspaceId : 'workspace-1' , name : 'Contacts' } )
98+ await expect ( pending ) . resolves . toMatchObject ( { tableId : 'table-1' , workspaceId : 'workspace-1' } )
99+ } )
100+
101+ it ( 'waits for the table before loading a workspace when none is asserted' , async ( ) => {
102+ let releaseTable : ( table : unknown ) => void = ( ) => { }
103+ getTableById . mockImplementationOnce (
104+ ( ) =>
105+ new Promise ( ( resolve ) => {
106+ releaseTable = resolve
107+ } )
108+ )
109+
110+ const pending = resolveActiveTableContext ( { tableId : 'table-1' } )
111+ await Promise . resolve ( )
112+ await Promise . resolve ( )
113+
114+ expect ( loadWorkspace ) . not . toHaveBeenCalled ( )
115+
116+ releaseTable ( { id : 'table-1' , workspaceId : 'workspace-1' , name : 'Contacts' } )
117+ await expect ( pending ) . resolves . toMatchObject ( { tableId : 'table-1' , workspaceId : 'workspace-1' } )
118+ expect ( loadWorkspace ) . toHaveBeenCalledWith ( 'workspace-1' )
119+ } )
120+
121+ it ( 'conceals an asserted cross-workspace table as not found' , async ( ) => {
48122 await expect (
49123 resolveActiveTableContext ( { tableId : 'table-1' , assertedWorkspaceId : 'workspace-2' } )
50124 ) . rejects . toMatchObject ( { code : 'not_found' , message : 'Table not found' } )
125+ } )
126+
127+ it ( 'conceals a table that does not exist at all' , async ( ) => {
128+ getTableById . mockResolvedValueOnce ( null )
129+
130+ await expect (
131+ resolveActiveTableContext ( { tableId : 'missing' , assertedWorkspaceId : 'workspace-1' } )
132+ ) . rejects . toMatchObject ( { code : 'not_found' , message : 'Table not found' } )
133+ } )
134+
135+ it ( 'conceals a missing table with no asserted workspace' , async ( ) => {
136+ getTableById . mockResolvedValueOnce ( null )
137+
138+ await expect ( resolveActiveTableContext ( { tableId : 'missing' } ) ) . rejects . toMatchObject ( {
139+ code : 'not_found' ,
140+ message : 'Table not found' ,
141+ } )
51142 expect ( loadWorkspace ) . not . toHaveBeenCalled ( )
52143 } )
53144
145+ it ( 'surfaces not_found rather than a failing workspace load on a mismatched assertion' , async ( ) => {
146+ const failure = new Error ( 'workspace database unavailable' )
147+ loadWorkspace . mockRejectedValueOnce ( failure )
148+
149+ const unhandled = await withUnhandledRejectionWatch ( async ( ) => {
150+ await expect (
151+ resolveActiveTableContext ( { tableId : 'table-1' , assertedWorkspaceId : 'workspace-2' } )
152+ ) . rejects . toMatchObject ( { code : 'not_found' , message : 'Table not found' } )
153+ } )
154+
155+ expect ( unhandled ) . toEqual ( [ ] )
156+ } )
157+
158+ it ( 'surfaces not_found rather than a failing table load on a matched assertion' , async ( ) => {
159+ const failure = new Error ( 'table database unavailable' )
160+ getTableById . mockRejectedValueOnce ( failure )
161+
162+ const unhandled = await withUnhandledRejectionWatch ( async ( ) => {
163+ await expect (
164+ resolveActiveTableContext ( { tableId : 'table-1' , assertedWorkspaceId : 'workspace-1' } )
165+ ) . rejects . toBe ( failure )
166+ } )
167+
168+ expect ( unhandled ) . toEqual ( [ ] )
169+ } )
170+
171+ it ( 'refuses a workspace context that is not the canonical workspace of the table' , async ( ) => {
172+ loadWorkspace . mockResolvedValueOnce ( WORKSPACE_TWO )
173+
174+ await expect (
175+ resolveActiveTableContext ( { tableId : 'table-1' , assertedWorkspaceId : 'workspace-1' } )
176+ ) . rejects . toMatchObject ( { code : 'not_found' , message : 'Table not found' } )
177+ } )
178+
54179 it ( 'fails when the canonical workspace is unavailable' , async ( ) => {
55180 loadWorkspace . mockResolvedValueOnce ( null )
56181
@@ -60,10 +185,31 @@ describe('table application context', () => {
60185 } )
61186 } )
62187
188+ it ( 'fails when the canonical workspace is unavailable on the asserted path' , async ( ) => {
189+ loadWorkspace . mockResolvedValueOnce ( null )
190+
191+ await expect (
192+ resolveActiveTableContext ( { tableId : 'table-1' , assertedWorkspaceId : 'workspace-1' } )
193+ ) . rejects . toMatchObject ( { code : 'not_found' , message : 'Workspace not found' } )
194+ } )
195+
63196 it ( 'propagates canonical workspace database failures' , async ( ) => {
64197 const failure = new Error ( 'workspace database unavailable' )
65198 loadWorkspace . mockRejectedValueOnce ( failure )
66199
67200 await expect ( resolveActiveTableContext ( { tableId : 'table-1' } ) ) . rejects . toBe ( failure )
68201 } )
202+
203+ it ( 'propagates canonical workspace database failures on the asserted path' , async ( ) => {
204+ const failure = new Error ( 'workspace database unavailable' )
205+ loadWorkspace . mockRejectedValueOnce ( failure )
206+
207+ const unhandled = await withUnhandledRejectionWatch ( async ( ) => {
208+ await expect (
209+ resolveActiveTableContext ( { tableId : 'table-1' , assertedWorkspaceId : 'workspace-1' } )
210+ ) . rejects . toBe ( failure )
211+ } )
212+
213+ expect ( unhandled ) . toEqual ( [ ] )
214+ } )
69215} )
0 commit comments