1- import { mkdir , writeFile } from 'node:fs/promises'
1+ import { mkdir , readFile , writeFile } from 'node:fs/promises'
22import { dirname , resolve } from 'node:path'
33import { db } from '@sim/db'
44import {
@@ -18,6 +18,7 @@ import {
1818 workspaceForkPromoteRun ,
1919 workspaceForkResourceMap ,
2020} from '@sim/db/schema'
21+ import { readTestDatabaseUrl } from '@sim/db/testing/test-infrastructure'
2122import {
2223 createSessionPrincipal ,
2324 createWorkspaceApiKeyPrincipal ,
@@ -27,6 +28,7 @@ import { getErrorMessage, getPostgresErrorCode } from '@sim/utils/errors'
2728import { sleep } from '@sim/utils/helpers'
2829import { generateId } from '@sim/utils/id'
2930import { and , eq , inArray , sql } from 'drizzle-orm'
31+ import postgres from 'postgres'
3032import { afterAll , beforeEach , describe , expect , it , vi } from 'vitest'
3133import { removeUserFromOrganization } from '@/lib/billing/organizations/membership'
3234import { prepareProjectsForAccountDeletion } from '@/lib/projects/account-deletion'
@@ -194,7 +196,79 @@ afterAll(async () => {
194196
195197describe ( 'Project foundation at the database and application boundary' , ( ) => {
196198 check (
197- 'disabled rollout preserves legacy creation and fork/disconnect without Project rows' ,
199+ 'application creation, disconnect, organization deletion and archive commit with SQL enforcement' ,
200+ async ( ) => {
201+ const client = postgres ( readTestDatabaseUrl ( ) , { max : 1 , onnotice : ( ) => undefined } )
202+ try {
203+ await client . unsafe (
204+ await readFile (
205+ new URL (
206+ '../../../../../packages/db/migrations/0394_project_membership_enforcement.sql' ,
207+ import . meta. url
208+ ) ,
209+ 'utf8'
210+ )
211+ )
212+ const f = await fixture ( true , 1 )
213+ const organizationId = f . organizationId
214+ if ( ! organizationId ) throw new Error ( 'Missing organization fixture' )
215+ const created = await createProject . execute ( {
216+ principal : f . owner ,
217+ input : {
218+ organizationId : f . organizationId ,
219+ name : 'Enforced' ,
220+ initialEnvironment : { name : 'Production' } ,
221+ } ,
222+ request,
223+ } )
224+ environments . push ( created . initialEnvironment . id )
225+ const source = await getWorkspaceWithOwner ( created . initialEnvironment . id )
226+ if ( ! source ) throw new Error ( 'Missing source environment' )
227+ const fork = await createFork ( {
228+ source,
229+ policy : await getWorkspaceCreationPolicy ( { userId : f . ownerId } ) ,
230+ userId : f . ownerId ,
231+ name : 'Staging' ,
232+ } )
233+ environments . push ( fork . workspace . id )
234+ await unlinkForkEdge ( { parentWorkspaceId : source . id , childWorkspaceId : fork . workspace . id } )
235+ const [ detached ] = await db
236+ . select ( )
237+ . from ( projectWorkspace )
238+ . where ( eq ( projectWorkspace . workspaceId , fork . workspace . id ) )
239+ expect ( detached . projectId ) . not . toBe ( created . project . id )
240+ await db . transaction ( async ( tx ) => {
241+ await detachOrganizationWorkspacesTx ( tx , organizationId )
242+ await tx . delete ( organization ) . where ( eq ( organization . id , organizationId ) )
243+ } )
244+ await archiveProject . execute ( {
245+ principal : f . owner ,
246+ input : { projectId : created . project . id } ,
247+ request,
248+ } )
249+ const [ archived ] = await db . select ( ) . from ( project ) . where ( eq ( project . id , created . project . id ) )
250+ expect ( archived . organizationId ) . toBeNull ( )
251+ expect ( archived . archivedAt ) . not . toBeNull ( )
252+ expect (
253+ await db
254+ . select ( )
255+ . from ( workflow )
256+ . where ( and ( eq ( workflow . workspaceId , source . id ) , sql `${ workflow . archivedAt } IS NULL` ) )
257+ ) . toHaveLength ( 0 )
258+ } finally {
259+ await client . unsafe ( 'ROLLBACK' )
260+ for ( const table of [ 'project' , 'project_workspace' , 'workspace' , 'workflow' ] ) {
261+ await client . unsafe (
262+ `DROP TRIGGER IF EXISTS project_contract_lock ON ${ table } ; DROP TRIGGER IF EXISTS project_contract_check ON ${ table } `
263+ )
264+ }
265+ await client . end ( )
266+ }
267+ }
268+ )
269+
270+ check (
271+ 'workspace creation and fork/disconnect assign Projects even with the retired writer flag off' ,
198272 async ( ) => {
199273 vi . stubEnv ( 'PROJECT_WRITES_ENABLED' , 'false' )
200274 vi . stubEnv ( 'PROJECT_API_ENABLED' , 'false' )
@@ -214,7 +288,7 @@ describe('Project foundation at the database and application boundary', () => {
214288 environments . push ( source . id )
215289 expect (
216290 await db . select ( ) . from ( projectWorkspace ) . where ( eq ( projectWorkspace . workspaceId , source . id ) )
217- ) . toEqual ( [ ] )
291+ ) . toHaveLength ( 1 )
218292 const parent = await getWorkspaceWithOwner ( source . id )
219293 if ( ! parent ) throw new Error ( 'Missing source fixture' )
220294 const fork = await createFork ( {
@@ -229,11 +303,11 @@ describe('Project foundation at the database and application boundary', () => {
229303 . select ( )
230304 . from ( projectWorkspace )
231305 . where ( eq ( projectWorkspace . workspaceId , fork . workspace . id ) )
232- ) . toEqual ( [ ] )
306+ ) . toHaveLength ( 1 )
233307 await unlinkForkEdge ( { parentWorkspaceId : source . id , childWorkspaceId : fork . workspace . id } )
234308 const [ child ] = await db . select ( ) . from ( workspace ) . where ( eq ( workspace . id , fork . workspace . id ) )
235309 expect ( child . forkedFromWorkspaceId ) . toBeNull ( )
236- expect ( await db . select ( ) . from ( project ) . where ( eq ( project . ownerId , f . ownerId ) ) ) . toHaveLength ( 1 )
310+ expect ( await db . select ( ) . from ( project ) . where ( eq ( project . ownerId , f . ownerId ) ) ) . toHaveLength ( 3 )
237311 }
238312 )
239313
@@ -316,7 +390,7 @@ describe('Project foundation at the database and application boundary', () => {
316390 }
317391 )
318392
319- check ( 'writer activation assigns new workspaces while Project APIs remain disabled' , async ( ) => {
393+ check ( 'new workspaces receive Projects while Project APIs remain disabled' , async ( ) => {
320394 vi . stubEnv ( 'PROJECT_API_ENABLED' , 'false' )
321395 const f = await fixture ( false , 1 )
322396 const created = await db . transaction ( ( tx ) =>
@@ -335,18 +409,6 @@ describe('Project foundation at the database and application boundary', () => {
335409 expect (
336410 await db . select ( ) . from ( projectWorkspace ) . where ( eq ( projectWorkspace . workspaceId , created . id ) )
337411 ) . toHaveLength ( 1 )
338- vi . stubEnv ( 'PROJECT_WRITES_ENABLED' , 'false' )
339- vi . stubEnv ( 'PROJECT_API_ENABLED' , 'true' )
340- await expect (
341- createProject . execute ( {
342- principal : f . owner ,
343- input : { organizationId : null , name : 'Invalid' , initialEnvironment : { name : 'First' } } ,
344- request,
345- } )
346- ) . rejects . toThrow ( 'PROJECT_API_ENABLED requires PROJECT_WRITES_ENABLED' )
347- expect ( await db . select ( ) . from ( workspace ) . where ( eq ( workspace . ownerId , f . ownerId ) ) ) . toHaveLength (
348- 2
349- )
350412 } )
351413
352414 check ( 'legacy fork and disconnect refuse a partially assigned subtree' , async ( ) => {
0 commit comments