From 777d891c255d8fb9f15658f391d2f957c3ad1ddc Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 8 Sep 2026 22:02:28 -0500 Subject: [PATCH] test: stabilize startup checks and document shutdown drain requirements Mock the native shutdown boundary in the startup unit test and wait for the persisted process identity before allowing the runtime readiness fixture to listen. Co-Authored-By: Paperclip --- doc/sandbox-work-folders.md | 7 +++++++ .../server-startup-feedback-export.test.ts | 6 ++++++ server/src/__tests__/workspace-runtime.test.ts | 15 ++++++++++++--- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/doc/sandbox-work-folders.md b/doc/sandbox-work-folders.md index 771e888586..3eedba2e4a 100644 --- a/doc/sandbox-work-folders.md +++ b/doc/sandbox-work-folders.md @@ -327,3 +327,10 @@ as incomplete. Active native turns keep their existing restart/reattach behavior Local and SSH session shutdown behavior is unchanged. A hard process kill cannot guarantee a provider-session checkpoint; scoped files and repository durability remain limited to the last successfully published work-folder checkpoint. + +Deployments must allow enough graceful shutdown time for that drain and other +server cleanup. On Railway, configure at least 60 seconds of draining time for +the service before testing an app redeployment; the platform default is zero. +This is a deployment prerequisite, not a fleet-default promotion. When upgrading +from a release without the idle-session drain, park warm native sessions and +verify their completed harness checkpoints before stopping the old app. diff --git a/server/src/__tests__/server-startup-feedback-export.test.ts b/server/src/__tests__/server-startup-feedback-export.test.ts index c8ff994b11..5702743ac9 100644 --- a/server/src/__tests__/server-startup-feedback-export.test.ts +++ b/server/src/__tests__/server-startup-feedback-export.test.ts @@ -345,6 +345,12 @@ vi.mock("../services/question-response-delivery.js", () => ({ })), })); +vi.mock("../services/native-runtime/native-session-executor.js", () => ({ + closeIdleSandboxNativeSessionsForShutdown: vi.fn(async () => ({ + closed: 0, busy: 0, failed: 0, + })), +})); + vi.mock("../services/native-runtime/native-question-bridge.js", () => ({ deliverNativeQuestionResponse: vi.fn(async () => "not_native"), nativeQuestionCancellationIdentity: vi.fn(() => null), diff --git a/server/src/__tests__/workspace-runtime.test.ts b/server/src/__tests__/workspace-runtime.test.ts index bc344b63ab..d6e96c6058 100644 --- a/server/src/__tests__/workspace-runtime.test.ts +++ b/server/src/__tests__/workspace-runtime.test.ts @@ -6510,6 +6510,7 @@ describeEmbeddedPostgres("workspace runtime service control persistence", () => const executionWorkspaceId = randomUUID(); const provisionMarkerPath = path.join(workspaceRoot, "runtime-provisioning.marker"); const markerPath = path.join(workspaceRoot, "runtime-spawned.marker"); + const readyMarkerPath = path.join(workspaceRoot, "runtime-ready.marker"); const provisionScript = [ `require("node:fs").writeFileSync(${JSON.stringify(provisionMarkerPath)}, "provisioning");`, "setTimeout(() => {}, 1200);", @@ -6518,11 +6519,13 @@ describeEmbeddedPostgres("workspace runtime service control persistence", () => `${JSON.stringify(process.execPath)} -e ${JSON.stringify(provisionScript)}`; const serverScript = [ `require("node:fs").writeFileSync(${JSON.stringify(markerPath)}, "spawned");`, - "setTimeout(() => {", + "const ready = setInterval(() => {", + ` if (!require("node:fs").existsSync(${JSON.stringify(readyMarkerPath)})) return;`, + " clearInterval(ready);", " require(\"node:http\")", " .createServer((_req, res) => { res.end(\"ok\"); })", " .listen(Number(process.env.PORT), \"127.0.0.1\");", - "}, 100);", + "}, 10);", "setInterval(() => {}, 1000);", ].join(" "); const command = `${JSON.stringify(process.execPath)} -e ${JSON.stringify(serverScript)}`; @@ -6589,7 +6592,9 @@ describeEmbeddedPostgres("workspace runtime service control persistence", () => .from(workspaceRuntimeServices) .where(eq(workspaceRuntimeServices.executionWorkspaceId, executionWorkspaceId)) .then((rows) => rows[0] ?? null); - if (row?.status === status) return row; + if (row?.status === status && (status !== "starting" || row.providerRef !== null)) { + return row; + } await new Promise((resolve) => setTimeout(resolve, 25)); } throw new Error(`Timed out waiting for persisted runtime service status ${status}`); @@ -6670,6 +6675,10 @@ describeEmbeddedPostgres("workspace runtime service control persistence", () => expect(startingRow.providerRef).toMatch(/^\d+$/); expect(startingRow.port).toEqual(expect.any(Number)); + // A process marker can arrive before its PID is persisted. Hold readiness + // until the complete starting row has been observed, rather than racing a + // fixed delay on a busy test runner. + await fs.writeFile(readyMarkerPath, "ready"); const services = await startPromise; expect(services).toHaveLength(1); expect(services[0]).toMatchObject({