Testing
A host runs without a CLI, a login or a network when its parts are swapped for fakes. Getting started is a whole test of that kind: a fake CLI in the process, an injected detection, the environment written out, the clocks off and a temporary data folder.
Fakes
| Module | Export | |
|---|---|---|
chat/fake-cli | inProcess(cli) | A spawn that runs a fake CLI in the process. started lists every run. |
chat/fake-claude | fakeClaude | Claude Code's stream-json protocol. |
chat/fake-codex | fakeCodex, fakeCodexWith | Codex's app-server protocol, the second with options. |
host/memory-port | memoryPortPair() | Two ends of a FramePort in one process; a frame arrives as a copy, a moment later. |
outbox/manual-clock | ManualClock | Time for the outbox that moves when you say. |
watch-test-helpers | FakeWatch, EventLog | File changes you raise yourself, and a log of events to wait on. |
providers/accounts/test-accounts | testAccounts, memorySecrets | An account service with fake checks and secrets in a Map. |
fakeClaude answers a message with echo: and its text. A message slow keeps the turn running, so a test can stop it, and crash or crash loudly end the process the way a crash does. fake-claude and fake-codex have the rest, for approvals, questions and background work; a few of those write files or start a process, so read them before you use them.
A run of inProcess has runLater() for what the fake put off, crash(code), and exited. Wait on events rather than on time, and put a limit on every wait, so a test that goes wrong fails rather than hangs:
import { FakeWatch } from '@adecore/agents/watch-test-helpers';
import { settled } from '@adecore/agents/watch-seam';
const seams = new FakeWatch();
let scans = 0;
const refresh = settled(seams, 100, () => {
scans += 1;
});
refresh.nudge();
refresh.nudge();
await seams.settle();
refresh.stop();That scans once. Close what a test opened, the host, the clients, the outbox worker and the watches, and check that it closed: an answered request says nothing about what is still running.
Error codes
A refusal carries a code; branch on it, show the message.
| Code | |
|---|---|
bad-request, unknown-request | The frame, the name or the payload does not fit the protocol. |
chat-not-found | Create or load the chat first. |
chat-unreadable | Its record is from a version this one cannot read. It is left on disk. |
chat-busy | A turn runs; wait, or ask for force. |
request-not-found | The approval, question or queued message is no longer there: read the state again. |
history-expired | The cursor is gone; attach again. |
chat-unsupported | This host or this CLI does not do that. |
invalid-attachments | An attachment the host could not take. |
account-unavailable, account-incompatible, invalid-account | The account is not there, cannot take the chat over, or is not valid. |
secrets-unavailable | No keychain on this machine. |
internal | Something else failed; the host logged it. |
Others name what was not found (subagent-not-found, task-not-found, turn-not-found, item-not-found, bookmark-not-found), too-many-bookmarks, a visual that breaks a limit (visual-invalid, visual-too-large), and a CLI transcript that is missing or not readable (transcript-missing, transcript-format). Context commands refuse in their own format.