diff --git a/server/src/modules/computers/adapters/boat.test.ts b/server/src/modules/computers/adapters/boat.test.ts index 5485df9565..8c072c152e 100644 --- a/server/src/modules/computers/adapters/boat.test.ts +++ b/server/src/modules/computers/adapters/boat.test.ts @@ -19,6 +19,11 @@ import { afterEach, describe, it, expect, vi } from "vitest"; import { boatBackend, createBoatTransportAdmission, desktopReadinessProgram, processProgram } from "./boat.js"; import { buildGitAuthInvocation } from "../../../services/git-credentials.js"; import type { ComputerRecord } from "../domain/ledger.js"; +const sshFactory = vi.hoisted(() => vi.fn()); +vi.mock("@paperclipai/adapter-utils/ssh", async importOriginal => ({ + ...await importOriginal(), + createSshCommandManagedRuntimeRunner: sshFactory, +})); const record: ComputerRecord = { id: "computer", companyId: "company", @@ -607,6 +612,34 @@ describe("Boat SSH admission", () => { await Promise.all(jobs); expect(runner.execute).toHaveBeenCalledTimes(12); }); + it("classifies checkout and file operations below reserved lifecycle capacity", async () => { + const id = randomUUID(); + const scoped = { ...record, id, providerId: `bx_${id}` }; + const completions: Array<() => void> = []; + const execute = vi.fn(async (_input: { stdin?: string }) => new Promise(resolve => { + completions.push(() => resolve({ ...result, stdout: "{}" })); + })); + sshFactory.mockReturnValue({ execute }); + const backend = boatBackend(async () => "fixture-key", vi.fn(async () => json({ + hostKey: "ssh-ed25519 AAAA", sshEndpoint: "fixture.invalid:2222", + }))); + const jobs = Array.from({ length: 8 }, (_, index) => backend.remote(scoped, { + action: index % 2 ? "workspace" : "list", root: "/home/user/paperclip/company/project", + })); + await vi.waitFor(() => expect(execute).toHaveBeenCalledTimes(6)); + jobs.push(backend.retire(scoped, { id: "owner", generation: 1, kind: "runner", phase: "retiring", port: 43127, + deadline: null, absoluteDeadline: null, process: null })); + jobs.push(backend.remote(scoped, { action: "owned-port", port: 5173, ownerId: "owner" })); + await vi.waitFor(() => expect(execute).toHaveBeenCalledTimes(8)); + const actions = execute.mock.calls.map(([input]) => JSON.parse(input.stdin!).action); + expect(actions.filter(action => action === "workspace" || action === "list")).toHaveLength(6); + expect(actions).toContain("retire"); expect(actions).toContain("owned-port"); + completions.splice(0).forEach(finish => finish()); + await vi.waitFor(() => expect(execute).toHaveBeenCalledTimes(10)); + completions.splice(0).forEach(finish => finish()); + await Promise.all(jobs); + sshFactory.mockReset(); + }); it("expires queued work without starting it and preserves the original remaining timeout", async () => { vi.useFakeTimers(); try { diff --git a/server/src/modules/computers/adapters/boat.ts b/server/src/modules/computers/adapters/boat.ts index 9944f1d222..16bd5c27c2 100644 --- a/server/src/modules/computers/adapters/boat.ts +++ b/server/src/modules/computers/adapters/boat.ts @@ -342,9 +342,10 @@ export function boatBackend( record: ComputerRecord, code: string, payload: unknown, + control = true, ) { const result = await ( - await runner(record, { control: true }) + await runner(record, { control }) ).execute({ command: "python3", args: ["-c", code], @@ -550,7 +551,9 @@ print('{}') return { url: url.toString(), secretHeaders: { Cookie: cookie } }; }, async remote(record, input) { - return execute(record, remoteProgram, input); + // Git setup and file I/O may wait on locks or transfer substantial data. + // Keep lifecycle capacity reserved; only the short owned-port probe uses it. + return execute(record, remoteProgram, input, input.action === "owned-port"); }, }; }