|
| 1 | +# Fresh-eyes Review — factory p5 (pear teardown) |
| 2 | + |
| 3 | +**Verdict: APPROVE WITH MINOR FINDINGS.** The teardown is clean and the core claims verify. No |
| 4 | +hard blockers. Findings below are test-coverage gaps and dead/asymmetric surface, all actionable. |
| 5 | + |
| 6 | +## What I verified (independently re-run, not trusted from self-reflection) |
| 7 | + |
| 8 | +- `npm run typecheck:node` — PASS |
| 9 | +- `npm run typecheck:web` — PASS (FactoryPage rewrite typechecks against the new `FactoryStatus`/`FactoryNodeConfig`) |
| 10 | +- `npx vitest run src/main/ipc-handlers.test.ts` — PASS (10/10); the existing IPC test does **not** reference the deleted `factory-manager`, so the deletion broke nothing. |
| 11 | +- Proof greps over `src bin scripts`: no `factory-sdk`, `factoryManager`, `FactoryManager`, `factory:start`, `factory:stop`, `FactoryLogLine`, `/tmp/factory-run`, `packages/factory-sdk`. Clean. |
| 12 | +- No dangling callers of `pear.factory.start/stop` in the renderer. |
| 13 | +- `bin/pear.mjs`: all imports still used after the `pear factory` branch removal — no dead imports. |
| 14 | +- `@agent-relay/factory@0.1.1` is installed and exports `FactoryConfig`; the `Pick<FactoryConfig, 'capabilities'|'clonePaths'|'dryRun'> & Partial<Pick<…,'workspaceId'|'cloneRoot'>>` type resolves (those fields exist at the FactoryConfig top level). |
| 15 | +- `auth.resolveCloudAuth` / `getAccountWorkspaceId` / `accountWorkspaceReadyRetryOptions` exist with the shape used (`CloudAuth` has `apiUrl` + `accessToken`); `fetch` in the Electron-42 main process is an established pattern (cloud-agent/integrations/broker all use it). |
| 16 | +- `factoryStatusInFlight` in-flight coalescing aligns with the AGENTS.md "coalesce concurrent starts/attaches with keyed in-flight promises" rule. |
| 17 | +- `electron-builder.mcp-resources.yml` diff is a benign regenerated artifact: the removed nested `@agent-relay/harnesses/node_modules/@agent-relay/broker-*` globs are deduped — the top-level `@agent-relay/broker-*` entries remain, so the broker binaries are still bundled. `@agent-relay/fleet/**` + `jiti/**` were added as factory transitive deps. No packaging regression. |
| 18 | + |
| 19 | +## Findings |
| 20 | + |
| 21 | +### 1. [Medium] New factory cloud-status + config-parsing logic has ZERO test coverage |
| 22 | +**File:** `src/main/ipc-handlers.ts` (lines ~82–430) |
| 23 | + |
| 24 | +The teardown added a substantial block of non-trivial, branch-heavy logic with no tests: |
| 25 | +`normalizeNodeConfigInput`, `extractNodeConfig`, `readFactoryNodeConfig`, `saveFactoryNodeConfig`, |
| 26 | +`normalizeIssue`, `normalizeAgent`, `normalizeFactoryCloudStatus`, `readFactoryCloudStatus` |
| 27 | +(endpoint fallback on 404/405/501, 401/403 → `unauthenticated`, non-OK → `unavailable`, payload |
| 28 | +normalization, config-shape detection on save). The existing `ipc-handlers.test.ts` does not touch |
| 29 | +factory, and `npm test` (the `src/main/__tests__/*.test.ts` glob) does not even run the vitest file |
| 30 | +that would. |
| 31 | + |
| 32 | +**Required fix:** Add a vitest spec exercising this surface. The functions are module-private, so test |
| 33 | +through the registered IPC handlers — the existing `ipc-handlers.test.ts` mock already captures |
| 34 | +handlers via the `ipcMain.handle` mock. Required cases: |
| 35 | +- `factory:read-config` / `factory:save-config` round-trip against a temp `factory.config.json` (assert the saved file shape and that a re-read reproduces the draft). |
| 36 | +- `factory:status` with a mocked global `fetch`: (a) all endpoints 404 → `state: 'empty'`; (b) 401 → `state: 'unauthenticated'`, `connected: false`; (c) a 200 payload with `agents`/`issues`/`counters` → normalized output with `state: 'online'`; (d) unauthenticated (`resolveCloudAuth` → null) → `state: 'unauthenticated'`. |
| 37 | +- A duplicate `factory:status` call while one is in flight returns the **same** promise (covers the `factoryStatusInFlight` coalescing — this is the AGENTS.md replay-hardening path and deserves an explicit regression test). |
| 38 | + |
| 39 | +### 2. [Low] Dead `factory:event` push path — status never updates live |
| 40 | +**Files:** `src/preload/index.ts:309`, `src/shared/types/ipc.ts:459` (`FactoryEvent`), `src/renderer/src/components/factory/FactoryPage.tsx:96-98` |
| 41 | + |
| 42 | +Main no longer emits `factory:event` (the emitter was removed with `factoryManager`), but the channel |
| 43 | +is still plumbed through preload, the `FactoryEvent` type, the mock, and `FactoryPage` subscribes via |
| 44 | +`pear.factory.onEvent`. That subscription can never fire. Cloud status is therefore fetched only on |
| 45 | +mount and on the manual Refresh button — there is no polling/auto-refresh. |
| 46 | + |
| 47 | +**Required fix:** Pick one and make it intentional: |
| 48 | +- (a) If a refreshing view is intended, add a polling interval in the `FactoryPage` effect (guard against overlap with the existing in-flight coalescer) — and add a test for it; **or** |
| 49 | +- (b) If a manual read-only snapshot is the spec, delete the now-dead `onEvent`/`FactoryEvent`/`factory:event` plumbing (preload + type + mock + FactoryPage subscription), or leave a one-line comment marking it reserved for Phase 2 so it isn't read as live. |
| 50 | + |
| 51 | +**Required test:** whichever path is chosen, assert the refresh behavior (mock returns a new status on the second `pear.factory.status()` and the UI reflects it after refresh/poll). |
| 52 | + |
| 53 | +### 3. [Low] `read` vs `save` asymmetry for `cloneRoot` / `clonePaths` |
| 54 | +**File:** `src/main/ipc-handlers.ts` — `extractNodeConfig` (reads `source.cloneRoot ?? repos?.cloneRoot`) vs `saveFactoryNodeConfig` (else branch writes top-level `{...raw, ...parsed.config}`) |
| 55 | + |
| 56 | +This repo's `factory.config.json` is the compact single-source shape (operative values under `repos.cloneRoot`/`repos.clonePaths`, no `nodeConfig`/`factoryConfig` wrapper). On read, the editor |
| 57 | +falls back into `repos.*`; on save it writes a **top-level** `cloneRoot`/`clonePaths` and leaves |
| 58 | +`repos.*` untouched. Net: after editing "Clone root" and saving, the file carries both a top-level |
| 59 | +`cloneRoot` (edited) and a stale `repos.cloneRoot`, which can silently diverge. This is *probably* |
| 60 | +correct for the published loader (NodeConfigSchema picks the top-level fields), but the duplication is |
| 61 | +a foot-gun and the read-fallback-then-promote behavior is undocumented. |
| 62 | + |
| 63 | +**Required fix:** Add a brief comment documenting that NodeConfig fields live at the top level, |
| 64 | +distinct from the workspace-level `repos.*`. **Required test:** the round-trip test from Finding 1 |
| 65 | +should assert the saved file shape explicitly against the `repos`-based input so this asymmetry is |
| 66 | +pinned and any future loader-location change fails loudly. |
| 67 | + |
| 68 | +### 4. [Nit] Issue list can emit duplicate React keys |
| 69 | +**File:** `src/main/ipc-handlers.ts` — `normalizeFactoryCloudStatus`; `FactoryPage.tsx:187` (`key={issue.key}`) |
| 70 | + |
| 71 | +`issues` is built by concatenating the `issues` + `inFlight`/`inflight` + `queued` buckets with no |
| 72 | +dedup by `key`. If a speculative cloud payload lists the same issue in two buckets, React warns on |
| 73 | +duplicate keys. |
| 74 | + |
| 75 | +**Required fix:** dedup by `key` (keep first occurrence) before returning `issues` — also the more |
| 76 | +correct behavior and consistent with the AGENTS.md "treat duplicate delivery as normal" guidance. |
| 77 | +**Required test:** covered by adding a duplicate-key issue to the mocked payload in the Finding 1 |
| 78 | +`factory:status` test and asserting the returned `issues` length. |
| 79 | + |
| 80 | +## Summary |
| 81 | + |
| 82 | +Spec coverage is met: the in-Pear daemon model is fully removed (no `FactoryManager`, no spawn, no |
| 83 | +heartbeat/reaper/registry, no `pear factory` passthrough, no `factory:start`/`stop` IPC), config IPC is |
| 84 | +NodeConfig-only, and `FactoryPage` is a read-only cloud view + local NodeConfig editor. Typechecks and |
| 85 | +the existing vitest pass; nothing dangling. The gaps are: (1) untested new parsing/status logic incl. |
| 86 | +the coalescer, (2) a dead `factory:event` path with no auto-refresh, (3) an undocumented read/write |
| 87 | +location asymmetry for `cloneRoot`/`clonePaths`, and (4) a duplicate-React-key nit. All are |
| 88 | +test-and-polish items, not blockers. |
0 commit comments