fix daemon-not-connected race on startup
The socket-loopback DaemonClient (T-127) raced the UI on first launch: panels queried before the socket finished connecting and cached a "daemon not connected" error, and the Claude pane's spawn gate tripped, leaving an empty terminal. Three fixes in the startup/connection path: - DaemonClient.request() now waits briefly (5s) for an in-flight connection instead of failing instantly, gated on _started so a never-started client still fails fast. start() sets _started synchronously so the gate is armed before the UI builds. - swapIpcServer reuses the live server when the opened project matches the workspace it already serves, instead of tearing it down — the project-open flow fired right as the Claude pane spawned, dropping the connection mid-spawn. - _connect bails if already connected, so start() arming the reconnect loop and swapIpcServer's reconnectAt can't open a second socket (which had been double-delivering events). This whole orchestration had no automated coverage — integration tests stub a FakeDaemonClient. Adds a real wait-then-connect client test. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -131,8 +131,8 @@ void main() {
|
||||
});
|
||||
|
||||
group('DaemonClient — error + lifecycle paths', () {
|
||||
test('request while disconnected returns a not-connected error', () async {
|
||||
// Don't connect — point at a non-existent socket path.
|
||||
test('request while disconnected (never started) returns a not-connected error fast', () async {
|
||||
// Never started → no connect attempt in flight → fail fast (no wait).
|
||||
final bus = DaemonBus();
|
||||
addTearDown(bus.dispose);
|
||||
final client = _build('/tmp/does-not-exist.sock', bus);
|
||||
@@ -142,6 +142,37 @@ void main() {
|
||||
expect(resp.error?.message, contains('not connected'));
|
||||
});
|
||||
|
||||
test('a request issued before connect waits, then sends once connected', () async {
|
||||
// Reproduces the startup race: the UI queries before the socket
|
||||
// finishes connecting. Started (so a connect is in flight) but no
|
||||
// server yet — the request must park, not fail, and send once the
|
||||
// server comes up.
|
||||
final path = await _tmpSocket();
|
||||
final bus = DaemonBus();
|
||||
addTearDown(bus.dispose);
|
||||
final client = _build(path, bus);
|
||||
addTearDown(client.dispose);
|
||||
|
||||
await client.start(); // no server yet → connect fails, reconnect armed
|
||||
expect(client.isConnected, isFalse);
|
||||
|
||||
final respFuture = client.request('ping'); // parks (does not fail)
|
||||
|
||||
// Bring the server up; the reconnect loop connects, waking the request.
|
||||
final daemon = _TestDaemon(path);
|
||||
await daemon.start();
|
||||
addTearDown(daemon.close);
|
||||
|
||||
final line = await daemon.lines.first;
|
||||
expect(line, contains('ping'));
|
||||
final req = IpcMessage.decode(line) as IpcRequest;
|
||||
daemon.send(IpcResponse.ok(id: req.id, data: const {'pong': true}).encode());
|
||||
|
||||
final resp = await respFuture;
|
||||
expect(resp.ok, isTrue);
|
||||
expect(resp.data['pong'], isTrue);
|
||||
});
|
||||
|
||||
test('malformed line is logged and skipped, real lines still work', () async {
|
||||
final path = await _tmpSocket();
|
||||
final daemon = _TestDaemon(path);
|
||||
|
||||
Reference in New Issue
Block a user