From 2b27e7e2b3b60e79a5877feeb7417875d002f29f Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Mon, 31 Aug 2026 06:12:32 -0400 Subject: [PATCH] Make development rebuilds reliable and plan-safe --- src/cli/commands/development.ts | 6 ++++ src/cli/runtime.ts | 1 + src/cli/support/host-client.ts | 6 ++-- .../support/host-client.test.ts | 28 +++++++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 tests/unit/command-boundary/support/host-client.test.ts diff --git a/src/cli/commands/development.ts b/src/cli/commands/development.ts index a315a72..a289bc0 100644 --- a/src/cli/commands/development.ts +++ b/src/cli/commands/development.ts @@ -328,6 +328,12 @@ async function rebuild(invocation: ParsedInvocation, context: CommandContext, st const repository = record.session.repositories.find((entry) => entry.projectId === selection.projectId); if (!repository) throw new Error(`No worktree is registered for ${selection.projectId}.`); const mode = selected.mode as 'candidate' | 'live'; + if (invocation.options.plan === true) return { + sessionId, + target: `${selection.projectId}.${selection.targetId}`, + mode, + mutation: false, + }; if (target.kind === 'package-watch') await rebuildPackage({ state, runtime, target, worktree: repository.worktree, mode, context }); else if (target.kind === 'rebuild-restart') { if (!target.operations.build) throw new Error(`${selection.projectId}.${selection.targetId} does not declare a build operation.`); diff --git a/src/cli/runtime.ts b/src/cli/runtime.ts index 23ca4bb..511bff5 100644 --- a/src/cli/runtime.ts +++ b/src/cli/runtime.ts @@ -108,6 +108,7 @@ export async function runCommandLine(argv: string[], overrides: Partial[0], body: string, secure }); } -export function invokeLocalHostManager(input: unknown) { - return invoke({ socketPath: managerSocket }, JSON.stringify(input), false); +export function invokeLocalHostManager(input: unknown, socketPath = managerSocket) { + // Development operations may spend minutes building between manager calls. + // A closed idle Unix-socket connection must never be reused for the next call. + return invoke({ socketPath, agent: false }, JSON.stringify(input), false); } export function invokeHostManager(input: unknown, server: string | undefined, env: NodeJS.ProcessEnv) { diff --git a/tests/unit/command-boundary/support/host-client.test.ts b/tests/unit/command-boundary/support/host-client.test.ts new file mode 100644 index 0000000..f1f4618 --- /dev/null +++ b/tests/unit/command-boundary/support/host-client.test.ts @@ -0,0 +1,28 @@ +import assert from 'node:assert/strict'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { createServer } from 'node:http'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; +import { invokeLocalHostManager } from '../../../../src/cli/support/host-client.ts'; + +test('local host manager calls use fresh Unix-socket connections after long development operations', async () => { + const root = mkdtempSync(join(tmpdir(), 'treeseed-host-client-')); + const socket = join(root, 'manager.sock'); let requests = 0; + const server = createServer((request, response) => { + request.resume(); request.on('end', () => { + requests += 1; response.setHeader('content-type', 'application/json'); + response.end(JSON.stringify({ ok: true, data: { request: requests }, error: null })); + }); + }); + server.keepAliveTimeout = 1; + await new Promise((resolve) => server.listen(socket, resolve)); + try { + assert.deepEqual(await invokeLocalHostManager({ operation: 'first' }, socket), { request: 1 }); + await new Promise((resolve) => setTimeout(resolve, 10)); + assert.deepEqual(await invokeLocalHostManager({ operation: 'second' }, socket), { request: 2 }); + } finally { + await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); + rmSync(root, { recursive: true, force: true }); + } +});