mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Each doc verified against current code; stale claims fixed in place. - architecture: handshake/binary-frame/route/module-table corrections - data-model: atomic-write claim narrowed, missing daemon files/fields added - development: db:query removed (SQLite/Drizzle gone), tmux→concurrently+portless, PASEO_HOME split - unistyles: withUnistyles(Icon) is the dominant pattern; new Animated.View+dynamic-styles iOS gotcha - SECURITY: bearer-token auth, DNS-rebinding allowlist semantics, ephemeral phone keypair, wire format - glossary: Project-checkout rename has shipped - providers: claude-acp not built-in, Pi/mock, async isCommandAvailable, interface drift - coding-standards: rewritten as compressed /unslop adaptation - product/release/testing/custom-providers/android/mobile-testing/ad-hoc-daemon-testing/file-icons/design: smaller corrections
166 lines
5.5 KiB
Markdown
166 lines
5.5 KiB
Markdown
# Ad-hoc daemon testing
|
|
|
|
Spin up an isolated in-process daemon test harness without touching the main daemon on port 6767.
|
|
|
|
This is for test code only. Executable daemon processes must start through
|
|
`scripts/supervisor-entrypoint.ts` or `dist/scripts/supervisor-entrypoint.js`;
|
|
do not use `createPaseoDaemon` as a product launch path.
|
|
|
|
## Quick start
|
|
|
|
```typescript
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
import pino from "pino";
|
|
import { createPaseoDaemon } from "./bootstrap.js";
|
|
import { DaemonClient } from "./test-utils/daemon-client.js";
|
|
|
|
const logger = pino({ level: "warn" });
|
|
const paseoHomeRoot = await mkdtemp(path.join(os.tmpdir(), "paseo-test-"));
|
|
const paseoHome = path.join(paseoHomeRoot, ".paseo");
|
|
await mkdir(paseoHome, { recursive: true });
|
|
const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-"));
|
|
|
|
const daemon = await createPaseoDaemon(
|
|
{
|
|
listen: "127.0.0.1:0", // OS picks a free port
|
|
paseoHome,
|
|
corsAllowedOrigins: [],
|
|
hostnames: true,
|
|
mcpEnabled: false,
|
|
staticDir,
|
|
mcpDebug: false,
|
|
agentClients: {},
|
|
agentStoragePath: path.join(paseoHome, "agents"),
|
|
relayEnabled: false,
|
|
relayEndpoint: "relay.paseo.sh:443",
|
|
appBaseUrl: "https://app.paseo.sh",
|
|
// Add custom config here, e.g.:
|
|
// providerOverrides: { ... },
|
|
},
|
|
logger,
|
|
);
|
|
|
|
await daemon.start();
|
|
const target = daemon.getListenTarget();
|
|
const port = target!.type === "tcp" ? target!.port : null;
|
|
|
|
const client = new DaemonClient({
|
|
url: `ws://127.0.0.1:${port}/ws`,
|
|
appVersion: "0.1.70", // see gotcha #1
|
|
});
|
|
await client.connect();
|
|
await client.fetchAgents({ subscribe: { subscriptionId: "test" } });
|
|
|
|
// ... do your testing ...
|
|
|
|
await client.close();
|
|
await daemon.stop();
|
|
await rm(paseoHomeRoot, { recursive: true, force: true });
|
|
await rm(staticDir, { recursive: true, force: true });
|
|
```
|
|
|
|
Run with:
|
|
|
|
```bash
|
|
npx tsx packages/server/src/server/your-script.ts
|
|
```
|
|
|
|
## Using the test helper
|
|
|
|
For simpler cases, `createTestPaseoDaemon` + `DaemonClient` handles temp dirs and port selection:
|
|
|
|
```typescript
|
|
import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.js";
|
|
import { DaemonClient } from "./test-utils/daemon-client.js";
|
|
|
|
const daemon = await createTestPaseoDaemon();
|
|
const client = new DaemonClient({
|
|
url: `ws://127.0.0.1:${daemon.port}/ws`,
|
|
appVersion: "0.1.70",
|
|
});
|
|
await client.connect();
|
|
await client.fetchAgents({ subscribe: { subscriptionId: "test" } });
|
|
|
|
// ... test ...
|
|
|
|
await client.close();
|
|
await daemon.close(); // stops daemon + cleans up temp dirs
|
|
```
|
|
|
|
The test helper does **not** expose `providerOverrides`. In test harnesses, use `createPaseoDaemon` directly when you need it (see quick start above).
|
|
|
|
## Common client methods
|
|
|
|
```typescript
|
|
// Provider discovery
|
|
const snapshot = await client.getProvidersSnapshot({ cwd: "/tmp" });
|
|
const models = await client.listProviderModels("claude");
|
|
const modes = await client.listProviderModes("claude");
|
|
|
|
// Agent lifecycle
|
|
const agent = await client.createAgent({ provider: "claude", cwd: "/tmp" });
|
|
await client.sendMessage(agent.id, "Hello");
|
|
const updated = await client.waitForAgentUpsert(agent.id, (s) => s.status === "idle");
|
|
```
|
|
|
|
## Gotchas
|
|
|
|
### 1. appVersion gates provider visibility
|
|
|
|
The daemon hides non-legacy providers (anything other than claude, codex, opencode) from clients that don't send an `appVersion >= 0.1.45`. The `DaemonClient` sends no version by default, so custom providers like ACP-based ones will be invisible in snapshot responses.
|
|
|
|
Always pass `appVersion`:
|
|
|
|
```typescript
|
|
const client = new DaemonClient({
|
|
url: `ws://127.0.0.1:${port}/ws`,
|
|
appVersion: "0.1.70",
|
|
});
|
|
```
|
|
|
|
### 2. Provider snapshots are async
|
|
|
|
After the daemon starts, providers are probed in the background. The first `getProvidersSnapshot()` call will likely return `status: "loading"` for most providers. Poll until the provider you care about is no longer loading:
|
|
|
|
```typescript
|
|
let snapshot = await client.getProvidersSnapshot({ cwd: "/tmp" });
|
|
for (let i = 0; i < 20; i++) {
|
|
const entry = snapshot.entries.find((e) => e.provider === "gemini");
|
|
if (entry && entry.status !== "loading") break;
|
|
await new Promise((r) => setTimeout(r, 2_000));
|
|
snapshot = await client.getProvidersSnapshot({ cwd: "/tmp" });
|
|
}
|
|
```
|
|
|
|
### 3. fetchAgents is required before most operations
|
|
|
|
Call `client.fetchAgents()` after connecting. The daemon session expects this handshake before it processes other requests — without it, messages like `get_providers_snapshot_request` will silently hang.
|
|
|
|
### 4. listen: "127.0.0.1:0" for port allocation
|
|
|
|
Always use port `0` so the OS picks a free port. Never hardcode a port — it will collide with the main daemon or other test runs.
|
|
|
|
### 5. Script must live inside packages/server
|
|
|
|
The test utilities use relative imports through the TypeScript project. Place your script somewhere under `packages/server/src/` and import from there. Scripts outside the repo will fail with module resolution errors.
|
|
|
|
### 6. Cleanup on failure
|
|
|
|
Wrap your test logic in try/finally to ensure the daemon stops and temp dirs are cleaned up, even if an assertion fails:
|
|
|
|
```typescript
|
|
try {
|
|
// ... test logic ...
|
|
} finally {
|
|
await client.close();
|
|
await daemon.stop().catch(() => undefined);
|
|
await rm(paseoHomeRoot, { recursive: true, force: true });
|
|
}
|
|
```
|
|
|
|
### 7. ACP providers spawn real processes
|
|
|
|
When testing ACP providers (e.g., Gemini with `extends: "acp"`), the daemon will spawn real processes to probe for models and modes. The binary must be installed and on PATH. Probing can take 5-15 seconds depending on the provider.
|