@gotgenes/pi-subagents 6.8.0 → 6.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [6.8.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.8.0...pi-subagents-v6.8.1) (2026-05-21)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* plan remove vi.fn() cast smell from test helpers ([#123](https://github.com/gotgenes/pi-packages/issues/123)) ([d0e33b3](https://github.com/gotgenes/pi-packages/commit/d0e33b39cf419bb03a2d69c39992600752ce8517))
|
|
14
|
+
* **retro:** add retro notes for issue [#111](https://github.com/gotgenes/pi-packages/issues/111) ([37eea32](https://github.com/gotgenes/pi-packages/commit/37eea32b32135b792c6c94933267c3e5a5f2cd7b))
|
|
15
|
+
|
|
8
16
|
## [6.8.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.7.0...pi-subagents-v6.8.0) (2026-05-21)
|
|
9
17
|
|
|
10
18
|
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 123
|
|
3
|
+
issue_title: "refactor(pi-subagents): remove vi.fn() cast smell from test helpers"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Remove vi.fn() cast smell from test helpers
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
Several test files construct mock objects typed to narrow interfaces (`AgentManagerLike`, `LifecycleRuntime`, `LifecycleManager`, `ToolStartRuntime`).
|
|
11
|
+
Because the returned objects are typed to the interface — not to Vitest's mock types — tests that need to configure individual method stubs are forced to cast:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
(deps.manager.abort as ReturnType<typeof vi.fn>).mockReturnValue(false);
|
|
15
|
+
(deps.manager.getRecord as ReturnType<typeof vi.fn>).mockReturnValue(record);
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This silences TypeScript without constraining the call's return type — if `getRecord`'s return type changes, the cast won't catch it.
|
|
19
|
+
Nine occurrences exist across three test files.
|
|
20
|
+
|
|
21
|
+
## Goals
|
|
22
|
+
|
|
23
|
+
- Eliminate all `as ReturnType<typeof vi.fn>` casts from the test suite.
|
|
24
|
+
- Preserve type safety: mock configuration calls should be checked against the real method signatures.
|
|
25
|
+
- Keep the change minimal — this is a test hygiene fix, not a structural redesign.
|
|
26
|
+
|
|
27
|
+
## Non-Goals
|
|
28
|
+
|
|
29
|
+
- Changing `AgentManagerLike`, `LifecycleRuntime`, `LifecycleManager`, `ToolStartRuntime`, or any production code.
|
|
30
|
+
- Restructuring test layout or merging describe blocks.
|
|
31
|
+
|
|
32
|
+
## Background
|
|
33
|
+
|
|
34
|
+
The cast pattern was noted during #111 implementation and preserved to keep scope tight.
|
|
35
|
+
Issue #111 (split `AgentRecord` lifecycle state) is now closed and implemented.
|
|
36
|
+
|
|
37
|
+
### Affected files
|
|
38
|
+
|
|
39
|
+
| File | Occurrences | Interface |
|
|
40
|
+
| ---------------------------------- | ----------- | -------------------------------------- |
|
|
41
|
+
| `test/service-adapter.test.ts` | 5 | `AgentManagerLike` |
|
|
42
|
+
| `test/handlers/lifecycle.test.ts` | 2 | `LifecycleRuntime`, `LifecycleManager` |
|
|
43
|
+
| `test/handlers/tool-start.test.ts` | 2 | `ToolStartRuntime` |
|
|
44
|
+
|
|
45
|
+
### Cast sites by file
|
|
46
|
+
|
|
47
|
+
`service-adapter.test.ts` — the "steer, abort, waitForAll, hasRunning" block's `createDeps` returns `AdapterDeps` directly.
|
|
48
|
+
Five casts reconfigure `getRecord`, `abort`, or `queueSteer` after construction:
|
|
49
|
+
|
|
50
|
+
1. `(deps.manager.abort as ReturnType<typeof vi.fn>).mockReturnValue(false)`
|
|
51
|
+
2. `(deps.manager.getRecord as ReturnType<typeof vi.fn>).mockReturnValue({...})` (×4)
|
|
52
|
+
|
|
53
|
+
`lifecycle.test.ts` — mock objects are assigned to `let` variables in `beforeEach`, typed to `LifecycleRuntime` and `LifecycleManager`.
|
|
54
|
+
Two casts reconfigure methods to track call order:
|
|
55
|
+
|
|
56
|
+
1. `(runtime.setSessionContext as ReturnType<typeof vi.fn>).mockImplementation(...)`
|
|
57
|
+
2. `(manager.clearCompleted as ReturnType<typeof vi.fn>).mockImplementation(...)`
|
|
58
|
+
|
|
59
|
+
Note: the same file already uses `vi.mocked()` in the shutdown-order test — both patterns coexist, which is itself a consistency smell.
|
|
60
|
+
|
|
61
|
+
`tool-start.test.ts` — mock object assigned to a `let` variable typed to `ToolStartRuntime`.
|
|
62
|
+
Two casts reconfigure methods to track call order:
|
|
63
|
+
|
|
64
|
+
1. `(runtime.setUICtx as ReturnType<typeof vi.fn>).mockImplementation(...)`
|
|
65
|
+
2. `(runtime.onTurnStart as ReturnType<typeof vi.fn>).mockImplementation(...)`
|
|
66
|
+
|
|
67
|
+
### Approach: named-variable extraction
|
|
68
|
+
|
|
69
|
+
Extract individual `vi.fn()` stubs into named variables.
|
|
70
|
+
This is the approach the issue recommends and it aligns with the testing skill's guidance on extractable stubs.
|
|
71
|
+
|
|
72
|
+
The alternative — `vi.mocked()` — is already used in `lifecycle.test.ts` for the shutdown-order test and works for hand-built mocks, but is semantically less clean: `vi.mocked()` asserts that a value is already a mock, which is true here but opaque to readers.
|
|
73
|
+
Named variables make the mock-ness explicit at the construction site.
|
|
74
|
+
|
|
75
|
+
For `lifecycle.test.ts`, the named-variable approach also eliminates the inconsistency between the two ordering tests — one currently uses `vi.mocked()` and the other uses casts.
|
|
76
|
+
After this change both will use named stubs.
|
|
77
|
+
|
|
78
|
+
## Design Overview
|
|
79
|
+
|
|
80
|
+
### service-adapter.test.ts
|
|
81
|
+
|
|
82
|
+
Refactor the "steer, abort, waitForAll, hasRunning" block's `createDeps` to return named stubs:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
function createDeps(overrides: Partial<AdapterDeps> = {}) {
|
|
86
|
+
const mockGetRecord = vi.fn<AgentManagerLike["getRecord"]>();
|
|
87
|
+
const mockAbort = vi.fn<AgentManagerLike["abort"]>(() => true);
|
|
88
|
+
const mockQueueSteer = vi.fn<AgentManagerLike["queueSteer"]>(() => true);
|
|
89
|
+
|
|
90
|
+
const deps: AdapterDeps = {
|
|
91
|
+
manager: {
|
|
92
|
+
spawn: vi.fn(() => "id"),
|
|
93
|
+
getRecord: mockGetRecord,
|
|
94
|
+
listAgents: vi.fn(() => []),
|
|
95
|
+
abort: mockAbort,
|
|
96
|
+
waitForAll: vi.fn(async () => {}),
|
|
97
|
+
hasRunning: vi.fn(() => true),
|
|
98
|
+
queueSteer: mockQueueSteer,
|
|
99
|
+
},
|
|
100
|
+
resolveModel: vi.fn(),
|
|
101
|
+
getCtx: () => ({ pi: {}, ctx: {} }),
|
|
102
|
+
getModelRegistry: () => ({ find: () => null, getAll: () => [] }),
|
|
103
|
+
...overrides,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return { deps, mockGetRecord, mockAbort, mockQueueSteer };
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Callers destructure what they need:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
const { deps, mockAbort } = createDeps();
|
|
114
|
+
mockAbort.mockReturnValue(false); // ← type-checked, no cast
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### lifecycle.test.ts
|
|
118
|
+
|
|
119
|
+
Promote the `beforeEach`-scoped `runtime` and `manager` mock construction to use named stubs.
|
|
120
|
+
The stubs that need reconfiguration (`setSessionContext`, `clearCompleted`) become named `let` variables alongside the existing `runtime`/`manager` lets, reset in `beforeEach`:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
let mockSetSessionContext: MockInstance<LifecycleRuntime["setSessionContext"]>;
|
|
124
|
+
let mockClearCompleted: MockInstance<LifecycleManager["clearCompleted"]>;
|
|
125
|
+
// ...assigned in beforeEach when building runtime/manager
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Also convert the shutdown-order test's `vi.mocked()` calls to the same pattern for consistency — `unpublishService`, `clearSessionContext`, `abortAll`, `disposeNotifications`, `dispose` all become named stubs.
|
|
129
|
+
|
|
130
|
+
### tool-start.test.ts
|
|
131
|
+
|
|
132
|
+
Same pattern: promote `setUICtx` and `onTurnStart` to named `let` variables:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
let mockSetUICtx: MockInstance<ToolStartRuntime["setUICtx"]>;
|
|
136
|
+
let mockOnTurnStart: MockInstance<ToolStartRuntime["onTurnStart"]>;
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Module-Level Changes
|
|
140
|
+
|
|
141
|
+
| File | Change |
|
|
142
|
+
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
143
|
+
| `test/service-adapter.test.ts` | Refactor `createDeps` in the "steer, abort, waitForAll, hasRunning" block to return named mock stubs alongside `deps`. Update all 5 cast sites to use named stubs. |
|
|
144
|
+
| `test/handlers/lifecycle.test.ts` | Extract `mockSetSessionContext`, `mockClearCompleted`, `mockAbortAll`, `mockDispose`, `mockClearSessionContext` as named `let` variables. Replace 2 casts and 5 `vi.mocked()` calls with named stubs. |
|
|
145
|
+
| `test/handlers/tool-start.test.ts` | Extract `mockSetUICtx` and `mockOnTurnStart` as named `let` variables. Replace 2 casts with named stubs. |
|
|
146
|
+
|
|
147
|
+
No production files are changed.
|
|
148
|
+
|
|
149
|
+
## Test Impact Analysis
|
|
150
|
+
|
|
151
|
+
1. No new tests are added — this is a refactoring of existing test infrastructure.
|
|
152
|
+
2. No tests become redundant — every existing assertion stays.
|
|
153
|
+
3. All existing tests must pass unchanged; only the mock-wiring changes.
|
|
154
|
+
|
|
155
|
+
## TDD Order
|
|
156
|
+
|
|
157
|
+
1. **Commit:** Refactor `createDeps` in `service-adapter.test.ts` to return named stubs; update all 5 cast sites.
|
|
158
|
+
All tests pass before and after.
|
|
159
|
+
Commit: `test: remove vi.fn() cast smell from service-adapter tests (#123)`
|
|
160
|
+
2. **Commit:** Extract named stubs in `lifecycle.test.ts`; replace 2 casts and 5 `vi.mocked()` calls.
|
|
161
|
+
All tests pass.
|
|
162
|
+
Commit: `test: remove vi.fn() cast smell from lifecycle tests (#123)`
|
|
163
|
+
3. **Commit:** Extract named stubs in `tool-start.test.ts`; replace 2 casts.
|
|
164
|
+
All tests pass.
|
|
165
|
+
Commit: `test: remove vi.fn() cast smell from tool-start tests (#123)`
|
|
166
|
+
|
|
167
|
+
Each step is an independent file — order doesn't matter, but one-file-per-commit keeps diffs reviewable.
|
|
168
|
+
|
|
169
|
+
## Risks and Mitigations
|
|
170
|
+
|
|
171
|
+
| Risk | Mitigation |
|
|
172
|
+
| ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
173
|
+
| Overrides via `...overrides` in `service-adapter.test.ts` could replace a manager method, leaving the named stub disconnected | Only `manager`-level overrides are spread; individual method overrides aren't used in this block. |
|
|
174
|
+
| Named stubs add return-surface to helpers | Each helper is test-local and the extra names are self-documenting. The alternative (casting) is worse. |
|
|
175
|
+
| Converting `vi.mocked()` in `lifecycle.test.ts` shutdown test expands scope slightly beyond the cast pattern | Worth it for consistency — mixing `vi.mocked()` and named stubs in the same file is a different smell. |
|
|
176
|
+
|
|
177
|
+
## Open Questions
|
|
178
|
+
|
|
179
|
+
None — the issue is fully scoped and the approach is established in the codebase.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 111
|
|
3
|
+
issue_title: "refactor(pi-subagents): split AgentRecord lifecycle state into phase-specific objects"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #111 — split AgentRecord lifecycle state into phase-specific objects
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-22T01:50:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned and implemented the `AgentRecord` lifecycle split across 12 TDD cycles plus doc updates, released as `pi-subagents-v6.8.0`.
|
|
13
|
+
Three new phase-specific collaborators (`ExecutionState`, `WorktreeState`, `NotificationState`) replace 9 post-construction mutable fields.
|
|
14
|
+
`pendingSteers` moved to a `Map` on `AgentManager`; stats (`toolUses`, `lifetimeUsage`, `compactionCount`) encapsulated behind mutation methods with read-only getters.
|
|
15
|
+
`AgentRecordInit` trimmed from 19 optional fields to 4.
|
|
16
|
+
|
|
17
|
+
### Observations
|
|
18
|
+
|
|
19
|
+
#### What went well
|
|
20
|
+
|
|
21
|
+
- **Lift-and-shift scaled from 7 files (#110) to 18 files (#111) without any intermediate test breakage.**
|
|
22
|
+
Every commit left all 41 test files passing.
|
|
23
|
+
The pattern — add new alongside old, migrate consumers with fallbacks (`record.execution?.session ?? record.session`), strip fallbacks in a final commit — is reliable for multi-step encapsulation refactors.
|
|
24
|
+
- **Stats encapsulation was simpler than expected.**
|
|
25
|
+
Converting `toolUses`, `lifetimeUsage`, `compactionCount` to private fields with getters and mutation methods required zero changes to read-only consumers because the getter names match the old field names.
|
|
26
|
+
Only `record-observer.ts` (the sole writer) needed updating.
|
|
27
|
+
- **The `createTestRecord` factory intersection type trick preserved backward compatibility.**
|
|
28
|
+
The factory accepts `toolUses?: number` via `Partial<AgentRecordInit> & { toolUses?: number; ... }` and internally calls `record.incrementToolUses()` in a loop.
|
|
29
|
+
This let 10+ test files continue passing `toolUses: 5` without rewriting each to call mutation methods directly.
|
|
30
|
+
- **`Promise.withResolvers` timing analysis in the plan was unnecessary.**
|
|
31
|
+
The plan spent ~40 lines analyzing whether `promise` should live inside `ExecutionState` and concluded it should stay separate.
|
|
32
|
+
Implementation confirmed: `record.execution` is set in `onSessionCreated` (async callback), `record.promise` is set after `runner.run()` (synchronous return) — different moments, straightforward.
|
|
33
|
+
|
|
34
|
+
#### What caused friction (agent side)
|
|
35
|
+
|
|
36
|
+
- `missing-context` — In the step 7 test for `record.execution`, the initial mock runner used `mockResolvedValue(...)` which doesn't call `onSessionCreated`, so `record.execution` stayed `undefined`.
|
|
37
|
+
Had to switch to `mockImplementation(async (..., opts) => { opts.onSessionCreated?.(session); ... })`.
|
|
38
|
+
The existing tests in the same file already use this pattern for record-observer tests, but I didn't check them first.
|
|
39
|
+
Impact: one test rewrite (~2 minutes), no rework to production code.
|
|
40
|
+
- `scope-drift` — Step 4 absorbed step 5 (adding collaborator fields) without noting the merge in the commit or session log.
|
|
41
|
+
Step 5 became a no-op.
|
|
42
|
+
Impact: no rework, but the session narrative skipped a plan step without explanation.
|
|
43
|
+
- `wrong-abstraction` — Step 12 was planned as a simple cleanup ("remove old fields and trim `AgentRecordInit`") but required coordinated changes across 18 files: removing 9 fields from `AgentRecordInit`, updating the `createTestRecord` factory, fixing 5 test files that passed removed fields, and stripping all fallback patterns.
|
|
44
|
+
This was 2-3 steps' worth of work compressed into one.
|
|
45
|
+
Impact: step 12 took significantly longer than other steps, though it landed cleanly.
|
|
46
|
+
- `missing-context` — Did not proactively flag the `as ReturnType<typeof vi.fn>` cast smell in `service-adapter.test.ts` while migrating that file.
|
|
47
|
+
The user noticed it and asked about it.
|
|
48
|
+
Filed as #123.
|
|
49
|
+
Impact: added friction but no rework; follow-up issue created.
|
|
50
|
+
User-caught.
|
|
51
|
+
|
|
52
|
+
#### What caused friction (user side)
|
|
53
|
+
|
|
54
|
+
- No material friction observed.
|
|
55
|
+
The user's `ask_user` decisions during planning (NotificationState collaborator, Map on AgentManager) gave clear direction.
|
|
56
|
+
Quick "follow-up" response on the cast smell kept scope tight.
|
|
57
|
+
|
|
58
|
+
### Changes made
|
|
59
|
+
|
|
60
|
+
1. `packages/pi-subagents/docs/retro/0111-split-agent-record-lifecycle.md` — this retro file.
|
|
61
|
+
2. `.pi/skills/testing/SKILL.md` — added field-removal rule symmetric to the existing field-addition rule (esbuild silent pass-through on unknown init properties).
|