@@ -314,6 +314,60 @@ afterAll(async () => {
314314} )
315315
316316describe ( 'Project foundation at the database and application boundary' , ( ) => {
317+ check (
318+ 'completed contraction needs no rollout marker, but incomplete schemas fail closed' ,
319+ async ( ) => {
320+ const f = await fixture ( false , 1 )
321+ const rollback = new Error ( 'Restore expansion schema after contraction proof' )
322+ await expect (
323+ db . transaction ( async ( tx ) => {
324+ await tx . execute ( sql `LOCK TABLE workspace IN ACCESS EXCLUSIVE MODE NOWAIT` )
325+ await tx . execute ( sql `DROP TABLE project_membership_rollout` )
326+ await expect ( getProjectMembershipPhase ( tx ) ) . rejects . toThrow ( 'authority is missing' )
327+ await tx . execute ( sql `DROP TABLE project_workspace` )
328+ await expect ( getProjectMembershipPhase ( tx ) ) . rejects . toThrow ( 'authority is missing' )
329+ await tx . execute ( sql `ALTER TABLE workspace ALTER COLUMN project_id SET NOT NULL` )
330+ await expect ( getProjectMembershipPhase ( tx ) ) . rejects . toThrow ( 'authority is missing' )
331+ await tx . execute ( sql `ALTER TABLE project ADD COLUMN organization_scope_key text
332+ GENERATED ALWAYS AS (CASE WHEN organization_id IS NULL THEN 'personal' ELSE 'organization:' || organization_id END) STORED` )
333+ await tx . execute ( sql `ALTER TABLE workspace ADD COLUMN organization_scope_key text
334+ GENERATED ALWAYS AS (CASE WHEN organization_id IS NULL THEN 'personal' ELSE 'organization:' || organization_id END) STORED` )
335+ await tx . execute (
336+ sql `ALTER TABLE project ADD CONSTRAINT project_id_organization_scope_unique UNIQUE (id, organization_scope_key)`
337+ )
338+ await tx . execute (
339+ sql `ALTER TABLE workspace ADD CONSTRAINT workspace_id_project_unique UNIQUE (id, project_id)`
340+ )
341+ await tx . execute ( sql `ALTER TABLE workspace ADD CONSTRAINT workspace_project_organization_fk
342+ FOREIGN KEY (project_id, organization_scope_key) REFERENCES project(id, organization_scope_key) DEFERRABLE INITIALLY DEFERRED` )
343+ await tx . execute ( sql `ALTER TABLE workspace ADD CONSTRAINT workspace_fork_project_fk
344+ FOREIGN KEY (forked_from_workspace_id, project_id) REFERENCES workspace(id, project_id) DEFERRABLE INITIALLY DEFERRED` )
345+ await tx . execute (
346+ sql `ALTER TABLE workspace VALIDATE CONSTRAINT workspace_project_id_project_id_fk`
347+ )
348+ expect ( await getProjectMembershipPhase ( tx ) ) . toBe ( 'column' )
349+ const created = await createWorkspaceInTransaction ( tx , {
350+ userId : f . ownerId ,
351+ name : 'After contraction' ,
352+ organizationId : null ,
353+ observedOrganizationId : null ,
354+ governingPermissionGroupOrganizationId : null ,
355+ workspaceMode : 'personal' ,
356+ billedAccountUserId : f . ownerId ,
357+ skipDefaultWorkflow : true ,
358+ } )
359+ const environments = await getProjectEnvironmentSource ( tx )
360+ const [ row ] = await tx
361+ . select ( { projectId : environments . projectId } )
362+ . from ( environments )
363+ . where ( eq ( environments . id , created . id ) )
364+ expect ( row . projectId ) . toEqual ( expect . any ( String ) )
365+ throw rollback
366+ } )
367+ ) . rejects . toBe ( rollback )
368+ }
369+ )
370+
317371 for ( const phase of [ 'connector' , 'column' ] as const ) {
318372 check ( `workspace creation and fork/disconnect use only ${ phase } authority` , async ( ) => {
319373 setProjectsEnabled ( false )
0 commit comments