---
issue: 297
issue_title: "Add composition-root test coverage for pi-permission-system (makeFakePi harness + backfill)"
---

# Retro: #297 — Add composition-root test coverage for pi-permission-system

## Stage: Planning (2026-06-01T16:55:17Z)

### Session summary

Produced a numbered TDD plan to build a `makeFakePi()` harness in `test/helpers/` and backfill six composition-root wiring tests against the real `piPermissionSystemExtension(pi)` factory.
The plan covers the [#296] regression class (registry sharing), handler-registration completeness, shutdown teardown, service/gate formatter-registry sharing, `ready`-after-publish ordering, and a characterization of the suspected multi-instance global-state bug, then a final step consolidating the existing inline `createToolCallHarness` onto the new harness.

### Observations

- Discovered an existing inline `createToolCallHarness` in `test/permission-system.test.ts` (≈line 110) that already runs the real factory with a hand-rolled fake `pi` — but with a **no-op** event bus (not `createEventBus()`), a `Record` of handlers (not an inspectable map), and no `fire()` driver.
  `makeFakePi()` is its generalization; user chose to build standalone first, then fold consolidation into this plan as a final step.
- Key correction carried into the plan: the issue pseudocode keys the subagent registry and the `subagents:child:session-created` payload by `sessionDir`, but the current code (post [#221] / [#296]) keys by `sessionId`.
  `isSubagentExecutionContext` checks `registry.has(ctx.sessionManager.getSessionId())`.
  Tests must use `sessionId`.
- The factory calls `getAgentDir()` internally (via `createExtensionRuntime()` with no `agentDir` option), so every composition-root test must `vi.stubEnv("PI_CODING_AGENT_DIR", <tmpdir>)` and clean **both** `Symbol.for()` global slots (`:service` and `:subagent-registry`) in `afterEach`, or factory runs leak across tests.
  The registry slot has no public unpublish accessor by design, so tests delete it directly (the `subagent-registry.test.ts` pattern).
- User decision: target 6 (suspected latent bug where a child's `session_shutdown` unpublishes the parent's global service) is **characterize-only** — assert current behavior, use `test.fails` for the desired behavior if confirmed, and file a separate fix issue.
- `pnpm exec markdownlint-cli2` is not installed in the workspace; a `rumdl fmt` pre-commit hook handles markdown formatting and passed on commit.
- Next stage is `/tdd-plan` — the plan is structured as red→green→commit cycles.

## Stage: Implementation — TDD (2026-06-01T17:14:15Z)

### Session summary

Completed all 7 planned TDD cycles: built the `makeFakePi()` harness and the six composition-root wiring tests (handler-registration completeness, subagent-registry sharing, shutdown teardown, service/gate formatter sharing, `ready`-after-publish ordering, multi-instance characterization), then consolidated the inline `createToolCallHarness` onto `makeFakePi`.
Test count went from 1662 to 1669 (`+6` passing `+1` expected-fail); the full suite is green and `make-fake-pi.ts` now backs `permission-system.test.ts` as well.

### Observations

- Target 1 (registry sharing) could not use a bare "not blocked" assertion: the forwarding path polls for a parent response with a 10-minute timeout (`PERMISSION_FORWARDING_TIMEOUT_MS`), so a no-response forward times out to a block.
  Implemented a real round-trip: fire the child `tool_call` without awaiting, poll the parent's `requests/` dir for the child's request file, write an approval response, then await.
  This both proves the shared registry (the child detected itself as a subagent and entered forwarding) and completes in well under a second.
- Target 4 (formatter sharing) avoided the `mcp` branch of `formatAskPrompt` (which needs a resolved `result.target`) by registering a formatter for a plain extension tool name (`demo`) and asserting the formatter's marker string appears in the captured `ui.select` title.
  The preview is embedded into the ask message, which becomes the first line of the `ui.select` title.
- Target 6 confirmed the suspected latent bug: a child instance's `session_shutdown` calls `unpublishPermissionsService()`, which deletes the single global service slot, leaving a still-live parent with `getPermissionsService() === undefined`.
  Documented with a passing characterization `it` plus an `it.fails` for the desired behavior, and filed follow-up fix issue #302 (the fix is intentionally out of scope here).
- Consolidation (step 7) was low-risk: `.handlers` was only used in three internal spots of the 2585-line file (`createToolCallHarness`, `cleanup`, `runToolCall`) plus one stray `harness.handlers.session_shutdown` at line ~2320 that the first grep missed; switching the harness to store a `FakePi` and drive handlers via `pi.fire(...)` let the `MockHandler` type be removed.
- Pre-completion reviewer: WARN — one non-blocking finding (the `package-pi-permission-system` skill's Testing section did not list `make-fake-pi.ts`).
  Addressed in a follow-up `docs:` commit that documents the harness and the required global-slot/env cleanup.

## Stage: Final Retrospective (2026-06-01T17:25:56Z)

### Session summary

A single session carried issue #297 cleanly through all four stages: plan → TDD (7 cycles) → ship → retro.
The work delivered the `makeFakePi()` composition-root harness, six wiring tests, and a consolidation of the inline `createToolCallHarness`, confirmed a latent multi-instance global-service bug (filed as #302), and shipped green CI with the issue closed and no release-please bump (test-only commits).

### Observations

#### What went well

- Read-before-write discipline prevented rework on the two trickiest tests.
  Reading `forwarded-permissions/polling.ts` first surfaced the 10-minute `PERMISSION_FORWARDING_TIMEOUT_MS`, which forced a real fire-without-await → poll `requests/` → write response round-trip for target 1 instead of a naive "not blocked" assertion that would have hung.
  Reading `permission-prompts.ts` revealed the `mcp` branch of `formatAskPrompt` needs a resolved `result.target`, so target 4 used a plain extension tool name (`demo`) instead.
- Incremental verification: `pnpm run check` and the affected test file ran after every one of the 7 TDD steps, not just at the end.
  No feedback-loop gap.
- The forwarding round-trip pattern (fire the child `tool_call` without awaiting, poll the parent `requests/` dir, write an approval response, then await) is a novel, reusable technique for exercising the file-based permission-forwarding IPC without hitting its long timeout.
- Target 6's confirm-and-defer flow was clean: a passing characterization `it` documents current behavior, an `it.fails` documents the desired behavior and flips when fixed, and #302 carries the fix out of scope.

#### What caused friction (agent side)

- `missing-context` — during the step-7 consolidation, the enumeration grep for `.handlers` usages was piped through `head -40`, which truncated output and hid a stray `harness.handlers.session_shutdown` at line ~2320 of the 2585-line `permission-system.test.ts`.
  Impact: one failing-test iteration, caught immediately by running the affected test file; ~1 extra tool cycle, no follow-up commit.
  The existing testing-skill "grep all call sites before removal" rules were followed in spirit — the slip was truncating the grep output, an execution detail not worth a durable rule.

#### What caused friction (user side)

- None.
  User involvement was strategic and minimal: two `ask_user` decisions during planning (consolidation scope, and characterize-vs-fix for target 6) set the direction for the whole session, and the "did we find any bugs?"
  check between ship and retro confirmed the #302 hand-off landed.

### Diagnostic details

- **Model-performance correlation** — the `pre-completion-reviewer` subagent ran on `anthropic/claude-sonnet-4-6` (its frontmatter default), an appropriate fit for judgment-heavy review work; it correctly surfaced the one documentation-staleness WARN.
- **Escalation-delay tracking** — no `rabbit-hole` friction; the single test failure resolved in one iteration.
- **Feedback-loop gap analysis** — verification ran incrementally after each TDD step; no gap.
- **Unused-tool detection** — `colgrep` was barely used during implementation, but exact-symbol `grep`/`Read` were the right tools here (the relevant symbols were already known), so no missed-tool finding.

### Changes made

1. Added the file-based forwarding round-trip test pattern (fire-without-await → poll `requests/` → write `responses/<id>.json` → await) to the Testing section of `.pi/skills/package-pi-permission-system/SKILL.md`, to help the #302 follow-up write composition-root forwarding tests without hitting the 10-minute timeout.
2. Appended this Final Retrospective stage entry to `packages/pi-permission-system/docs/retro/0297-composition-root-test-coverage.md`.

[#221]: https://github.com/gotgenes/pi-packages/issues/221
[#296]: https://github.com/gotgenes/pi-packages/issues/296
