Loading files
// How the wheel's rows are composed: the local row, and the fleet's rows around it.
import { describe, expect, it, vi } from 'vitest'
import { DaemonCableHost, cableEventFor, type CableHostWiring } from './cableHost.js'
import type { FleetMachine, MachineFleet } from './machineFleet.js'
const AGENTS: Array<{ agentId: string; registeredAt: number; active: boolean; terminalAvailable: boolean; engine: string }> = []
vi.mock('../lib/registry.js', () => ({
registry: {
list: () => AGENTS,
active: () => AGENTS.filter((a) => a.active),
advertised: () => AGENTS.filter((a) => a.terminalAvailable),
},
projectDisplayName: (s: { agentId: string }) => s.agentId,
}))
function wiring(over: Partial<CableHostWiring> = {}): CableHostWiring {
return {
machineName: () => 'MacbookPro.local',
machineId: () => 'mine',
computerId: () => 'abc-123',
sendTurn: vi.fn(),
stopTurn: vi.fn(),
answer: vi.fn(),
recent: () => [],
recentAsks: () => [],
log: vi.fn(),
...over,
}
}
function fleetOf(machines: FleetMachine[]): MachineFleet {
return {
list: async () => ({ machines, source: 'backend' as const }),
recentAsks: async () => [],
online: async () => {},
select: async () => {},
release: vi.fn(),
listAgents: async () => [],
sendTurn: vi.fn(), stopTurn: vi.fn(), answer: vi.fn(), updateAgent: vi.fn(),
listModels: async () => [],
recentSummaries: async () => [],
onEvent: () => () => {},
}
}
const REMOTE: FleetMachine = { machineId: 'other', name: 'office-imac', state: 'ready', authMode: 'remote' }
/** The window, open on one tab that holds these panes in this order. */
function onTab(host: DaemonCableHost, agentIds: string[], id = 't1'): void {
host.setSwarms({ active: id, swarms: [{ id, name: 'Tab', agentIds, panes: agentIds.length }] })
host.setDesk(agentIds)
}
describe('DaemonCableHost.listMachines', () => {
it('names the local row after the machine, and marks it local', async () => {
// The row carries the machine's name; what identifies it as the cabled one is the `local` flag, which
// the dial turns into the second line. Two facts, one field each.
const host = new DaemonCableHost(wiring(), fleetOf([REMOTE]))
const { machines } = await host.listMachines()
expect(machines[0]).toMatchObject({ id: 'mine', name: 'MacbookPro.local', local: true, state: 'ready' })
})
it('puts the local row first and keeps the fleet’s rows after it', async () => {
const host = new DaemonCableHost(wiring(), fleetOf([REMOTE]))
const { machines } = await host.listMachines()
expect(machines.map((m) => m.id)).toEqual(['mine', 'other'])
expect(machines[1]).toMatchObject({ name: 'office-imac', local: false })
})
it('never lists this computer twice, and a rename on the account does not touch its label', async () => {
// `GET /api/machines` contains this computer too. Letting that row through would put the same machine
// on the wheel under two names, and the ✓ could only ever mark one of them.
const dupe: FleetMachine = { machineId: 'mine', name: 'renamed-in-the-web-ui', state: 'offline', authMode: 'remote' }
const host = new DaemonCableHost(wiring(), fleetOf([dupe, REMOTE]))
const { machines } = await host.listMachines()
expect(machines.map((m) => m.id)).toEqual(['mine', 'other'])
expect(machines[0]).toMatchObject({ name: 'MacbookPro.local', state: 'ready' })
})
it('shows one row and says why when there is no fleet at all', async () => {
const host = new DaemonCableHost(wiring())
const { machines, source } = await host.listMachines()
expect(source).toBe('signed-out')
expect(machines).toHaveLength(1)
expect(machines[0].local).toBe(true)
})
it('falls back to a placeholder id rather than an empty one', async () => {
// A belt, not a mode: `harness start` refuses to run signed out and resolves the machineId before the
// daemon spawns, so this should never happen. It is guarded because the alternative is silent — an
// empty id renders a row that is tappable and can never be selected.
const host = new DaemonCableHost(wiring({ machineId: () => '' }))
const { machines } = await host.listMachines()
expect(machines[0].id).toBe('cable:abc-123')
expect(host.isLocalSelected()).toBe(true)
})
})
describe('DaemonCableHost.listAgentsFlat', () => {
const set = (rows: Array<{ agentId: string; registeredAt: number; active?: boolean; terminalAvailable?: boolean }>): void => {
AGENTS.length = 0
for (const r of rows) AGENTS.push({ engine: 'claude', active: true, terminalAvailable: true, ...r })
}
it('lists only terminal-available agents — including a dormant engine with a live pane', async () => {
// Two surfaces reading one registry must not disagree about what is on it. A dead agent holding a tile
// on the dial and nowhere else is a tile that cannot be driven and cannot be explained.
set([
{ agentId: 'a', registeredAt: 1 },
{ agentId: 'b', registeredAt: 2, active: false },
{ agentId: 'stale', registeredAt: 3, terminalAvailable: false },
])
const host = new DaemonCableHost(wiring())
expect((await host.listAgentsFlat()).map((a) => a.id)).toEqual(['a', 'b'])
})
it('orders oldest first', async () => {
set([{ agentId: 'new', registeredAt: 30 }, { agentId: 'old', registeredAt: 10 }, { agentId: 'mid', registeredAt: 20 }])
const host = new DaemonCableHost(wiring())
expect((await host.listAgentsFlat()).map((a) => a.id)).toEqual(['old', 'mid', 'new'])
})
it('breaks a tie by id, so the order never falls through to insertion order', async () => {
// Without the tie-break the winner is whichever the Map happens to hold first — which differs between
// daemon runs, so the web, the app and the dial would each show a different order for one registry.
set([{ agentId: 'zz', registeredAt: 5 }, { agentId: 'aa', registeredAt: 5 }])
const host = new DaemonCableHost(wiring())
expect((await host.listAgentsFlat()).map((a) => a.id)).toEqual(['aa', 'zz'])
set([{ agentId: 'aa', registeredAt: 5 }, { agentId: 'zz', registeredAt: 5 }])
expect((await new DaemonCableHost(wiring()).listAgentsFlat()).map((a) => a.id)).toEqual(['aa', 'zz'])
})
})
describe('the cloud lane follows the CABLE, not the selection', () => {
const remoteFleet = () => {
const f = fleetOf([REMOTE])
f.online = vi.fn(async () => {})
f.select = vi.fn(async () => {})
f.release = vi.fn()
return f
}
it('drops the lane the moment the dial goes away', async () => {
// Held on the dial's behalf and used by nothing else here. Keeping it open leaves the account showing
// a device attached to a machine while the dial sits unplugged in a drawer, with that machine's cards
// relayed to a screen that is not there.
const fleet = remoteFleet()
const host = new DaemonCableHost(wiring(), fleet)
await host.selectMachine('other')
host.onDialGone()
expect(fleet.release).toHaveBeenCalledWith(true) // true = now, not after the linger
})
it('drops it even when the dial was on the local machine — cancelling a linger', () => {
const fleet = remoteFleet()
const host = new DaemonCableHost(wiring(), fleet)
host.onDialGone()
expect(fleet.release).toHaveBeenCalledWith(true)
})
it('opens the socket the moment the dial appears, whatever is selected', async () => {
// The whole point: plugged in means online. The wheel's dots are live from that moment, instead of
// waiting for the user to pick a machine that is not this computer.
const fleet = remoteFleet()
const host = new DaemonCableHost(wiring(), fleet)
host.onDialAttached()
await new Promise((r) => setTimeout(r, 0))
expect(fleet.online).toHaveBeenCalled()
})
it('announces the selected machine on attach — including the local one', async () => {
// `DeviceBinding.activeMachineId` is what the web and the mobile app read to say where a dial is.
// Skipping the local machine leaves it naming whatever was selected last, forever.
const fleet = remoteFleet()
const host = new DaemonCableHost(wiring(), fleet)
host.onDialAttached()
await new Promise((r) => setTimeout(r, 0))
expect(fleet.select).toHaveBeenCalledWith('mine')
})
it('announces a remote selection after a replug', async () => {
const fleet = remoteFleet()
const host = new DaemonCableHost(wiring(), fleet)
await host.selectMachine('other')
;(fleet.select as ReturnType<typeof vi.fn>).mockClear()
host.onDialAttached()
await new Promise((r) => setTimeout(r, 0))
expect(fleet.select).toHaveBeenCalledWith('other')
})
it('announces nothing for the placeholder id — it is not a machineId', async () => {
const fleet = remoteFleet()
const host = new DaemonCableHost(wiring({ machineId: () => '' }), fleet)
host.onDialAttached()
await new Promise((r) => setTimeout(r, 0))
expect(fleet.online).toHaveBeenCalled()
expect(fleet.select).not.toHaveBeenCalled()
})
it('survives a lane that will not reopen, instead of rejecting into the greeting', async () => {
const fleet = remoteFleet()
const host = new DaemonCableHost(wiring(), fleet)
await host.selectMachine('other')
;(fleet.select as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('unreachable'))
expect(() => host.onDialAttached()).not.toThrow()
await new Promise((r) => setTimeout(r, 0)) // let the rejection land on its catch
})
})
describe('DaemonCableHost.listAgentsFlat across machines, and the tab the dial gets', () => {
/** A fleet whose remote agent lists are scripted per machine. */
function crossFleet(byMachine: Record<string, Array<{ id: string; name: string }>>, machines = [REMOTE]): MachineFleet {
const fleet = fleetOf(machines)
fleet.listAgents = vi.fn(async (machineId: string) => (byMachine[machineId] ?? []) as never)
fleet.sendTurn = vi.fn()
fleet.stopTurn = vi.fn()
fleet.recentSummaries = vi.fn(async () => [])
return fleet
}
/** Two ticks: the first kicks the background refresh off, the second reads what it wrote. */
async function settled(host: DaemonCableHost): Promise<Awaited<ReturnType<DaemonCableHost['listAgentsFlat']>>> {
await host.listAgentsFlat()
await new Promise((r) => setTimeout(r, 0))
return host.listAgentsFlat()
}
it('puts this computer first and every other machine after it, in wheel order', async () => {
// The order IS the contract: the dial swipes through it and the desktop app's rail reads top to
// bottom in the same order, so the two surfaces can be compared by eye.
AGENTS.length = 0
AGENTS.push({ agentId: 'local-1', registeredAt: 1, active: true, terminalAvailable: true, engine: 'claude' })
const second: FleetMachine = { machineId: 'third', name: 'studio', state: 'ready', authMode: 'remote' }
const host = new DaemonCableHost(
wiring(),
crossFleet({ other: [{ id: 'r1', name: 'api' }], third: [{ id: 'r2', name: 'ui' }] }, [REMOTE, second]),
)
const agents = await settled(host)
expect(agents.map((a) => a.id)).toEqual(['local-1', 'r1', 'r2'])
expect(agents.map((a) => a.machine)).toEqual(['MacbookPro.local', 'office-imac', 'studio'])
expect(agents.map((a) => a.machineId)).toEqual(['mine', 'other', 'third'])
})
it('lists a terminal beside the agents, and says it is one', async () => {
// It used to be left out, on the reading that the dial drives agents and a shell has no turn to
// drive. But the window draws a shell as a tile like any other, so leaving it out gave the dial
// a desk with a hole in it — a pane the carousel could not walk to and could not explain. It is
// listed now, and `engine` is how the tile knows to wear a prompt instead of a product mark and
// to offer no Voice; the same row is reported as `claude` the moment one is typed into it.
AGENTS.length = 0
AGENTS.push({ agentId: 'term-1', registeredAt: 1, active: true, terminalAvailable: true, engine: 'terminal' })
AGENTS.push({ agentId: 'local-1', registeredAt: 2, active: true, terminalAvailable: true, engine: 'claude' })
const host = new DaemonCableHost(wiring(), crossFleet({}, []))
const agents = await settled(host)
expect(agents.map((a) => a.id)).toEqual(['term-1', 'local-1'])
expect(agents.map((a) => a.engine)).toEqual(['terminal', 'claude'])
// …and the overview's number is not the list's length. The carousel carries the shell because it
// is a tile; "N agents · all idle" counts work in flight, which a shell has none of.
expect(host.agentTotal()).toBe(1)
})
it('keeps a machine\'s agents when the backend cannot be asked', async () => {
// The failure this exists for, measured on the real dial: `unknown` means the backend could not be
// reached — MachineListCache.degrade() marks EVERY machine with it in one go — and reading it as
// "the machine is gone" emptied a remote machine off the carousel mid-session, silently, while the
// window went on showing those agents.
AGENTS.length = 0
const machines: FleetMachine[] = [{ ...REMOTE }]
const host = new DaemonCableHost(wiring(), crossFleet({ other: [{ id: 'r1', name: 'api' }] }, machines))
expect((await settled(host)).map((a) => a.id)).toEqual(['r1'])
machines[0].state = 'unknown'
// Well past REMOTE_GRACE_MS: this is not a grace period, it is a different question.
vi.setSystemTime(Date.now() + 10 * 60_000)
expect((await settled(host)).map((a) => a.id)).toEqual(['r1'])
vi.useRealTimers()
})
it('drops them when the machine itself says it is offline, and says so', async () => {
AGENTS.length = 0
const log = vi.fn()
const machines: FleetMachine[] = [{ ...REMOTE }]
const host = new DaemonCableHost(wiring({ log }), crossFleet({ other: [{ id: 'r1', name: 'api' }] }, machines))
expect((await settled(host)).map((a) => a.id)).toEqual(['r1'])
machines[0].state = 'offline'
expect((await settled(host)).map((a) => a.id)).toEqual([])
// An agent leaving the carousel is the one event that cannot be diagnosed from its absence.
expect(log.mock.calls.flat().join('\n')).toContain('left the carousel')
})
it('holds a tile the window has open even when its machine goes offline', async () => {
// The desk is the contract between the two screens. A hole in it makes a swipe skip a tile and an
// agent chosen in the window have nowhere to land.
AGENTS.length = 0
const machines: FleetMachine[] = [{ ...REMOTE }]
const host = new DaemonCableHost(wiring(), crossFleet({ other: [{ id: 'r1', name: 'api' }] }, machines))
await settled(host)
onTab(host, ['r1'])
machines[0].state = 'offline'
const agents = await settled(host)
expect(agents.map((a) => a.id)).toEqual(['r1'])
// Under the name and machine it was last seen with, not a placeholder — and still on the dial's tab.
expect(agents[0]).toMatchObject({ name: 'api', machineId: 'other', machine: 'office-imac' })
expect((await host.listAgents()).map((a) => a.id)).toEqual(['r1'])
expect(host.knows('r1')).toBe(true)
})
it('answers the RAIL\'s order flat, and the dial\'s tab separately, from one snapshot', async () => {
// Two questions, two right answers, and they must not be confused for each other. The dial holds the
// window's open tab, in tile order; ⌘K weighs "the first fifteen agents" against a list the person is
// reading top to bottom in their rail. Serving the tab to ⌘K makes the fifteen it picks impossible to
// predict from the screen — the tile someone happened to open last silently reorders what a typed
// task is even compared against.
AGENTS.length = 0
AGENTS.push({ agentId: 'local-1', registeredAt: 1, active: true, terminalAvailable: true, engine: 'claude' })
AGENTS.push({ agentId: 'local-2', registeredAt: 2, active: true, terminalAvailable: true, engine: 'claude' })
const host = new DaemonCableHost(wiring(), crossFleet({ other: [{ id: 'r1', name: 'api' }] }))
await settled(host)
// Two tiles, in an order the rail does not have: the remote agent first.
onTab(host, ['r1', 'local-1'])
// The rail: this computer's agents in their own order, then the other machine's.
expect((await host.listAgentsFlat()).map((a) => a.id)).toEqual(['local-1', 'local-2', 'r1'])
// The dial: the tab's two tiles in tile order, and local-2 — open nowhere — not at all.
expect((await host.listAgents()).map((a) => a.id)).toEqual(['r1', 'local-1'])
expect(host.agentTotal()).toBe(3)
})
it('sends the dial nothing with no window, and says so in the tab id', async () => {
// A shut app is not an empty tab. Both send zero rows; the dial draws "Run OpenHarness" for one and
// "Nothing on this tab" for the other, and `activeSwarm` is the only thing that tells them apart.
AGENTS.length = 0
for (const agentId of ['a1', 'a2']) {
AGENTS.push({ agentId, registeredAt: 1, active: true, terminalAvailable: true, engine: 'claude' })
}
const host = new DaemonCableHost(wiring())
await settled(host)
expect(await host.listAgents()).toEqual([])
expect(host.activeSwarm()).toBe('')
// The count still travels: the overview prints it without holding a row per agent.
expect(host.agentTotal()).toBe(2)
onTab(host, [], 'fresh')
expect(await host.listAgents()).toEqual([])
expect(host.activeSwarm()).toBe('fresh')
host.setSwarms(null)
host.setDesk([])
expect(host.activeSwarm()).toBe('')
})
it('names the machine, and routes an open, for an agent heard from before it was listed', async () => {
// A remote machine's question can arrive before its agent list has ever been read. The card still has
// to say where it came from, and a tap on it has to open — "ignored open for unknown agent" was a
// question screen nobody could act on.
AGENTS.length = 0
const opened = vi.fn()
const host = new DaemonCableHost(wiring({ opened }), crossFleet({ other: [] }))
await settled(host)
expect(host.describe('r9')).toBeUndefined()
host.noteAgent('other', 'r9')
expect(host.describe('r9')).toEqual({ name: '', engine: '', machine: 'office-imac' })
host.openAgent('r9')
expect(opened).toHaveBeenCalledWith('other', 'r9', undefined)
// A question screen's own open says so, and the window hears it.
host.openAgent('r9', 'question')
expect(opened).toHaveBeenLastCalledWith('other', 'r9', 'question')
})
it('forks a local agent through the daemon and opens the fork in the window', async () => {
AGENTS.length = 0
AGENTS.push({ agentId: 'a1', registeredAt: 1, active: true, terminalAvailable: true, engine: 'claude' })
const opened = vi.fn()
const forkAgent = vi.fn(async () => ({ ok: true as const, agentId: 'a1-fork' }))
const host = new DaemonCableHost(wiring({ opened, forkAgent }))
await settled(host)
expect(await host.forkAgent('a1')).toEqual({ ok: true, agentId: 'a1-fork' })
expect(forkAgent).toHaveBeenCalledWith('a1')
// The new agent lands in the window on its machine — before any list has named it. Through
// `forked` when the wiring has it (beside its source), else the plain `opened`.
expect(opened).toHaveBeenCalledWith('mine', 'a1-fork')
const forked = vi.fn()
const host2 = new DaemonCableHost(wiring({ opened, forked, forkAgent }))
await settled(host2)
await host2.forkAgent('a1')
expect(forked).toHaveBeenCalledWith('mine', 'a1-fork', 'a1')
expect(host.describe('a1-fork')).toMatchObject({ machine: 'MacbookPro.local' })
})
it('forks a remote agent on its own machine, and reports the far end\'s refusal', async () => {
AGENTS.length = 0
const opened = vi.fn()
const fleet = crossFleet({ other: [{ id: 'r1', name: 'api' }] })
fleet.forkAgent = vi.fn(async (_m: string, id: string) => { if (id === 'r1') return 'r1-fork'; throw new Error('r1 is in the middle of a turn') })
const host = new DaemonCableHost(wiring({ opened }), fleet)
await settled(host)
expect(await host.forkAgent('r1')).toEqual({ ok: true, agentId: 'r1-fork' })
expect(fleet.forkAgent).toHaveBeenCalledWith('other', 'r1')
expect(opened).toHaveBeenCalledWith('other', 'r1-fork')
expect(await host.forkAgent('ghost')).toMatchObject({ ok: false, error: 'AGENT_NOT_FOUND' })
})
it('describes an agent it has listed, for a card about one the dial does not hold', async () => {
AGENTS.length = 0
AGENTS.push({ agentId: 'a1', registeredAt: 1, active: true, terminalAvailable: true, engine: 'codex' })
const host = new DaemonCableHost(wiring(), crossFleet({ other: [{ id: 'r1', name: 'api' }] }))
await settled(host)
onTab(host, ['a1'])
// r1 is off the tab — the dial has no row for it — and the frame has to carry who it is.
expect(host.describe('r1')).toEqual({ name: 'api', engine: '', machine: 'office-imac' })
expect(host.describe('a1')).toMatchObject({ name: 'a1', engine: 'codex', machine: 'MacbookPro.local' })
expect(host.describe('ghost')).toBeUndefined()
})
it('does not resurrect an agent that has no tile', async () => {
// The memory is only a warrant for what the window is showing. An agent that was deleted must stay
// deleted.
AGENTS.length = 0
const machines: FleetMachine[] = [{ ...REMOTE }]
const host = new DaemonCableHost(wiring(), crossFleet({ other: [{ id: 'r1', name: 'api' }] }, machines))
await settled(host)
machines[0].state = 'offline'
expect((await settled(host)).map((a) => a.id)).toEqual([])
expect(host.knows('r1')).toBe(false)
})
it('sends a turn to the agent’s OWN machine, not to the selected one', async () => {
// The single worst outcome this feature can produce is a turn delivered to a different computer, so
// the routing is asserted with the wheel deliberately pointed somewhere else.
AGENTS.length = 0
const fleet = crossFleet({ other: [{ id: 'r1', name: 'api' }] })
const host = new DaemonCableHost(wiring(), fleet)
await settled(host)
expect(host.isLocalSelected()).toBe(true) // the wheel never moved
host.sendTurn('r1', 'ship it')
expect(fleet.sendTurn).toHaveBeenCalledWith('other', 'r1', 'ship it')
})
it('reports a dial focus with the machine that owns the agent', async () => {
AGENTS.length = 0
const focused = vi.fn()
const host = new DaemonCableHost(
wiring({ focused }),
crossFleet({ other: [{ id: 'r1', name: 'api' }] }),
)
await settled(host)
host.focus('r1')
expect(focused).toHaveBeenCalledWith('other', 'r1')
})
it('steps focus along the tab, wrapping at both ends, with the same forward the dial uses', async () => {
AGENTS.length = 0
for (const agentId of ['a1', 'a2', 'a3']) {
AGENTS.push({ agentId, registeredAt: 1, active: true, terminalAvailable: true, engine: 'claude' })
}
const focused = vi.fn()
const host = new DaemonCableHost(wiring({ focused }))
await settled(host)
// Tiles in the window's order, not registration order; a3 has no tile and is never stepped onto.
onTab(host, ['a2', 'a1'])
expect(await host.stepFocus('next', 'a2')).toEqual({ machineId: 'mine', agentId: 'a1' })
expect(await host.stepFocus('next', 'a1')).toEqual({ machineId: 'mine', agentId: 'a2' })
expect(await host.stepFocus('previous', 'a2')).toEqual({ machineId: 'mine', agentId: 'a1' })
// Off the tab, or nothing focused: the walk starts at the first (next) or last (previous) tile.
expect(await host.stepFocus('next', 'a3')).toMatchObject({ agentId: 'a2' })
expect(await host.stepFocus('previous', undefined)).toMatchObject({ agentId: 'a1' })
expect(focused.mock.calls.map((c) => c[1])).toEqual(['a1', 'a2', 'a1', 'a2', 'a1'])
})
it('steps onto a remote tile with that machine’s id, and reports an empty desk', async () => {
AGENTS.length = 0
AGENTS.push({ agentId: 'a1', registeredAt: 1, active: true, terminalAvailable: true, engine: 'claude' })
const focused = vi.fn()
const host = new DaemonCableHost(wiring({ focused }), crossFleet({ other: [{ id: 'r1', name: 'api' }] }))
await settled(host)
onTab(host, ['a1', 'r1'])
expect(await host.stepFocus('next', 'a1')).toEqual({ machineId: 'other', agentId: 'r1' })
expect(focused).toHaveBeenLastCalledWith('other', 'r1')
AGENTS.length = 0
const empty = new DaemonCableHost(wiring({ focused }))
await settled(empty)
expect(await empty.stepFocus('next', undefined)).toBe('no_agents')
})
it('reports a focus with no edge, on or off the desk', async () => {
// `edge` is gone with the arcs. It answered "which tile does this replace" for an agent the window
// had none for, which the carousel could reach by walking past the end of the desk; it walks only
// the desk now, so every focus it can raise is about a tile that already exists.
AGENTS.length = 0
for (const agentId of ['a1', 'a2', 'a3']) {
AGENTS.push({ agentId, registeredAt: 1, active: true, terminalAvailable: true, engine: 'claude' })
}
const focused = vi.fn()
const host = new DaemonCableHost(wiring({ focused }))
await settled(host)
onTab(host, ['a2'])
await host.listAgents()
host.focus('a2')
expect(focused).toHaveBeenLastCalledWith(expect.any(String), 'a2')
// Even for one the dial does not hold — a notification can still name it, and the window opening it
// is what puts it on the tab.
host.focus('a3')
expect(focused).toHaveBeenLastCalledWith(expect.any(String), 'a3')
})
it('holds the tab in tile order and nothing else', async () => {
AGENTS.length = 0
for (const agentId of ['a1', 'a2', 'a3', 'a4']) {
AGENTS.push({ agentId, registeredAt: 1, active: true, terminalAvailable: true, engine: 'claude' })
}
const host = new DaemonCableHost(wiring())
await settled(host)
onTab(host, ['a3', 'a1'])
const listed = await host.listAgents()
// Tile order — that is what the dial walks — and the two agents with no tile are not sent at all.
// They used to ride along as "off-ring" for the overview's count and the switcher; the count is a
// number now and the switcher is gone.
expect(listed.map((a) => a.id)).toEqual(['a3', 'a1'])
expect(host.agentTotal()).toBe(4)
// A tile naming an id this daemon has never listed is skipped, not invented.
onTab(host, ['a3', 'ghost'])
expect((await host.listAgents()).map((a) => a.id)).toEqual(['a3'])
})
it('does not guess a machine for an unknown dial focus', async () => {
AGENTS.length = 0
const focused = vi.fn()
const host = new DaemonCableHost(wiring({ focused }), crossFleet({ other: [] }))
await settled(host)
host.focus('missing-agent')
expect(focused).not.toHaveBeenCalled()
})
it('keeps a local agent local even while another machine is selected', async () => {
AGENTS.length = 0
AGENTS.push({ agentId: 'local-1', registeredAt: 1, active: true, terminalAvailable: true, engine: 'claude' })
const sendTurn = vi.fn()
const fleet = crossFleet({ other: [] })
const host = new DaemonCableHost(wiring({ sendTurn }), fleet)
await settled(host)
await host.selectMachine('other')
host.sendTurn('local-1', 'hello')
expect(sendTurn).toHaveBeenCalledWith('local-1', 'hello')
expect(fleet.sendTurn).not.toHaveBeenCalled()
})
it('drops a turn for an agent it has never listed rather than guessing a machine', async () => {
// Falling back to the selected machine here is how a turn lands on the wrong computer. The local
// wiring is the only safe default: it is the machine the cable can vouch for.
AGENTS.length = 0
const sendTurn = vi.fn()
const fleet = crossFleet({ other: [{ id: 'r1', name: 'api' }] })
const host = new DaemonCableHost(wiring({ sendTurn }), fleet)
await settled(host)
await host.selectMachine('other')
host.sendTurn('ghost', 'where does this go?')
expect(fleet.sendTurn).not.toHaveBeenCalled()
expect(sendTurn).toHaveBeenCalledWith('ghost', 'where does this go?')
})
it('holds a machine’s last good list through a failure instead of blanking its tiles', async () => {
AGENTS.length = 0
const fleet = crossFleet({ other: [{ id: 'r1', name: 'api' }] })
const host = new DaemonCableHost(wiring(), fleet)
expect((await settled(host)).map((a) => a.id)).toEqual(['r1'])
;(fleet.listAgents as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('cloud blip'))
// Date.now rather than fake timers: the refresh is paced by wall-clock comparisons, and faking the
// whole timer wheel would also freeze the `setTimeout(0)` this test uses to let the refresh land.
const real = Date.now
const nowSpy = vi.spyOn(Date, 'now').mockImplementation(() => real() + 6_000)
try {
const agents = await settled(host) // past the refresh cadence, well inside the grace period
expect(agents.map((a) => a.id)).toEqual(['r1'])
} finally {
nowSpy.mockRestore()
}
})
it('asks nothing of a machine that is not ready, and shows none of its agents', async () => {
// An offline or unlinked machine costs a 15-second RPC timeout per round to learn nothing.
AGENTS.length = 0
const unlinked: FleetMachine = { machineId: 'other', name: 'office-imac', state: 'needs-link', authMode: 'remote' }
const fleet = crossFleet({ other: [{ id: 'r1', name: 'api' }] }, [unlinked])
const host = new DaemonCableHost(wiring(), fleet)
expect(await settled(host)).toEqual([])
expect(fleet.listAgents).not.toHaveBeenCalled()
})
})
describe('DaemonCableHost.listSwarms', () => {
// The bug this pair exists for: a tab holding only a shell drives no agent, so it named none, and
// the dial's switcher — which had nothing else to go on — dropped it. You could see that tab while
// standing on it and never reach it again once you left (openharness#160).
it('reports a tab that holds only a terminal as having a tile, not as empty', () => {
const host = new DaemonCableHost(wiring(), fleetOf([]))
host.setSwarms({
active: 'work',
swarms: [
{ id: 'work', name: 'harness-1', agentIds: ['a1'], panes: 1 },
{ id: 'shell', name: 'harness-2', agentIds: [], panes: 1 },
{ id: 'fresh', name: 'New Harness', agentIds: [], panes: 0 },
],
})
expect(host.listSwarms()).toEqual({
selected: 'work',
swarms: [
{ id: 'work', name: 'harness-1', agents: 1, panes: 1 },
// No agents and still a place to go: this is the row that used to disappear.
{ id: 'shell', name: 'harness-2', agents: 0, panes: 1 },
// Nothing on it at all — still nothing the dial can offer, which was always the intent.
{ id: 'fresh', name: 'New Harness', agents: 0, panes: 0 },
],
})
})
it('says nothing at all while no window is connected', () => {
const host = new DaemonCableHost(wiring(), fleetOf([]))
host.setSwarms(null)
expect(host.listSwarms()).toEqual({ selected: '', swarms: [] })
})
})
describe('cableEventFor', () => {
it('carries whether the card is a sub-agent\'s turn end, and nothing else about it', () => {
const base = { type: 'commander_event', agentId: 'a1', payload: { kind: 'summary', text: 'body', recap: 'recap' } }
expect(cableEventFor(base)).toEqual({ kind: 'summary', agentId: 'a1', text: 'body', recap: 'recap', subagent: false })
expect(cableEventFor({ ...base, payload: { ...base.payload, subagent: true } })?.subagent).toBe(true)
expect(cableEventFor({ ...base, payload: { ...base.payload, subagent: 'yes' } })?.subagent).toBe(false)
expect(cableEventFor({ ...base, payload: { kind: 'tool', text: 'Bash' } })).toBeNull()
})
})
15ae7b724This file is pinned to the selected release. Download the original to open it in your design tool.