diff --git a/server/src/__tests__/workspace-runtime.test.ts b/server/src/__tests__/workspace-runtime.test.ts index e753259544..c65f26c4d6 100644 --- a/server/src/__tests__/workspace-runtime.test.ts +++ b/server/src/__tests__/workspace-runtime.test.ts @@ -5884,7 +5884,12 @@ describeEmbeddedPostgres("workspace runtime service control persistence", () => }); afterEach(async () => { - await resetRuntimeServicesForTests(); + // Terminate the real child processes this block starts and persist their + // stopped rows before the row deletes below. A left-over child exits later + // and its detached exit handler then writes a workspace_runtime_services row + // that references the deleted project, which raises an unhandled foreign-key + // error in the next test. + await resetRuntimeServicesForTests({ terminateProcesses: true }); // Service control writes activity_log rows. Delete them before the company // delete so a lingering foreign-key row cannot block the company delete and // leak rows into the next test. @@ -6923,6 +6928,12 @@ describeEmbeddedPostgres("workspace runtime startup reconciliation", () => { }); afterEach(async () => { + // Startup reconciliation starts real child processes and registers them. + // Terminate them and persist their stopped rows before the row deletes + // below. A left-over child exits later and its detached exit handler then + // writes a workspace_runtime_services row that references the deleted + // project, which raises an unhandled foreign-key error in the next test. + await resetRuntimeServicesForTests({ terminateProcesses: true }); // Startup reconciliation writes activity_log rows (for example, exposure // reservation drift). Delete those rows before the company delete. A stale // activity_log row holds a foreign key to the company and makes the company diff --git a/server/src/services/workspace-runtime.ts b/server/src/services/workspace-runtime.ts index f305aec894..261e00613c 100644 --- a/server/src/services/workspace-runtime.ts +++ b/server/src/services/workspace-runtime.ts @@ -6638,9 +6638,22 @@ function registerRuntimeService(db: Db | undefined, record: RuntimeServiceRecord runtimeServicesByReuseKey.delete(current.reuseKey); } void (async () => { - await cleanupRecordExposure(current); - await removeLocalServiceRegistryRecord(current.serviceKey); - await persistRuntimeServiceRecord(db, current); + // The child exited on its own. Record the terminal status as best effort. + // The persist can fail when a parent row is already gone: a caller can + // delete the project or the company while this service still runs, and the + // `project_id` foreign key then rejects the write. Catch every error here, + // or the detached persist becomes an unhandled rejection and crashes the + // host. This path runs off the child `exit` event, so no caller awaits it. + try { + await cleanupRecordExposure(current); + await removeLocalServiceRegistryRecord(current.serviceKey); + await persistRuntimeServiceRecord(db, current); + } catch (err) { + const detail = err instanceof Error ? err.message : String(err); + console.warn( + `[workspace-runtime] runtime service exit cleanup failed for ${current.id}: ${detail}`, + ); + } })(); }); }