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 <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-08 22:02:28 -05:00
parent c7b68b52b0
commit 777d891c25
3 changed files with 25 additions and 3 deletions

View File

@ -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.

View File

@ -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),

View File

@ -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({