@@ -182,61 +182,91 @@ describe('Operator archive repair against the full compatible schema', () => {
182182 60000
183183 )
184184
185- it ( 'repairs an explicitly reviewed detached family without changing environments and replays a lost report' , async ( ) => {
186- const artifacts = {
187- manifest : join ( directory , 'detach-manifest.json' ) ,
188- report : join ( directory , 'detach-report.json' ) ,
189- }
190- try {
191- await client `INSERT INTO "user" (id,name,email,email_verified,created_at,updated_at)
185+ it . each ( [ 'column' , 'mixed' ] as const ) (
186+ 'repairs an explicitly reviewed detached family with %s membership and replays a lost report' ,
187+ async ( membership ) => {
188+ const artifacts = {
189+ manifest : join ( directory , `detach-${ membership } -manifest.json` ) ,
190+ report : join ( directory , `detach-${ membership } -report.json` ) ,
191+ }
192+ try {
193+ await client `INSERT INTO "user" (id,name,email,email_verified,created_at,updated_at)
192194 VALUES ('detach-owner','Owner','detach@fixture.test',true,now(),now())`
193- await client `INSERT INTO project (id,name,owner_id) VALUES ('detach-project','Keep context','detach-owner')`
194- await client `INSERT INTO workspace (id,name,owner_id,billed_account_user_id,project_id)
195+ await client `INSERT INTO project (id,name,owner_id) VALUES ('detach-project','Keep context','detach-owner')`
196+ await client `INSERT INTO workspace (id,name,owner_id,billed_account_user_id,project_id)
195197 VALUES ('retained-root','Retained','detach-owner','detach-owner','detach-project'),
196198 ('detached-root','Detached','detach-owner','detach-owner','detach-project')`
197- await client `INSERT INTO workspace (id,name,owner_id,billed_account_user_id,project_id,forked_from_workspace_id)
199+ await client `INSERT INTO workspace (id,name,owner_id,billed_account_user_id,project_id,forked_from_workspace_id)
198200 VALUES ('detached-child','Child','detach-owner','detach-owner','detach-project','detached-root')`
199- await expect ( run ( 'plan' , artifacts ) ) . rejects . toMatchObject ( { code : 2 } )
200- const plan = JSON . parse ( await readFile ( artifacts . manifest , 'utf8' ) )
201- plan . groupings [ 0 ] . decision = 'detach'
202- plan . groupings [ 0 ] . detachRootId = 'detached-root'
203- await writeFile ( artifacts . manifest , JSON . stringify ( plan ) )
204- const before =
205- await client `SELECT id,owner_id,organization_id,archived_at,forked_from_workspace_id FROM workspace ORDER BY id`
206- const holder = postgres ( url . toString ( ) , { max : 1 } )
207- try {
208- await holder . begin ( async ( tx ) => {
209- await tx `SELECT pg_advisory_xact_lock(hashtextextended('project:detach-project',0))`
210- await expect ( run ( 'repair' , artifacts ) ) . rejects . toMatchObject ( { code : 2 } )
211- expect ( await client `SELECT id FROM project WHERE owner_id='detach-owner'` ) . toHaveLength ( 1 )
212- } )
201+ if ( membership === 'mixed' ) {
202+ await client `INSERT INTO project_workspace (project_id,workspace_id)
203+ VALUES ('detach-project','retained-root'),('detach-project','detached-child')`
204+ await client `UPDATE workspace SET project_id=NULL WHERE id IN ('retained-root','detached-child')`
205+ }
206+ await expect ( run ( 'plan' , artifacts ) ) . rejects . toMatchObject ( { code : 2 } )
207+ const plan = JSON . parse ( await readFile ( artifacts . manifest , 'utf8' ) )
208+ plan . groupings [ 0 ] . decision = 'detach'
209+ plan . groupings [ 0 ] . detachRootId = 'detached-root'
210+ await writeFile ( artifacts . manifest , JSON . stringify ( plan ) )
211+ const before =
212+ await client `SELECT id,owner_id,organization_id,archived_at,forked_from_workspace_id FROM workspace ORDER BY id`
213+ const holder = postgres ( url . toString ( ) , { max : 1 } )
214+ try {
215+ await holder . begin ( async ( tx ) => {
216+ await tx `SELECT pg_advisory_xact_lock(hashtextextended('project:detach-project',0))`
217+ await expect ( run ( 'repair' , artifacts ) ) . rejects . toMatchObject ( { code : 2 } )
218+ expect ( await client `SELECT id FROM project WHERE owner_id='detach-owner'` ) . toHaveLength (
219+ 1
220+ )
221+ } )
222+ if ( membership === 'mixed' ) {
223+ await client `INSERT INTO project (id,name,owner_id) VALUES ('changed-project','Changed','detach-owner')`
224+ await holder . begin ( async ( tx ) => {
225+ await tx `UPDATE project_workspace SET project_id='changed-project' WHERE workspace_id='retained-root'`
226+ await expect ( run ( 'repair' , artifacts ) ) . rejects . toMatchObject ( { code : 2 } )
227+ expect (
228+ await client `SELECT project_id FROM workspace WHERE id='retained-root'`
229+ ) . toEqual ( [ { project_id : null } ] )
230+ expect (
231+ await client `SELECT id FROM project WHERE id=${ plan . groupings [ 0 ] . destinationProjectId } `
232+ ) . toHaveLength ( 0 )
233+ } )
234+ await expect ( run ( 'repair' , artifacts ) ) . rejects . toMatchObject ( { code : 1 } )
235+ expect ( await client `SELECT project_id FROM workspace WHERE id='retained-root'` ) . toEqual (
236+ [ { project_id : null } ]
237+ )
238+ await client `UPDATE project_workspace SET project_id='detach-project' WHERE workspace_id='retained-root'`
239+ await client `DELETE FROM project WHERE id='changed-project'`
240+ }
241+ } finally {
242+ await holder . end ( )
243+ }
244+ await client `UPDATE workspace SET archived_at='2026-05-01' WHERE id='detached-child'`
245+ await expect ( run ( 'repair' , artifacts ) ) . rejects . toMatchObject ( { code : 1 } )
246+ expect ( await client `SELECT id FROM project WHERE owner_id='detach-owner'` ) . toHaveLength ( 1 )
247+ await client `UPDATE workspace SET archived_at=NULL WHERE id='detached-child'`
248+ await expect ( run ( 'repair' , artifacts ) ) . rejects . toMatchObject ( { code : 2 } )
249+ const destination = plan . groupings [ 0 ] . destinationProjectId
250+ expect (
251+ await client `SELECT id FROM workspace WHERE project_id = ${ destination } ORDER BY id`
252+ ) . toEqual ( [ { id : 'detached-child' } , { id : 'detached-root' } ] )
253+ expect ( await client `SELECT id,name FROM project WHERE id = 'detach-project'` ) . toEqual ( [
254+ { id : 'detach-project' , name : 'Keep context' } ,
255+ ] )
256+ expect (
257+ await client `SELECT id,owner_id,organization_id,archived_at,forked_from_workspace_id FROM workspace ORDER BY id`
258+ ) . toEqual ( before )
259+ await rm ( artifacts . report )
260+ await expect ( run ( 'repair' , artifacts ) ) . rejects . toMatchObject ( { code : 2 } )
261+ expect ( await client `SELECT id FROM project WHERE owner_id = 'detach-owner'` ) . toHaveLength ( 2 )
213262 } finally {
214- await holder . end ( )
263+ await client `DELETE FROM workspace WHERE owner_id = 'detach-owner'`
264+ await client `DELETE FROM project WHERE owner_id = 'detach-owner'`
265+ await client `DELETE FROM "user" WHERE id = 'detach-owner'`
215266 }
216- await client `UPDATE workspace SET archived_at='2026-05-01' WHERE id='detached-child'`
217- await expect ( run ( 'repair' , artifacts ) ) . rejects . toMatchObject ( { code : 1 } )
218- expect ( await client `SELECT id FROM project WHERE owner_id='detach-owner'` ) . toHaveLength ( 1 )
219- await client `UPDATE workspace SET archived_at=NULL WHERE id='detached-child'`
220- await expect ( run ( 'repair' , artifacts ) ) . rejects . toMatchObject ( { code : 2 } )
221- const destination = plan . groupings [ 0 ] . destinationProjectId
222- expect (
223- await client `SELECT id FROM workspace WHERE project_id = ${ destination } ORDER BY id`
224- ) . toEqual ( [ { id : 'detached-child' } , { id : 'detached-root' } ] )
225- expect ( await client `SELECT id,name FROM project WHERE id = 'detach-project'` ) . toEqual ( [
226- { id : 'detach-project' , name : 'Keep context' } ,
227- ] )
228- expect (
229- await client `SELECT id,owner_id,organization_id,archived_at,forked_from_workspace_id FROM workspace ORDER BY id`
230- ) . toEqual ( before )
231- await rm ( artifacts . report )
232- await expect ( run ( 'repair' , artifacts ) ) . rejects . toMatchObject ( { code : 2 } )
233- expect ( await client `SELECT id FROM project WHERE owner_id = 'detach-owner'` ) . toHaveLength ( 2 )
234- } finally {
235- await client `DELETE FROM workspace WHERE owner_id = 'detach-owner'`
236- await client `DELETE FROM project WHERE owner_id = 'detach-owner'`
237- await client `DELETE FROM "user" WHERE id = 'detach-owner'`
238- }
239- } , 60000 )
267+ } ,
268+ 60000
269+ )
240270
241271 it ( 'keeps a Project active when another active environment still uses legacy membership' , async ( ) => {
242272 const artifacts = {
@@ -265,7 +295,7 @@ describe('Operator archive repair against the full compatible schema', () => {
265295 }
266296 } , 60000 )
267297
268- it ( 'completes provider cleanup despite a throwing local MCP subscriber' , async ( ) => {
298+ it ( 'notifies healthy local MCP subscribers and completes cleanup after another subscriber throws ' , async ( ) => {
269299 const serverId = generateId ( )
270300 try {
271301 await client `INSERT INTO "user" (id,name,email,email_verified,created_at,updated_at)
@@ -288,13 +318,17 @@ describe('Operator archive repair against the full compatible schema', () => {
288318 import { repairArchivedProjectEnvironment } from '@/lib/projects/backfill-repair';
289319 import { mcpPubSub } from '@/lib/mcp/pubsub';
290320 import { db, dbReplica } from '@sim/db';
321+ import assert from 'node:assert/strict';
291322 import postgres from 'postgres';
292323 const client = postgres(process.env.MIGRATION_DATABASE_URL, {max:1});
293324 const unsubscribe = mcpPubSub.onWorkflowToolsChanged(() => { throw new Error('fixture subscriber failed'); });
325+ const received = [];
326+ const unsubscribeHealthy = mcpPubSub.onWorkflowToolsChanged((event) => received.push(event));
294327 try {
295328 await repairArchivedProjectEnvironment(client, {workspaceId:'local-env', archivedAt:'2026-05-01 00:00:00', workflowIds:['local-flow']}, 'local-subscriber');
329+ assert.ok(received.some((event) => event.serverId === '${ serverId } ' && event.workspaceId === 'local-env'), 'Healthy subscriber missed the archive notification');
296330 } finally {
297- unsubscribe(); mcpPubSub.dispose(); await client.end();
331+ unsubscribe(); unsubscribeHealthy(); mcpPubSub.dispose(); await client.end();
298332 await Promise.all([...new Set([db.$client,dbReplica.$client])].map(client=>client.end()));
299333 }
300334 ` ,
0 commit comments