diff --git a/docs/plans/README.md b/docs/plans/README.md index 1bb0efaa..89a3f501 100644 --- a/docs/plans/README.md +++ b/docs/plans/README.md @@ -14,6 +14,7 @@ This directory is the source of truth for Aiden's implementation plans. The engi | [Designer Mode](designer-mode-plan.md) | Planned | Phase 0 validation has not started in the runtime. | | [Dynamic Model Catalog](dynamic-model-catalog-plan.md) | Implemented | Validated pi.dev overlays, offline `0600` cache hydration, scoped setup refresh, four-hour launch refresh, force refresh, Pi metadata fallback, and Mac/iOS projection ship on pinned Pi 0.80.10. | | [Generation Progress Notes](generation-progress-notes-plan.md) | Planned | No implementation yet. | +| [iOS Remote Caching Strategy](ios-remote-caching-strategy-plan.md) | Planned | Research complete: Workspace `GET /chats` still hydrates every full transcript, iOS Workspace home is not cache-first, connecting overlays hide warm cache, and Bot inbox already is cache-first. Implementation starts with chat summaries plus session/workspace snapshots; collection 304s and model-catalog reuse follow. Do not add a mega `/mobile-bootstrap` unless traces say handshake RTT dominates. | | [Onboarding Authentication and Provider Validation](onboarding-auth-and-provider-validation-plan.md) | Active | Codex onboarding now uses its dedicated auth surface, completion state is main-owned, and hosted/local endpoint readiness validation is under focused verification and fresh review. | | [Performance, Stability, Battery, and Efficiency](performance-stability-efficiency-plan.md) | Planned | Whole-app source audit is complete; implementation starts with instrumentation, durable state, and hard memory bounds. | | [Pi Provider Integration](pi-provider-integration-plan.md) | Partial | Pi built-ins, stores, auth, native routing, custom provider composition, canonical assistant provenance, and voice credential lookup ship; scalable UX and rollout cleanup remain. | diff --git a/docs/plans/ios-remote-caching-strategy-plan.md b/docs/plans/ios-remote-caching-strategy-plan.md new file mode 100644 index 00000000..f67d5cbe --- /dev/null +++ b/docs/plans/ios-remote-caching-strategy-plan.md @@ -0,0 +1,401 @@ +# iOS Remote Caching Strategy Plan + +- Status: Planned +- Created: 2026-08-24 +- Primary surface: Aiden On The Go (iPhone/iPad) talking to Aiden Agent Remote API v1 +- Authority: the paired Mac remains execution, persistence, grant, and mutation owner +- Related: `docs/aiden-remote-api-v1.md`, `docs/plans/aiden-on-the-go-plan.md`, `docs/plans/bot-first-aiden-on-the-go-plan.md`, `docs/plans/performance-stability-efficiency-plan.md`, `docs/testing/missing-test-coverage-audit.md` + +## Outcome + +Aiden On The Go should feel instant on a returning pair: Bots home, Workspace home, and an already-opened chat paint from device-local state, then reconcile with the Mac. First-session pairing can still wait on the network. Mutations stay disabled while disconnected. Cache never outranks grants, revocation, Bot classification, or revision checks. + +This plan is Mac API + iOS client work. It does not move agent execution onto the phone. + +## Research method + +Three parallel codebase research agents inventoried (1) tests, (2) every Remote/IPC API, (3) current iOS cache and launch waterfalls. The requested Deepseek v4 flash 0731 subagent model is not available in this environment; findings were produced with the default Cursor agent model and then triaged against the shipped OpenAPI, router, and Swift clients. + +## 1. Current architecture (what already ships) + +```text +iOS --HTTPS REST + resumable SSE--> Electron main Remote router --> application services + bearer device credential /api/aiden/v1 + URLSession ephemeral Cache-Control: no-store + device-local file caches +``` + +There are two API surfaces on the Mac: + +1. **Electron IPC** (renderer only). Allowlisted in `renderer/preload-channels.ts`, guarded by `main/handlers/ipc-contract.test.ts`. React Query in `renderer/lib/queries.ts` caches providers/chats/bots in the Mac UI. iOS never calls IPC. +2. **Aiden Remote HTTP** at `/api/aiden/v1`. This is the only iOS API. Normative shapes: `protocol/aiden-remote/v1/openapi.json`. Behavior: `docs/aiden-remote-api-v1.md`. Router: `main/services/aiden-remote-router.ts`. + +iOS already has several installation-scoped file caches. They are not one system. + +| Cache | Path / type | Holds | Missing | +| --- | --- | --- | --- | +| `AidenBotCache` | Application Support `RemoteBotCache-v1` | List, details, conversation page, catalog, notice, avatars; A→B→A generation fencing; segment merge | Home still refetches list+inbox on every appear when connected | +| `AidenChatCache` | Application Support `RemoteChatCache-v2` | Per-workspace chat lists, per-chat bodies, stream cursors, attachment images | **Workspace home does not read it** | +| `AidenChatDraftStore` | Application Support | Unsent composer text keyed by `(instanceId, chatId)` | Fine; keep | +| `AidenScheduledTaskCache` | Application Support | Task list + settings | Home fetches schedules live without painting cache first | +| `AidenWorkspaceEnvironmentCache` | Caches dir | File index + documents (8 MiB) | Only after Files/Review is opened | +| `AidenWorkspaceArchiveStore` | UserDefaults | Locally archived workspace IDs | Not a server cache | +| `AidenInstallationStore` + Keychain | Installations + bearer | Pairing restore | Workspaces/server projection not snapshotted for offline chrome | +| URLCache | **none** | `URLSessionConfiguration.ephemeral` | Intentional vs cookies/credentials; do not turn on a shared HTTP cache for bearer traffic | + +Bots home **is** cache-first stale-while-revalidate (`AidenBotsHomeView.load`): activate cache → paint snapshot → parallel `GET /bots` + `GET /bot-conversations` → `mergeAndStore`. Workspace home is not. + +## 2. API catalog (iOS-relevant) + +Base: `/api/aiden/v1`. Auth: `Authorization: Bearer` + `Aiden-Protocol-Version: 1` except `/health` and pairing bootstrap/exchange. Every JSON response is capped at **1 MiB** and sent with `cache-control: no-store`. + +### 2.1 Bootstrap + +| Method | Path | Hot path? | Cache today | Notes | +| --- | --- | --- | --- | --- | +| GET | `/health` | Probe only | No | Minimal `{ ok, protocolVersion }` | +| POST | `/pairing/manual-bootstrap` | Pairing | Never cache | Sealed envelope; setup code never on wire | +| POST | `/pairing/exchange` | Pairing | Never cache credential | Issues device grants + optional `serverCapabilities` | + +### 2.2 Session chrome (every cold start) + +| Method | Path | Cap | Today on iOS connect | Problem | +| --- | --- | --- | --- | --- | +| GET | `/server` | `server:read` | Parallel with workspaces | Small; must revalidate grants. Cache a **last-known projection** for chrome only | +| GET | `/workspaces` | `workspace:read` | Parallel with server | Small registry + `revision`. **Not persisted** on iOS | +| GET | `/workspaces/{id}` | `workspace:read` | Detail | Same DTO | + +### 2.3 Workspace chats (largest load) + +| Method | Path | Cap | Payload | Problem | +| --- | --- | --- | --- | --- | +| GET | `/chats?workspaceId=` | `chat:read` | **Full `Chat` for every regular chat**, including all visible messages (up to 10_000 each, 200k scalars/message, whole response ≤ 1 MiB) | `list()` does `listRegular` then `Promise.all(get)` + `projectAidenRemoteChat`. N disk reads, huge JSON, 413 if the workspace is history-heavy. iOS Workspace home calls `chats()` **without** `workspaceId`, so it hydrates **all** regular chats | +| GET | `/chats/{id}` | `chat:read` (+ `bot:read` if Bot) | Full transcript | Correct for the open thread; also refetched on every detail `load()` even when cache exists | +| POST/PATCH/DELETE | `/chats…` | `chat:write` | Mutations | Keep If-Match / idempotency; do not retry create/turn on TCP loss | + +### 2.4 Bots (already closer to cache-first) + +| Method | Path | Cap | Payload | Notes | +| --- | --- | --- | --- | --- | +| GET | `/bots` | `bot:read` | ≤256 summaries + favorites + revision | Home already caches. Still no `If-None-Match` | +| GET | `/bot-conversations` | `bot:read` | Pages ≤50, 500-scalar previews, 256 KiB inbox cap | **Do not copy Workspace list.** This is the pattern to steal | +| GET | `/bots/{id}` | `bot:read` | Detail including instructions | Cached in Bot snapshot details | +| GET | `/bot-capabilities` | `bot:read` | Catalog + notice | Cached; refetch when editor opens | +| GET | `/bot-favorites` | `bot:read` | ≤20 IDs | Included in list envelope | +| GET | `/bots/{id}/avatar/{assetRevision}` | `bot:read` | 512×512 PNG, `no-store` | **Content-addressed URL.** iOS file-caches by revision. Prefetch visible favorites | +| Other Bot mutations | create/patch/archive/restore/access/avatar PUT | `bot:read`+`bot:write` (+ chat/files as documented) | If-Match + Idempotency-Key | Optimistic UI already used for favorites | + +### 2.5 Models, usage, schedules + +| Method | Path | Cap | Problem | +| --- | --- | --- | --- | +| GET | `/models` | `chat:read` | Full configured catalog, optional PNG artwork base64, truncated ~900 KiB. **Fetched on every chat detail load** and again on Workspace home | +| GET | `/usage?range=` | `server:read` | Aggregates only. Workspace home fetches it **after** chats/tasks/catalog | +| GET | `/scheduled-tasks` (+ item/runs/settings/scripts/mcp-servers) | `schedule:read` | Home fetches full list live; disk cache exists but home does not hydrate first | + +### 2.6 Files, Git, browser + +| Method | Path | Cap | Cache today | +| --- | --- | --- | --- | +| GET | `/workspaces/{id}/files` | `files:read` | Recursive snapshot, max 4000 entries, depth 20. Environment cache after first open | +| GET/PUT | `.../files/{fileId}` | read/write | Documents in environment cache; writes expectedVersion | +| GET | `/bot-conversations/{chatId}/files` | `bot:read`+`files:read` | Same DTOs; Bot-home only | +| Git review/diff/compare/branches/worktrees | `git:read` | Snapshot IDs | Cached with environment; mutations confirmed + operation IDs | +| Workspace browser roots/children/selections | `workspace:browse` | Opaque handles | **Never persist handles across process/Mac**; existing lease tests cover A→B | + +### 2.7 Streams and approvals + +SSE `GET /streams/{id}/events` is not a cache. Persist only `AidenChatCache.ActiveStream` (`streamId`, `turnId`, `lastSequence`) and resume. Approvals are snapshots; do not cache an allow/deny decision. + +### 2.8 Immutable-looking GETs that still send `no-store` + +- `GET /bots/{botId}/avatar/{assetRevision}` +- `GET /chats/{chatId}/attachments/{attachmentId}/content` + +Bytes are already keyed by opaque id + revision. iOS stores them as files. Do **not** enable URLSession HTTP cache for these while the session carries a bearer token. Keep the file cache; add prefetch and LRU (attachment cache already prunes at 96 MiB). + +### 2.9 IPC-only (Mac renderer, not iOS) + +Providers auth, MCP OAuth, secrets, Computer Use, terminals, dictation, local models, Telegram pairing, subagent control, profile share, Artificial Analysis fetch. iOS must not grow equivalents. Mac React Query is a separate performance track (`docs/plans/performance-stability-efficiency-plan.md`); Git 4–5s polling is Mac-only. + +There is **no Remote catalog-push or invalidation channel**. Desktop `chats:changed` / `bots:changed` stay IPC-only. iOS stays SWR + SSE for the open stream. Do not invent a second event bus in v1 of this plan. + +### 2.10 First-paint blockers (coordinator + shells) + +These are client sequencing bugs on top of fat payloads: + +| Step | What happens | Effect | +| --- | --- | --- | +| Connect | `GET /server` and `GET /workspaces` in parallel (`AidenRemoteCoordinator.connect`) | Correct | +| Workspaces shell | Overlay **“Connecting to Aiden Agent…”** until `session?.isConnected` | Cached chats/tasks stay hidden even if disk is warm | +| Bots shell | Overlay until `session?.isConnected` **or** `botAccessNotice != nil` | Same | +| Bots notice | `prepareBotAccess()` can wait on live `GET /bot-capabilities` before the shell paints | Cache-first notice (Phase 1) | +| Chat open | Feature `load()` does `GET /chats/{id}`, then bot/capabilities, then **another** `GET /chats/{id}` plus `GET /models` | Duplicate round trips on a path that already has a disk snapshot | + +Opaque workspace-browser handles expire in **2–10 minutes**. Never persist them as long-lived cache keys; treat them like capability leases (existing A→B tests). + +## 3. Launch / load sequence today + +### Cold start, already paired + +1. `AidenRemoteCoordinator.start` → `connectActiveInstallation` +2. Keychain credential +3. **Parallel** `GET /server` + `GET /workspaces` (no last-known workspaces on screen until this returns) +4. `connectionState = .connected` unblocks area UI +5. If Bots area: cache hydrate → `GET /bots?includeArchived=true` + `GET /bot-conversations` +6. If Workspaces area: `AidenWorkspaceHomeModel.load` **requires connected**, then parallel `GET /chats` (all workspaces, full messages) + `GET /scheduled-tasks` + `GET /models`, **then** `GET /usage` +7. Opening a chat: cache body if any → parallel `GET /chats/{id}` + `GET /models` again → restore SSE + +Bottlenecks, in order: + +1. **Full chat list** (CPU, disk on Mac, JSON parse on iOS, 413 risk) +2. **Serial wait on connect** before area UI (Bots could paint cache during `.connecting`) +3. **Duplicate `/models`** (home + every chat) +4. **Usage after the heavy batch** (extends Workspace spinner; failure handling is OK-ish via `try?`) +5. **Avatar N+1** after Bot list (each raster is a separate GET; file cache helps on warm start) +6. **No workspace registry snapshot**, so the split view is empty until step 3 + +### Warm start / area switch + +Bots: last snapshot stays mounted (plan requirement: keep both stacks). Refresh still hits the network when connected. + +Workspaces: home `load` bails unless connected, so offline does not show last chats unless the in-memory `homeModel` survived. Process death loses Workspace home unless someone opened a chat (chat cache) or Files (environment cache). + +## 4. Non-goals + +- HTTP caching of authenticated JSON via `URLSession` shared cache +- Client-authored delta CRDTs; Mac remains canonical +- Caching credentials, paths, tool args, Pi journals, or SSE token streams +- Serving 304 for a device whose grants changed +- Replacing Bot conversation pages with full chat list semantics +- Computer Use or generic shell on iOS +- SQLite migration unless file envelopes exceed measured budgets (Bot envelope already 4 MiB, chat files 10 MiB) +- A composite `/mobile-bootstrap` mega-GET unless Phase 0 traces show handshake RTT dominating payload size. Prefer additive summaries + a session snapshot over a new kitchen-sink route +- Remote catalog-push / invalidation SSE for lists (stay SWR + open-stream SSE) + +## 5. Design principles + +1. **Paint last-good, then reconcile.** Bots already do this. Workspaces and session chrome must. +2. **Lists are summaries. Detail is a transcript.** Steal `GET /bot-conversations` (preview + activity + pagination). Do not send `messages[]` on Workspace home. +3. **Revisions are for writes and for cheap reads.** Today `If-Match` is mutation-only. Add collection versions so a warm phone can send `If-None-Match` / `Aiden-Collection-Revision` and get 304. +4. **Grants beat cache.** If `/server` capabilities drop `bot:read` or `chat:read`, purge that segment before paint. Revocation already purges; keep it. +5. **Identity isolation.** Every envelope is `(instanceId, deviceId)`. A→B→A activations must not publish. Already true for Bots; copy for Workspace list and models. +6. **Immutable bytes, mutable JSON.** Avatar/attachment file cache keyed by revision/id. JSON snapshots are replace-or-merge, never mixed across Bots vs Workspaces (`RemoteChatCache-v2` exists because v1 could not tell them apart). +7. **Disconnect never retries create/turn.** Cache cannot invent a chat id. +8. **Additive wire only.** Prefer new summary DTOs and optional headers. Do not break shipped TestFlight clients. Old phones still decode full `Chat` on `GET /chats` until a minimum client version is enforced. + +## 6. Target wire additions (additive) + +Keep `/api/aiden/v1`. Bump OpenAPI `x-aiden` annotations and fixture `contractRevision` together with Swift/TS tests. + +### 6.1 Chat summaries (Phase 1, highest leverage) + +Add one of: + +- `GET /chats` with `view=summary` (default for new clients), or +- `GET /chat-summaries` capability-gated + +Summary item (sketch, exact schema in OpenAPI when implementing): + +- `id`, `workspaceId`, optional `botId` (Workspace lists remain regular-only) +- `title`, optional `titlePending` +- `updatedAt`, `createdAt`, `revision` +- bounded `preview` (reuse Bot inbox 500-scalar rules) +- `activityState` (`idle` / `running` / `waiting_for_approval` / …) +- optional `providerId`+`modelId` pair +- **no `messages`** + +Mac implementation: `application.listRegular()` + bounded preview/activity batch, **no** `this.chat(id)` body read. Mirror `bot-inbox-projection.ts` (`listChatMetadata` + `projectBatch`). + +`GET /chats/{id}` stays the full transcript. + +Shipped clients that omit `view=summary` keep today’s full list until a documented sunset. + +### 6.2 Collection revisions (Phase 2) + +Return headers or envelope fields: + +- `Aiden-Workspaces-Revision` +- `Aiden-Chat-List-Revision` (scoped by workspace id or `regular`) +- `Aiden-Bots-Revision` (already have favorites/list revision in JSON; also send as header) +- `Aiden-Models-Revision` (hash of configured provider/model identities + artwork asset ids, **not** secrets) + +`If-None-Match` / `Aiden-If-Collection-Revision` → `304` with empty body. + +**Never 304 when** device grants, `serverCapabilities`, Bot classification rules, or protocol version changed. Compare those first from `/server` or from the request’s authenticated device record. + +### 6.3 Model catalog (Phase 3) + +- Split artwork from JSON: `GET /models` returns providers/models without `dataBase64`; `GET /models/{providerId}/artwork/{hash}` returns PNG with the same nosniff policy as avatars +- Or keep inline artwork but add `Aiden-Models-Revision` + iOS disk cache of the last catalog per instance +- Chat detail must use the session-level catalog, not refetch per chat + +### 6.4 Server + workspaces snapshot (Phase 1, iOS-only is enough if wire stays small) + +`GET /server` and `GET /workspaces` are small. Persist last JSON on iOS keyed by instance/device. Paint during `.connecting`. Revalidate immediately. If `instanceId` mismatches, fail closed (already required). + +## 7. iOS client strategy + +### 7.1 One activation gate + +Extend the Bot cache activation idea to a **session snapshot store**: + +```text +SessionSnapshot + server?: AidenServer // grants + support; never credentials + workspaces?: [AidenWorkspace] + models?: AidenModelCatalog // after artwork split, no huge inline images + savedAt +``` + +Same A→B→A generation token as `AidenBotCache.Activation`. Purge on revocation/remove installation (already in `purgeInstallationData`). + +### 7.2 Workspace home becomes cache-first + +Match Bots: + +1. Hydrate `AidenChatCache.loadChats` **per visible workspace** (not the unscoped all-chats GET) +2. Hydrate `AidenScheduledTaskCache` +3. Paint +4. If connected: summary list + schedules (+ usage in background, never blocking first paint) +5. Merge; do not clear warm rows on refresh failure + +Skeleton only when there is no snapshot (already the Bot rule; `AidenProductShellTests` covers the helper). + +### 7.3 Chat detail + +1. Paint cached `AidenChat` immediately (already) +2. Skip `GET /chats/{id}` when the disk `revision` matches the list/summary revision **and** no stream is live; otherwise fetch **once** (today `AidenChatFeature.load` can hit the same chat twice after bot/capabilities) +3. Do **not** fetch `/models` if the session catalog is current +4. Restore SSE from cached cursor (already) + +Optional later: `GET /chats/{id}?afterMessageId=` or a tail window. Only if traces show transcript parse dominating. Compaction on the Mac should remain the long-term bound. + +### 7.4 Prefetch (after first paint) + +- Favorite Bot avatars not already on disk (cap concurrency at 4, same as Mac `BotCanonicalPhotoCache`) +- Selected workspace summary if the user lands on Bots first (idle prefetch) +- Do not prefetch every transcript +- Do not prefetch Git/file trees until Files/Review is opened (environment cache is enough) + +### 7.5 Connection reuse + +`AidenRemoteCoordinator` already reuses `AidenRemoteClient` per activation key. Keep it. Home and detail must share that client so TLS/session setup is once per Mac. + +### 7.6 Offline + +Mutations stay disabled (existing). Cached Bots/chats/schedules/files are read-only. Copy must stay honest (“showing saved …”). If grants cannot be revalidated and the snapshot is older than a conservative bound, still show it but mark **unverified** rather than blanking the UI. + +## 8. Phased delivery + +```mermaid +flowchart LR + P0["Phase 0: measure"] --> P1["Phase 1: summaries + cache-first Workspace"] + P1 --> P2["Phase 2: collection 304"] + P1 --> P3["Phase 3: models + artwork"] + P2 --> P4["Phase 4: prefetch + connect paint"] + P3 --> P4 + P4 --> P5["Phase 5: optional transcript windowing"] +``` + +### Phase 0 — Measure (no wire break) + +- Privacy-safe client timings: connect start, first cache paint, first network paint, `/chats` bytes, `/models` bytes, avatar GETs, usage +- Mac timings: `listRegular` vs per-chat `get`, JSON serialize ms +- Fixtures: 1 / 20 / 100 chats; 1 / 50 / 200 messages; Bot home with 20 favorites +- Exit: numbers on a physical iPhone, not a guess + +### Phase 1 — Summaries + Workspace cache-first (user-visible) + +Mac: + +- Metadata+preview list path; no full body hydration +- Additive `view=summary` or sibling route +- Keep legacy full list for old clients +- Tests: N chats do not call `application.get` N times; payload has no `messages`; 1 MiB still enforced + +iOS: + +- Persist server+workspaces snapshot +- Workspace home hydrates chat cache + schedule cache before network +- Call summary list scoped by `workspaceId` (stop unscoped `GET /chats`) +- Usage non-blocking +- Hydrate `AidenBotAccessNotice` from the last capabilities snapshot so Bots home is not blocked on a live capabilities GET +- Tests: cache-first, Bot chats excluded, 413/timeout keeps last-good, A→B→A drop, notice cache-first + +### Phase 2 — Collection revisions + +- Headers + 304 +- iOS sends last revision; on 304 keep snapshot `savedAt` bump optional +- Capability change forces 200 +- Tests: grant drop, Bot-aware vs legacy device, workspace revision after rename + +### Phase 3 — Models + +- Session-level catalog; chat `load()` reuses it +- Artwork out of the JSON or content-addressed +- Disk cache of catalog JSON +- Tests: two chats one catalog fetch; artwork hash change invalidates; hidden models stay hidden + +### Phase 4 — Prefetch and connect paint + +- Paint Bots/Workspaces chrome while `connectionState == .connecting` if snapshot exists; do not hide the shell behind “Connecting to Aiden Agent…” +- Prefetch favorite avatars +- Optional: prefetch active workspace summaries after Bot home +- Tests: no mutation while connecting; prefetch cancelled on switch Mac; cached lists visible before `isConnected` + +### Phase 5 — Transcript windowing (only if Phase 0/1 traces still show pain) + +- Tail of N messages + `before` cursor +- Must fail closed with compaction: never imply missing history is complete without a marker +- Higher risk; do not start here + +## 9. Risks and fail-closed rules + +| Risk | Rule | +| --- | --- | +| Stale Bot chat in Workspace list | Keep `RemoteChatCache-v2` namespace; summaries still carry `botId`; Workspace UI filters `isBotChat` | +| 304 after revocation | Revocation path already purges; 304 must run after auth; revoked credential never 304s | +| Summary preview leaking tool args | Reuse Bot inbox: previews from Mac-owned bounded metadata only | +| Old iOS + new Mac | Legacy `GET /chats` remains full bodies until min client version | +| New iOS + old Mac | Client falls back to full list, strips messages for home rows, caches bodies for detail | +| Collection revision across devices | Revisions are Mac-authoritative; two phones can 304 independently | +| Artwork in catalog exploding memory | Phase 3; until then cap decode and do not store base64 in the session snapshot if over budget | +| Files snapshot stale vs expectedVersion | Keep expectedVersion writes; cached document is a hint, save conflicts reconcile from Mac | +| Prefetch on cellular/Tailscale | Cap concurrent GETs; skip prefetch when Low Data Mode / expensive path if reachable via `NWPath` without collecting extra identity | + +## 10. Tests required with the implementation + +Register every new file in the matching `package.json` script (`test:aiden-remote`, iOS target, protocol fixtures). See `docs/testing/missing-test-coverage-audit.md` §9. + +Must add: + +- TS: list-summary does not hydrate bodies; legacy list still does +- TS: collection 304 vs grant change +- TS: models revision / artwork split +- Swift: Workspace cache-first + scoped workspaceId +- Swift: session snapshot A→B→A +- Swift: chat detail does not refetch models when session catalog current +- Shared fixtures for summary DTO in `protocol/aiden-remote/v1/fixtures/` +- Router tests for new query/header +- No models.dev, no real provider calls + +## 11. Suggested implementation order on the Mac vs iOS + +Do **Mac summary list first** (can ship behind `view=summary` with iOS in the same PR). iOS cache-first Workspace home can ship in the same change using summaries when present and a defensive strip of `messages` otherwise, so a mixed-version Mac still cannot freeze the phone. + +Do not wait on 304 or artwork split to fix Workspace home. Those are multipliers after the payload shrinks. + +## 12. Success bar + +On a returning pair (LAN or Tailscale), physical iPhone: + +- Bots home shows last inbox before the first byte of `/bots` (already true if cache warm) +- Workspace home shows last chat rows before `/chats` returns +- Switching Bots ↔ Workspaces does not flash empty then reload +- Opening a previously viewed chat shows transcript before `/chats/{id}` returns +- Workspace home network payload is summaries + previews, not concatenated histories +- Revoke device → next launch cannot show that Mac’s chats/bots +- Grant loss of `bot:read` hides Bots without leftover cache rows + +Exact millisecond budgets come from Phase 0 traces, not from this document. diff --git a/docs/testing/missing-test-coverage-audit.md b/docs/testing/missing-test-coverage-audit.md new file mode 100644 index 00000000..6adfc208 --- /dev/null +++ b/docs/testing/missing-test-coverage-audit.md @@ -0,0 +1,301 @@ +# Missing Test Coverage Audit + +Date: 2026-08-24 +Scope: Aiden Agent Electron app (`main/`, `renderer/`, `scripts/`, `tests/e2e/`, `native/`) and Aiden On The Go (`ios/`) +Method: source-vs-test inventory, `package.json` script expansion against CI (`.github/workflows/ci.yml`), contract tests, and iOS XCTest files. Three parallel codebase research agents were used for coverage, Remote API, and iOS cache/load paths. The requested Deepseek v4 flash 0731 subagent model is not available in this environment; research used the default Cursor agent model. + +This is a findings document, not an implementation. It lists missing cases and CI holes. Do not treat “has a sibling `*.test.ts`” as behavioral coverage. + +## 1. Executive verdict + +Aiden’s suite is large and strong on protocol, pairing, Bot contracts, Remote routing, and focused core services. The gaps that actually matter are not “add a test file next to every module.” They are: + +1. **CI does not run several registered suites.** Hosted `npm test` is `pretest` + `test`. `test:preflight`, `test:command-system`, `test:artificial-analysis`, `test:model-pad`, `test:google-provider`, `test:config-recovery`, `test:concentrate`, and most of `test:coverage` are not on that path. Those tests exist and look maintained, but a regression can merge without them. +2. **Three test files are not registered in any npm script**, so they never run in CI or a normal local `npm test`. +3. **iOS XCTest is compile-only in CI.** Hosted GitHub Actions builds the test bundle for generic iOS hardware and does not execute it. Behavioral iOS coverage depends on signed physical-device runs. +4. **Workspace Remote chat list hydrates every full transcript** (`GET /chats` → `listRegular` then `Promise.all` of `get()` + full `projectAidenRemoteChat`). There is no test that this list stays a summary, stays under the 1 MiB cap with many chats, or that iOS home can render without that payload. +5. **High-risk runtime owners have little or no direct coverage:** `main/services/llm-client.ts`, several Electron handlers (`providers`, `workspaces`, `local-voice`, `dictation`, `telegram`, `subagents`, `computer-use`), iOS SSE/Keychain/App Intents/Live Activities, and almost every settings/chat-pane UI module. + +Priority: close the CI registration holes first, then add contract tests for the chat-list payload and iOS cache-first Workspace path, then fill P0 behavioral gaps around generation, handlers, and iOS networking. + +## 2. Inventory + +| Layer | What exists | How it is supposed to run | +| --- | --- | --- | +| Node/tsx unit and contract | ~400 `*.test.ts` / `*.test.tsx` / `*.test.mjs` files under `main/`, `renderer/`, `scripts/` | `npm test` (`pretest` then `test`) on CI | +| Coverage-oriented sibling scripts | `test:coverage`, `pretest:coverage`, `test:preflight`, `test:command-system`, `test:artificial-analysis`, `test:model-pad`, `test:google-provider`, `test:config-recovery`, `test:scheduled`, `test:concentrate` | **Not** invoked by `.github/workflows/ci.yml` | +| Playwright E2E | 8 specs in `tests/e2e/` | Separate `e2e` CI job via `npm run test:e2e` | +| iOS XCTest | 12 files / **264** `func test…` methods in `ios/AidenOnTheGoTests/` | Physical-device only; CI only `xcodebuild build-for-testing` | +| iOS release policy | Ruby + Node scripts | `pretest` → `test:ios-release` | +| Native Swift (Foundation Models helper) | `npm run test:native` | Separate CI step | +| Native Rust (Computer Use broker) | `test:computer-use:native` | End of `npm test` | +| Native C helpers | worktree remover, bot inbox writer, subagent run-store / file-mutator / shell-runner | Various `pretest:*` scripts | + +CI (`ci.yml` `verify` job) actually runs: `type-check`, `lint`, `test:branding`, `npm test`, `test:native`, iOS `build-for-testing`, then `build`. The `e2e` job runs Playwright. + +`npm test` already pulls in a large pretest graph: Remote, service-boundary, iOS release policy, terminal coverage, onboarding, assistant automations, slash commands, provider-failure, compaction, subagents, and bots. That is real coverage. It is not the full inventory of test files in the repo. + +## 3. Tests that never run in CI + +### 3.1 Unregistered files (exist, no npm script owner) + +These files are not named in any `package.json` script, including nested `npm run` expansion: + +| File | Why it matters | +| --- | --- | +| `main/services/subagents/background-subagent-coordinator-v2.test.ts` | App-lifetime background coordinator. The Subagent Orchestration plan’s next milestone is activating this coordinator. The test exists and is invisible to CI. | +| `main/services/subagents/subagent-runtime-diagnostics.test.ts` | Bounded diagnostic capture, log rotation, secret redaction, `0600` modes. Privacy/safety coverage that is easy to regress. | +| `scripts/subagent-inference-worker-smoke.test.mjs` | Electron worker smoke that the plan treats as a real gate. Not wired into `test:subagents` or `npm test`. | + +Playwright specs under `tests/e2e/*.spec.ts` are **not** listed file-by-file in `package.json`. That is fine: `npm run test:e2e` uses `playwright.config.ts`. Do not “register” them as `tsx --test` files. + +Naive scanners that match `.test.ts` before `.test.tsx` will report the renderer component files as missing. They are registered as `.tsx` in `test`, `test:preflight`, `test:onboarding`, `test:aiden-remote`, `test:bots`, and `test:subagents`. Do not “fix” those paths. A useful CI guard is: every path after `tsx --test` / `node --test` must exist on disk, matching `tsx` before `ts`. + +### 3.2 Suites registered but not on the `npm test` path + +These run only if someone remembers the extra script, or `npm run test:coverage` / `pretest:coverage`: + +| Script | Representative files that CI `npm test` does not execute | +| --- | --- | +| `test:preflight` | `appearance-preview-core.test.ts`, `generation-timeline.test.ts`, `mcp-tool-result.test.ts`, `pi-thinking-disclosure.integration.test.ts`, `reasoning-block.test.tsx`, `reasoning-visibility-control.test.tsx`, `thinking-control.test.tsx`, `agent-steps.test.ts`, `streaming-reveal.test.ts`, `voice-recorder-core.test.ts`, `pill-preload-channels.test.ts`, thinking-contract shared tests | +| `test:command-system` | `native-menu-command-contract.test.ts`, `shortcut-registration-core.test.ts`, `shortcut-transaction-core.test.ts`, `command-palette-*.test.ts`, `keybindings.test.ts` | +| `test:artificial-analysis` | `artificial-analysis-*.test.ts`, `model-data-control.test.ts` | +| `test:model-pad` | `model-pad-layout.test.ts`, `pi-provider-display.test.ts`, `google-provider-migration.test.ts` | +| `test:google-provider` | `anthropic-provider.test.ts`, `google-provider.test.ts`, `provider-config-migration-core.test.ts` | +| `test:config-recovery` | `legacy-pi-credential-migration-core.test.ts`, `mcp-credential-cleanup-core.test.ts`, `mcp-oauth-store-core.test.ts`, `provider-credential-rotation-core.test.ts` | +| `test:scheduled` extras | `schedule-notification.test.ts`, `schedule-script.test.ts`, `scheduled-task-view.test.ts` (other schedule files do run via `test:assistant-automations`) | +| `test:concentrate` | `concentrate-provider.test.ts` | +| `test:coverage` only | `renderer/lib/codex-auth-view-state.test.ts` | + +`test:branding` and `test:native` **are** on CI; they are just not inside `npm test`. That split is intentional. + +`test:bots:coverage` and terminal `--experimental-test-coverage` gates are local/coverage tools, not missing product tests. + +### 3.3 iOS tests are not executed on hosted CI + +`.github/workflows/ci.yml` says so explicitly: physical-device-only XCTest; CI only compiles. A broken iOS assertion will not fail GitHub Actions unless it also fails TypeScript/Node contract tests or the compile. + +Missing CI-executable iOS cases therefore have to be expressed twice when they are protocol-level: once in `main/services/aiden-remote-*.test.ts` / shared fixtures, and once in Swift. Several Bot/Remote cases already do that. Workspace home loading, SSE resume, Keychain, and App Intents do not. + +## 4. What is already well covered (do not duplicate) + +Keep these as regression anchors; new tests should extend them rather than fork new frameworks. + +- **IPC allowlist drift:** `main/handlers/ipc-contract.test.ts` parses production TypeScript so renderer invoke prefixes and notification channels cannot silently diverge. +- **Remote protocol / OpenAPI / fixtures:** `aiden-remote-protocol.test.ts`, `aiden-remote-operation-contract.test.ts`, `aiden-remote-router.test.ts`, `protocol/aiden-remote/v1/`. +- **Pairing, TLS pin, Tailscale ownership, revocation:** dedicated Remote service tests plus iOS pairing tests in `AidenRemotePhase0Tests` / `AidenRemoteClientTests`. +- **Bot classification, one-chat-per-bot, cache A→B→A fencing, avatars, access revisions:** `aiden-remote-bots.test.ts`, `AidenBotCacheTests`, `AidenBotContractTests`, `AidenBotGeneratedAvatarTests`. +- **Chat projection allowlists, 1 MiB cap, 10_000 message cap, Bot vs regular list split:** `aiden-remote-chats.test.ts`. +- **Onboarding, slash commands, subagent v2 contracts, Telegram unit graph, Computer Use native broker.** + +The Bot inbox path is the model for “cache-first + tests.” Workspace chat list is not. + +## 5. Missing cases by area + +Priority: + +- **P0** — can hide data loss, auth/capability bugs, CI-blind regressions, or multi-second iOS loads +- **P1** — user-visible wrong behavior without a current automated net +- **P2** — UI/polish, settings, or deferred product surfaces + +### 5.1 P0 — CI and payload contracts + +| Missing case | Evidence | Suggested test | +| --- | --- | --- | +| Register and run the three orphan subagent files | Files have no `package.json` owner | Add them to `test:subagents` / `pretest:subagents`; keep the Electron smoke behind the existing worker build | +| Put `test:preflight`, `test:command-system`, `test:config-recovery`, and provider suites on a CI path | `ci.yml` only runs `npm test` | Either fold them into `pretest` or add an explicit CI step. Do not leave “coverage” as the only runner | +| `GET /chats` hydrates every chat body | `AidenRemoteChatService.list` maps metadata → `this.chat(id)` → full `projectAidenRemoteChat` | Assert list uses metadata/previews only, or assert a many-chat fixture stays under `AIDEN_REMOTE_MAX_JSON_RESPONSE_BYTES` and does not read every journal. Today the test only checks IDs | +| List vs get payload shape | OpenAPI `listChats` items `$ref` the full `Chat` schema, including `messages` | Contract test that a Workspace home client can decode a summary DTO; fail if messages are required on list | +| iOS Workspace home ignores `AidenChatCache` | `AidenWorkspaceHomeModel.load` calls `client.chats()` with no workspace filter and no cache hydrate | Swift test: cached regular chats paint before network; Bot chats stay excluded; empty cache + 413/timeout shows a retryable error | +| Coordinator connect has no workspace snapshot cache | `AidenRemoteCoordinator.load` always `server()` + `workspaces()` | Cold start with saved workspaces should show the last registry before the round trip, then reconcile | + +### 5.2 P0 — generation, handlers, and persistence owners + +| Missing case | Source without adequate test | Suggested test | +| --- | --- | --- | +| Turn start / tool approval / cancel / remote vs renderer ownership | `llm-client.ts` (~2.7k lines), `chat-generation-owner.ts` is tested, the client is not | Extracted admission/stop/remote-owner cases: Bot policy refuse, Computer Use opt-in, stop does not leak tools, remote device owns the stream | +| Chat JSON durability / partial write | Called out in `performance-stability-efficiency-plan.md`; `chat-store-core.test.ts` covers happy path more than fault injection | Kill during rename, truncated JSON, last-good generation, index rebuild | +| Attachment aggregate bounds | `attachments.ts` / handlers; contract tests exist for DTO shape | Sparse huge file, many near-limit images, concurrent selection, remote staging capacity (`aiden-remote-attachments.ts` has **no sibling test file**) | +| Provider handler auth session | `main/handlers/providers.ts` | IPC parse + main-owned completion; onboarding plan already tracks hosted/local validation | +| Workspace handler permission elevation | `main/handlers/workspaces.ts` | Confirm foreground evidence required; Remote already tests this on HTTP, Electron IPC should match | +| Local voice / dictation main-thread load | `local-voice.ts`, `dictation.ts`, `parakeet.ts` | Do not load the recognizer in unit tests; test that bounds, cancellation, and cache keys fail closed | +| Telegram handler surface | `main/handlers/telegram.ts`; core Telegram files are well tested | Handler-level grant/ceiling so Computer Use/subagents cannot enter via IPC the way the Bot-first plan forbids on Telegram | +| Subagent IPC handler | `main/handlers/subagents.ts` | Control-plane parse already has some core tests; handler registration and document-owner checks need an explicit contract like `bots.contract.test.ts` | +| Chat handler semantics | `main/handlers/chat.ts`, `chats.ts` | `chat:cancel` origin; `chat:approve` decisions; `chats:remove` during generation; `chats:abandonTurn`; `chats:export` size; `chats:copyVisibleHistory` redaction | +| Computer Use / schedule / shortcut / profile IPC | `computer-use.ts`, `scheduled-tasks.ts`, `shortcuts.ts`, `profile.ts` | Enable/disable + session lifecycle; CRUD + run-now vs `schedule-service`; global shortcut conflicts; `profile:shareImage` validation | +| Artificial Analysis fetch gate | `main/handlers/artificial-analysis.ts` | Must not auto-contact models.dev; only explicit user fetch | + +### 5.3 P1 — Aiden Remote HTTP + +Covered well: pairing, router method matrix, Bot CRUD, files/git/schedules happy paths, opaque handles, revocation. + +Missing or thin: + +| Missing case | Notes | +| --- | --- | +| Chat list N+1 and 1 MiB overflow across many chats | See P0 | +| `GET /models` artwork budget under concurrent iOS chat opens | Catalog is truncated at 900 KiB; no test that iOS opening 3 chats does not refetch/decode artwork every time | +| `GET /usage` after Workspace home chats | Home loads chats+tasks+catalog, **then** usage. No test that usage failure does not blank chats | +| SSE `Last-Event-ID` / `after` resume across process death | Stream tests exist; iOS `AidenSSEParser` has no dedicated XCTest file | +| Router HTTP matrix vs OpenAPI | `aiden-remote-router.test.ts` is large but does not assert `POST /scheduled-tasks/preview`, `PATCH /scheduled-tasks/settings`, `POST /scheduled-tasks/{id}/resume`, or the full git route set at HTTP level (service tests exist) | +| Isolated attachment staging store | `aiden-remote-attachments.ts` has no sibling file; TTL, 10-minute expiry, per-device/chat caps, one-use consume | +| IPC contract is inventory, not semantics | `ipc-contract.test.ts` proves channel allowlists. It does not run `chats:remove` during generation, `chat:approve` decisions, telegram handlers, or Artificial Analysis’s explicit-fetch gate | +| Conditional GET / collection revision | Not implemented; when added, tests must prove stale 304 vs capability change (device grant dropped) never serves a 304 | +| `Cache-Control: no-store` on immutable avatar/attachment GETs | Today correct for privacy-by-default; a future immutable cache must still purge on revocation | +| `aiden-remote-service-main.ts`, `aiden-remote-errors.ts`, `aiden-remote-workspace-owners.ts` | Wiring/main adapters; test through the service, but error-code mapping deserves a table test | +| Git mutation disconnect ownership | Workspace environment tests cover idempotency keys; missing: operation still owned after SSE drop (plan requires this) | + +### 5.4 P1 — iOS application + +12 XCTest files, 264 tests. Strong: Remote client routes, Bot contracts, Bot cache, chat cache, avatars, pairing payload, product-shell policy helpers, scheduled-task cache, workspace files/git DTO safety. + +| Missing case | Source | Suggested test | +| --- | --- | --- | +| SSE parser framing, Last-Event-ID, unknown terminal event fail-closed | `Networking/AidenSSEParser.swift` | Isolated parser tests with truncated frames and unknown terminal states | +| Keychain write/read/delete and credential-scope isolation | `Auth/KeychainStore.swift` | Physical-device; at least a mockable seam test that A→B install cannot read A’s credential | +| App Intents are cache-only navigation | `AppIntents/AidenAppIntents.swift` | Assert intents never receive the bearer token and fail when the App Group snapshot is missing | +| Live Activity attributes bounded / no prompt text | `LiveActivities/*` | Decode/encode round-trip; reject oversized status | +| Workspace home waterfall | `AidenWorkspaceHomeModel.load` | Does not hydrate `AidenChatCache`; fetches **all** chats; usage is serial after the batch | +| Workspace registry offline | `AidenRemoteCoordinator` | No disk cache for `workspaces`; airplane mode after a successful session should still show names | +| Model catalog reuse across chats | `AidenChatFeature.load` always `chat()` + `modelCatalog()` | Second chat in the same session should not require a second catalog round trip | +| Draft store vs Bot/Workspace shared key | `AidenChatDraftStore` | BotCacheTests cover some; missing: instance switch, Bot vs Workspace same `chatId` (IDs should be globally unique—assert that) | +| Composer voice | `ComposerVoiceInputController.swift` | Dictation permission denied, empty buffer, no upload of audio | +| Deep link after revocation | `AidenDeepLink.swift` + coordinator purge | Open `aiden-otg://` after credential revoke does not restore purged cache | +| iPad split / Stage Manager | Shell views | Documented as physical-iPad acceptance; add layout-state unit tests for selection reconciliation (`AidenWorkspaceNavigation`) — some exist; missing compact↔split memory | + +### 5.5 P1 — Electron renderer + +Contract tests exist for composer, sidebar, assistant hook, chat-transition, some activity. Almost no tests for the actual panes. + +| Missing case | Source | +| --- | --- | +| Chat pane streaming + stop + approval | `renderer/main/chat-pane.tsx` | +| Markdown / streaming reveal idle RAF | `streaming-markdown-reveal.tsx` (performance plan P1) | +| Model picker closed-state catalog work | `model-picker.tsx` | +| Command palette | `command-palette.tsx` (logic tests exist in `renderer/lib`; the UI does not) | +| Settings sections | entire `renderer/components/settings/` except Remote Access | +| Git commit/push dialogs | `git-*-dialog.tsx` | +| Files / review panels | `files-panel.tsx`, `review-panel.tsx` | +| Bot face studio / avatar | `bot-face-studio.tsx`, `bot-avatar.tsx`; Mac canonical photo cache `bot-canonical-photo-cache.ts` has **no sibling test** | +| Terminal drawer | `terminal-drawer.tsx` | +| Assistant dock chrome | `assistant-dock.tsx` / panel / thread (hook/UI contract tests exist; chrome states do not) | + +Prefer testing extracted lib functions (already the house style) over mounting every settings page. The missing lib tests that hurt: `bot-canonical-photo-cache.ts`, `append-reconciliation.ts`, `queries.ts` refetch-interval policy. + +### 5.6 P1 — Mac React Query / Git polling + +`renderer/lib/queries.ts`: + +- `useGitInfo` refetches every **5s** +- `useGitReview` every **4s** +- `useGitPushCapability` every **5s** + +The performance plan already flags ~6 Git subprocesses per info call. There is no test that hidden/minimized windows disable these intervals, or that `enabled: false` is wired from the review panel closed state in all routes. + +### 5.7 P2 — scripts, packaging, E2E product surfaces + +| Missing case | Notes | +| --- | --- | +| Playwright: Bots mode | E2E covers onboarding, chat shell, attachments, terminal, model picker, assistant scheduled profile, Remote Access enable/health. No Bot create/archive/favorite/chat | +| Playwright: Subagent spawn/approve/kill | No Playwright spec; panel tests live in tsx units | +| Playwright: Telegram, Computer Use UI, compaction UI | Computer Use has packaged/native tests; no window-level E2E | +| Playwright: Remote pairing QR/manual | Lifecycle spec only toggles the setting and hits `/health` | +| Playwright: Git commit/push/review | Service tests exist; no window-level flow | +| `test:e2e:live:lmstudio` | Manual/live; keep it out of default CI | +| Packaging scripts | Many `scripts/*.test.mjs` are on `npm test`; keep it that way when adding scripts | + +## 6. Source modules without a sibling test (guidance, not a todo dump) + +A sibling-file scan finds ~238 production `main/` + `renderer/` files without `foo.test.ts` next to them. That number is inflated by adapters (`*-main.ts`, `*-production.ts`), type-only modules, and UI. Use it as a map, not a quota. + +Highest-value untested (or only indirectly tested) modules: + +**Main runtime** + +- `llm-client.ts`, `parakeet.ts`, `local-models.ts`, `mcp.ts` (if present as orchestrator), `pi-catalog-refresh.ts`, `pi-message-storage.ts`, `chat-cancel.ts`, `chat-deletion-reconciliation.ts` +- `aiden-remote-attachments.ts`, `aiden-remote-service-main.ts` +- `bot-canonical-chat.ts` (logic is covered via inbox/Remote tests; keep a direct unit if the selector changes) +- `main/handlers/{providers,workspaces,local-voice,dictation,telegram,subagents,computer-use,usage,profile,terminal,title-providers,app,artificial-analysis}.ts` + +**Renderer** + +- `renderer/lib/queries.ts`, `bot-canonical-photo-cache.ts`, `workspace-context.tsx` +- `renderer/main/{chat-pane,chat-layout,settings-view,router}.tsx` +- Settings and environment panels listed in §5.5 + +**iOS (no XCTest file at all)** + +- `AidenSSEParser.swift` +- `KeychainStore.swift` +- `AidenAppIntents.swift` +- `AidenRemoteLiveActivityManager.swift` / `AgentRunActivityAttributes.swift` +- `ComposerVoiceInputController.swift` +- `AidenDeepLink.swift` (partial via coordinator tests) +- `AidenWorkspaceShellView.swift` load path (navigation helpers are tested; the home model network mix is not) + +## 7. iOS XCTest map vs product surfaces + +| Surface | Test file | Gap | +| --- | --- | --- | +| Pairing / trust / protocol headers | `AidenRemotePhase0Tests`, `AidenRemoteClientTests` | Good | +| Bot DTO / access / catalog | `AidenBotContractTests` | Good | +| Bot disk cache / A→B→A | `AidenBotCacheTests` | Good; home `load()` orchestration is only indirectly tested | +| Product shell policy | `AidenProductShellTests` | Skeleton/cold-load helpers; not the network/cache merge | +| Chat cache / streams / attachments | `AidenChatTests` | Workspace **home** does not use this cache | +| Files/Git environment cache | `AidenWorkspaceEnvironmentTests` | Good for DTO + cache scope | +| Schedules | `AidenScheduledTaskTests` | 3 tests; thin on preview/run-now errors | +| Avatars / Image Playground | `AidenBotGeneratedAvatarTests`, `AidenBotImagePlaygroundTests` | Good | +| Native integration / shipping guards | `AidenNativeIntegrationTests` | Compile/source policy, not runtime cache | +| Prototype snapshots | `AidenBotPrototypeSnapshotTests` | 1 test; not product-critical | + +## 8. E2E map vs product surfaces + +| Spec | Covers | Does not cover | +| --- | --- | --- | +| `onboarding-lmstudio.spec.ts` | First-run LM Studio | Codex/hosted/local validation matrix | +| `chat-shell-interactions.spec.ts` | Shell chrome | Long-stream, approvals, stop | +| `lmstudio-chat-attachments.spec.ts` (+ live) | Attachments | Remote attachment staging | +| `terminal.spec.ts` | Terminal | Persistence across chat switch | +| `settings-model-picker.spec.ts` | Model picker | Closed-picker catalog cost | +| `assistant-scheduled-profile.spec.ts` | Assistant + schedules + profile | MCP tool loop | +| `remote-access-lifecycle.spec.ts` | Enable Remote, `/health` after window close | Pairing, Bot routes, chat list payload | + +## 9. Recommended delivery order + +1. **CI completeness (no product change)** + Register the three orphan files. Add `test:preflight`, `test:command-system`, `test:config-recovery`, and the provider scripts to CI or to `pretest`. Keep Playwright and native jobs as they are. + +2. **Contract the expensive reads (unblocks iOS cache work)** + Tests for chat-list projection size, metadata-only listing, iOS Workspace cache-first, coordinator workspace snapshot, model-catalog reuse. Details live in `docs/plans/ios-remote-caching-strategy-plan.md`. + +3. **Handler contracts** for providers, workspaces, telegram, subagents, attachments remote staging — copy the `bots.contract.test.ts` / `ipc-contract.test.ts` pattern. + +4. **iOS parser/intent/Live Activity unit tests** that do not need a device, plus keep physical-device tests for Keychain. + +5. **Renderer lib tests** for Git refetch gating and canonical photo cache. + +6. **Playwright** Bot happy path and Remote pairing only after the list/summary API is stable. + +## 10. What not to do + +- Do not add a test file for every `*-main.ts` adapter or every settings row. +- Do not run iOS XCTest on the GitHub-hosted runner without a device; keep compile-for-testing plus physical evidence. +- Do not call models.dev from tests. +- Do not weaken fail-closed decoding tests to make cache-first easier. +- Do not treat `test:coverage` as a substitute for CI. + +## 11. Traceability + +| Claim | Location | +| --- | --- | +| CI test commands | `.github/workflows/ci.yml` | +| `npm test` graph | `package.json` `pretest`, `test` | +| Chat list hydrates bodies | `main/services/aiden-remote-chats.ts` `list()` | +| JSON/message caps | `main/services/aiden-remote-protocol.ts` | +| iOS Bot cache-first | `ios/AidenOnTheGo/Features/Bots/AidenBotsHomeView.swift` `load()` | +| iOS Workspace home network mix | `ios/AidenOnTheGo/Features/Remote/AidenWorkspaceShellView.swift` `AidenWorkspaceHomeModel.load` | +| iOS chat detail + catalog | `ios/AidenOnTheGo/Features/Remote/AidenChatFeature.swift` `load()` | +| Ephemeral URLSession, no HTTP cache | `ios/AidenOnTheGo/Networking/AidenRemoteClient.swift` `makePinnedSession` | +| Global `Cache-Control: no-store` | `main/services/aiden-remote-router.ts` `responseHeaders()` | +| Unregistered subagent tests | files in §3.1 vs `package.json` | +| Git poll intervals | `renderer/lib/queries.ts` |