@harms-haus/pi-workflows 1.0.0

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.
@@ -0,0 +1,479 @@
1
+ # Testing
2
+
3
+ Test suite for pi-workflows, covering workflow configuration loading and validation, state machine transitions, prompt generation, event hooks, tool registration, command handling, TUI renderers, and the extension entry point.
4
+
5
+ ---
6
+
7
+ ## Test Framework
8
+
9
+ | | |
10
+ | --------------- | ---------------------------------------------------------- |
11
+ | **Runner** | [Vitest](https://vitest.dev/) v4.1.6 |
12
+ | **Config** | `vitest.config.ts` — includes `src/__tests__/**/*.test.ts` |
13
+ | **Test script** | `"test": "vitest run"` in `package.json` |
14
+
15
+ Tests use Vitest's built-in `describe`/`it`/`expect` API. No additional assertion libraries are required.
16
+
17
+ ---
18
+
19
+ ## Test Files
20
+
21
+ All tests live under `src/__tests__/`. There are 9 test files with **324 total test cases**.
22
+
23
+ | File | Tests | What's Covered |
24
+ | ------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
25
+ | `config.test.ts` | 84 | `resolveTemplate`, `validateWorkflowDefinition`, `detectCycles`, `findWorkflowByCommandName`, `getBlockedTools`, `getWhitelist`, `loadWorkflowFromDir`, `loadWorkflowsFromDir`, `loadWorkflows` |
26
+ | `state.test.ts` | 36 | `createInitialState`, `advancePhase` (linear, enter-subworkflow, breakout, multi-level, auto-enter, two-subworkflows), `loopPhase` (scope isolation, loopable inheritance), `resolveActive`, `reconstructState`, `isActive` |
27
+ | `prompts.test.ts` | 10 | `buildContextPrompt` (linear, nested, template resolution, profiles), `collectAllProfiles`, `getPreviousPhaseName`, default message constants |
28
+ | `hooks.test.ts` | 47 | `updateStatus`, `handleToolCall`, `handleBeforeAgentStart`, `handleAgentEnd` (completion, cancellation, countdown widget, abort detection, no-UI fallback, edge cases, TimerManager-tracked non-UI countdown), `timerManager.clearAll` |
29
+ | `tool.test.ts` | 34 | `registerWorkflowTool` — `status`, `next`, `cancel`, `loop` actions; `renderCall`, `renderResult`; edge cases (stale definition, nested path, unknown action) |
30
+ | `command.test.ts` | 28 | `registerWorkflowCommand` (start, validation, conflicts, tab completion, subworkflow rejection), `registerCancelWorkflowCommand` (cancel, persist, message) |
31
+ | `renderers.test.ts` | 10 | `registerRenderers` — `workflow:context`, `workflow:complete`, `workflow:countdown` renderers |
32
+ | `TimerManager.test.ts` | 10 | `TimerManager` — `startInterval`, `startTimeout`, `clearAll`, stale callback prevention, timer replacement |
33
+ | `index.test.ts` | 23 | Extension entry point — event handler registration, `session_start`, `session_tree`, `tool_call`, `before_agent_start`, `agent_end`, `turn_end` handlers |
34
+
35
+ ### config.test.ts (84 tests)
36
+
37
+ **`resolveTemplate`** — 4 tests covering placeholder replacement, unknown variables left as-is, multiple variables, and empty template.
38
+
39
+ **`validateWorkflowDefinition`** — 24 tests covering:
40
+
41
+ - Valid `show: "user"` workflow passes
42
+ - Missing `commandName` / `initialMessage` on user-visible workflows → error
43
+ - `show: "workflows"` (internal) workflows skip those required-field checks
44
+ - `loopable` type validation (string vs boolean)
45
+ - `SubworkflowReference` entries: valid and empty `workflowKey`
46
+ - Duplicate phase IDs → error
47
+ - Empty or missing `phases` array → error
48
+ - Invalid `show` value → error
49
+ - Missing/empty `name` → error
50
+ - Invalid `commandName` format → error
51
+ - Non-array `blacklist` / `whitelist` → error
52
+ - Both blacklist and whitelist set → error
53
+ - Valid tools config with blacklist
54
+ - Missing `id`, `name`, `emoji`, or `instructions` on concrete phases → error
55
+
56
+ **`detectCycles`** — 9 tests covering:
57
+
58
+ - No subworkflow references → no cycles
59
+ - A → B (no cycle), A → A (self-reference), A → B → A, A → B → C → A
60
+ - DAG with multiple paths (A→B, A→C, B→D, C→D) → no cycles
61
+ - Empty definitions
62
+ - Disconnected components with one cyclic pair
63
+ - Subworkflow refs to non-existent workflows → no cycle
64
+
65
+ **`findWorkflowByCommandName`** — 2 tests: finds matching workflow, returns `null` for unknown name.
66
+
67
+ **`getBlockedTools` / `getWhitelist`** — 6 tests covering blacklist extraction, whitelist extraction, no-tools fallback, and cross-exclusivity.
68
+
69
+ **`loadWorkflowFromDir`** — 18 tests covering:
70
+
71
+ - Missing `workflow.yaml` → null
72
+ - Valid workflow with phases loaded from `.md` files
73
+ - Tool config (blacklist/whitelist) parsing from frontmatter
74
+ - Subworkflow reference entries
75
+ - Invalid phase entry types
76
+ - Missing `name` / `commandName` / `initialMessage` → null
77
+ - Path traversal outside workflows root → null
78
+ - Internal (`show: "workflows"`) workflows without `commandName`
79
+ - Optional fields (`loopable`, `roleInstruction`, `advanceReminder`, etc.)
80
+ - Missing phase frontmatter fields (`id`, `name`, `emoji`)
81
+ - Invalid YAML (not an object)
82
+ - Phase file read errors
83
+ - `realpathSync` edge cases
84
+
85
+ **`loadWorkflowsFromDir`** — 4 tests covering non-existent directory, loading from subdirectories, individual workflow errors, `readdirSync` errors, non-directory entries.
86
+
87
+ **`loadWorkflows`** — 8 tests covering loading from global pi dir, merging project-local over global, deduplication by `commandName`, subworkflow reference resolution, cycle removal, missing subworkflow reference removal, invalid workflow skipping, `PI_CODING_AGENT_DIR` env variable.
88
+
89
+ ### state.test.ts (36 tests)
90
+
91
+ Uses shared fixture definitions imported from `helpers/fixtures.ts` (see [Test Helpers](#test-helpers)), exercising a 3-phase linear workflow and a parent workflow containing a nested subworkflow.
92
+
93
+ **`createInitialState`** — 2 tests: correct field initialization (`currentPath`, `active`, `taskId` prefix) and absence of legacy `currentPhaseIndex` field.
94
+
95
+ **`advancePhase` — linear** — 3 tests: advance through phases 0→1→2, final advance sets `active=false`.
96
+
97
+ **`advancePhase` — enter subworkflow** — 2 tests: path length increases from 1 to 2, new segment pushed with correct `workflowKey`.
98
+
99
+ **`advancePhase` — breakout** — 2 tests: last phase of subworkflow pops segment, path length decreases.
100
+
101
+ **`advancePhase` — multi-level** — 1 test: full journey — enter sub → advance within → breakout → advance parent to DONE.
102
+
103
+ **`advancePhase` — auto-enter concrete phase name** — 2 tests: advancing to subworkflow ref returns concrete first phase name.
104
+
105
+ **`advancePhase` — breakout + auto-enter (two subworkflows)** — 2 tests: advance through parent → sub → sub2 → phase3, verifying auto-enter at each transition.
106
+
107
+ **`loopPhase`** — 3 tests: resets `phaseIndex` to 0 and increments `globalStepCount`, rejects non-loopable workflows, resets only innermost scope in nested workflows.
108
+
109
+ **`loopPhase` — subworkflow scope** — 1 test: after auto-enter, loop resets subworkflow scope.
110
+
111
+ **`loopPhase` — loopable isolation** — 2 tests: parent `loopable=false` does not block subworkflow looping; subworkflow `loopable=false` blocks looping even if parent allows it.
112
+
113
+ **`resolveActive` — linear** — 2 tests: single-element path resolves correctly, returns correct `currentPhase` and `nextPhase`.
114
+
115
+ **`resolveActive` — nested** — 2 tests: multi-element path resolves to innermost phase, breadcrumb array has correct entries.
116
+
117
+ **`resolveActive` — edge cases** — 4 tests: missing definition, out-of-bounds index, null state, inactive state.
118
+
119
+ **`reconstructState`** — 5 tests: migration from legacy `currentPhaseIndex` to `currentPath`, passthrough for new-format states, null for missing entries, null for empty `currentPath` (tampered), null for malformed path segment (tampered).
120
+
121
+ **`isActive`** — 3 tests: active state, inactive state, null.
122
+
123
+ ### prompts.test.ts (10 tests)
124
+
125
+ **`buildContextPrompt`** — 4 tests:
126
+
127
+ - Linear workflow includes phase name, instructions, and progress (e.g. `1/2 phases`)
128
+ - Nested workflow includes `[Workflow path:` breadcrumb line
129
+ - All template variables resolved (no leftover `{varName}`)
130
+ - `availableProfiles` shown in prompt when phase defines them
131
+
132
+ **`collectAllProfiles`** (tested via prompt output) — 1 test: profiles from subworkflow phases are included in the "All profiles" section when parent is active.
133
+
134
+ **`getPreviousPhaseName`** (tested via prompt output) — 2 tests: first phase resolves to `(start)`, later phase resolves to the previous phase's name.
135
+
136
+ **Default message constants** — 3 tests: each constant (`DEFAULT_NOT_DONE_REMINDER`, `DEFAULT_COMPLETION_MESSAGE`, `DEFAULT_CANCELLED_MESSAGE`) contains expected template variables.
137
+
138
+ ### hooks.test.ts (47 tests)
139
+
140
+ **`timerManager.clearAll`** — 3 tests: clears widget when interval is active, is safe to call when no timers are active, is safe to call when `ctx.hasUI` is false.
141
+
142
+ **`handleAgentEnd` — countdown widget** — 4 tests: skips auto-continue on abort, shows countdown widget before auto-continue (3s→2s→1s→send), handles `sendUserMessage` throwing during countdown, prevents stacked intervals.
143
+
144
+ **`handleAgentEnd` — null state** — 2 tests: no widget when state is null, returns noOp for active state.
145
+
146
+ **`handleAgentEnd` — no-UI fallback** — 1 test: uses `sendMessage` + `setTimeout` when `hasUI` is false.
147
+
148
+ **`updateStatus`** — 7 tests: clears when state is null, clears when inactive, shows phase name for linear workflow, shows breadcrumb format for nested subworkflow, clears when `resolveActive` returns null, shows progress at every level for deeply nested workflow, clears status when intermediate segment has out-of-bounds phaseIndex.
149
+
150
+ **`handleToolCall`** — 9 tests: allows all tools when null/inactive state, blocks blacklisted tools, blocks non-whitelisted tools, allows whitelisted tools, allows all when no tool config, always allows `workflow_step`, allows non-blacklisted tools.
151
+
152
+ **`handleBeforeAgentStart`** — 4 tests: returns undefined when null/inactive, returns context prompt when active, returns undefined when `resolveActive` returns null.
153
+
154
+ **`handleAgentEnd` — completion path** — 3 tests: sends default completion message, uses custom `completionMessage` template, uses `DEFAULT_COMPLETION_MESSAGE` when no custom template.
155
+
156
+ **`handleAgentEnd` — cancellation path** — 5 tests: sends cancelled message with details, uses custom template for cancelled workflow, uses `DEFAULT_CANCELLED_MESSAGE`, sets `completionNotified` when cancelled, does not re-send cancel message on second call (regression).
157
+
158
+ **`handleAgentEnd` — edge cases** — 5 tests: returns noOp when already `completionNotified`, returns noOp when `resolveActive` returns null, detects abort from message history (not-last-message and `stopReason=aborted`), handles empty messages array for abort detection.
159
+
160
+ **`handleToolCall` — edge cases** — 2 tests: returns undefined when `resolveActive` returns null, uses custom `blockReasonTemplate`.
161
+
162
+ **`handleAgentEnd` — additional edge cases** — 2 tests: no-UI fallback `setTimeout` throwing, `setWidget` throwing inside interval gracefully (countdown outer catch).
163
+
164
+ **`handleAgentEnd` — non-UI countdown tracked by TimerManager** — 2 tests: non-UI timeout is tracked by `timerManager`; `clearAll` cancels it, non-UI timeout fires `sendUserMessage` after 3s.
165
+
166
+ ### tool.test.ts (34 tests)
167
+
168
+ **Status action** — 3 tests: no active workflow, current phase info when active, stale definition.
169
+
170
+ **Next action** — 5 tests: advances phase and updates status, marks complete on last phase, entering subworkflow pushes new scope, exiting subworkflow pops scope, stale definition.
171
+
172
+ **Cancel action** — 3 tests: first call sets `_cancelPending`, second call marks cancelled, no active workflow.
173
+
174
+ **Loop action** — 3 tests: resets phase index, non-loopable returns error, no active workflow.
175
+
176
+ **Summary parameter** — 1 test: stored in state when provided with next action.
177
+
178
+ **`renderCall`** — 2 tests: returns Text component with tool name and action, renders different actions correctly.
179
+
180
+ **`renderResult`** — 10 tests: error results, `Error:` prefix, `Could not` prefix, `Unknown action` prefix, `not found` content, cancel confirmation, cancelled result, completion result, normal results (first line only), Container for non-text/empty content.
181
+
182
+ **Unknown action** — 1 test: returns unknown action message for invalid action.
183
+
184
+ **Loop stale definition** — 1 test: resolves correctly after loop.
185
+
186
+ ### command.test.ts (28 tests)
187
+
188
+ **`registerWorkflowCommand`** — registers `/workflow` command.
189
+
190
+ - **No arguments** (2): shows usage info with available workflow names, handles `undefined` args.
191
+ - **Valid invocation** (5): creates state and sends initial message, sets session name, respects `sessionNamePrefix`, truncates long description, calls `persistState`.
192
+ - **Unknown commandName** (2): shows error notification, lists available workflows in error message.
193
+ - **Already active** (2): shows confirm dialog, starts new workflow when confirmed.
194
+ - **Subworkflow rejection** (1): rejects subworkflow-only workflows started directly.
195
+ - **Missing description** (2): shows usage warning for empty/whitespace-only description.
196
+ - **Tab completion** (4): returns matching names, excludes subworkflow-only workflows, returns null for no match, returns all user-visible when prefix is empty.
197
+
198
+ **`registerCancelWorkflowCommand`** — registers `/cancel-workflow` command.
199
+
200
+ - **When not active** (2): info notification for null state, info notification for inactive state.
201
+ - **When active** (5): persists cancelled state, clears status bar, sends cancellation message, includes task description/ID, sets state to null, shows cancellation notification.
202
+
203
+ ### renderers.test.ts (10 tests)
204
+
205
+ **Registration** — 1 test: calls `registerMessageRenderer` 3 times with correct message types.
206
+
207
+ **`workflow:context` renderer** — 3 tests: returns Text instance, ignores message content (fixed context text), produces same output regardless of content.
208
+
209
+ **`workflow:complete` renderer** — 3 tests: returns Text instance, extracts string content with bold+success styling, handles non-string content gracefully.
210
+
211
+ **`workflow:countdown` renderer** — 3 tests: returns Text instance, extracts string content with dim styling, handles non-string content gracefully.
212
+
213
+ ### TimerManager.test.ts (10 tests)
214
+
215
+ Tests the `TimerManager` class in isolation using Vitest fake timers (`vi.useFakeTimers()`) with `beforeEach`/`afterEach` cleanup.
216
+
217
+ **`startInterval`** — 1 test: creates a tracked interval that fires repeatedly at the specified delay.
218
+
219
+ **`startTimeout`** — 1 test: creates a tracked timeout that fires once and does not repeat.
220
+
221
+ **`clearAll`** — 3 tests: clears both interval and timeout so callbacks don't fire after clear, safe to call when no timers are active, safe to call multiple times in a row.
222
+
223
+ **Stale callback prevention** — 2 tests: `clearAll` before timeout fires prevents callback, `clearAll` before interval fires prevents callback.
224
+
225
+ **Replacing timers** — 3 tests: calling `startInterval` again replaces the previous one (old callback doesn't fire), calling `startTimeout` again replaces the previous one, interval and timeout are independent — replacing one doesn't affect the other.
226
+
227
+ ### index.test.ts (23 tests)
228
+
229
+ Tests the extension entry point by mocking all sub-modules (`config`, `state`, `hooks`, `tool`, `command`, `renderers`) and verifying the wiring.
230
+
231
+ **Module registration** — 4 tests: exports a default function, registers 6 event handlers, registers the workflow tool, registers commands and renderers.
232
+
233
+ **`session_start` handler** — 3 tests: loads workflows and updates status, catches stale errors, re-throws non-stale errors.
234
+
235
+ **`session_tree` handler** — 3 tests: loads workflows and updates status, catches stale errors, re-throws non-stale errors.
236
+
237
+ **`tool_call` handler** — 2 tests: delegates to `handleToolCall`, returns block result when blocking.
238
+
239
+ **`before_agent_start` handler** — 2 tests: delegates to `handleBeforeAgentStart`, returns undefined when void.
240
+
241
+ **`agent_end` handler** — 5 tests: persists when mutation says persist, unloads state when `unload=true`, updates state when `mutation.state` provided, catches stale errors, re-throws non-stale errors.
242
+
243
+ **`turn_end` handler** — 3 tests: delegates to `updateStatus`, catches stale errors, re-throws non-stale errors.
244
+
245
+ ---
246
+
247
+ ## Test Helpers
248
+
249
+ Test helpers are split between **local helpers** defined in individual test files and **shared helpers** in `src/__tests__/helpers/`.
250
+
251
+ ### Shared Helpers (`helpers/mocks.ts`)
252
+
253
+ Provides mock implementations of the pi agent runtime interfaces:
254
+
255
+ **`createMockContext`** — creates a mock `ExtensionContext` with sensible defaults:
256
+
257
+ ```typescript
258
+ import { createMockContext } from "./helpers/mocks";
259
+
260
+ // Default mock with hasUI: true
261
+ const ctx = createMockContext();
262
+
263
+ // Override specific fields
264
+ const ctx = createMockContext({ hasUI: false });
265
+ ```
266
+
267
+ **`createMockAPI`** — creates a mock `ExtensionAPI` using a dual-handle pattern that returns both the `api` object and individual mock functions:
268
+
269
+ ```typescript
270
+ import { createMockAPI } from "./helpers/mocks";
271
+
272
+ const { api, sendMessage, registerTool, registerCommand, on } = createMockAPI();
273
+
274
+ // Use api for registration
275
+ registerWorkflowTool(api, getState, getDefinitions, setState);
276
+
277
+ // Assert on captured calls
278
+ expect(registerTool).toHaveBeenCalledTimes(1);
279
+ ```
280
+
281
+ Used by `hooks.test.ts`, `tool.test.ts`, `renderers.test.ts`, and `index.test.ts`.
282
+
283
+ ### Shared Fixtures (`helpers/fixtures.ts`)
284
+
285
+ Provides factory functions and fixture data for constructing test `WorkflowDefinition`, `WorkflowState`, and `PhaseDefinition` objects. Exports are namespaced per test file to avoid collisions:
286
+
287
+ | Export | Used By | Description |
288
+ | ----------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------- |
289
+ | `STATE_PHASE_*`, `makeStateLinearDef`, `makeStateParentDef`, `makeStateSubDef`, `makeStateAllDefs` | `state.test.ts` | 3-phase linear, 2-phase sub, parent with sub ref |
290
+ | `TOOL_PHASE_*`, `makeToolLinearDef`, `makeToolParentDef`, `makeToolSubDef`, `makeToolNoLoopDef`, `makeToolAllDefs`, `makeToolActiveState` | `tool.test.ts` | Linear, parent, sub, and no-loop definitions + active state builder |
291
+ | `PROMPTS_PHASE_*`, `makePromptsLinearDef` | `prompts.test.ts` | 2-phase linear workflow for prompt tests |
292
+ | `CMD_*`, `makeCommandDefs` | `command.test.ts` | User-visible and subworkflow-only command definitions |
293
+ | `makeDefinition`, `makeActiveState` | `hooks.test.ts`, `index.test.ts` | Minimal single-workflow definition and state |
294
+
295
+ ### Local Helpers
296
+
297
+ Some test files define additional helpers locally:
298
+
299
+ **`makeUserDef` / `makeInternalDef`** (config.test.ts) — build minimal `show: "user"` or `show: "workflows"` workflow definitions with optional overrides. Used for `validateWorkflowDefinition` and `detectCycles` tests.
300
+
301
+ **`makeCtx`** (state.test.ts) — creates a mock extension context for `reconstructState` tests, simulating session branch entries.
302
+
303
+ **`makeActive`** (prompts.test.ts) — builds a complete `ActiveWorkflow` object from a workflow definition and optional state/path overrides.
304
+
305
+ **`setupTool`** (tool.test.ts) — registers the workflow tool with mock API and returns `{ execute, renderCall, renderResult, ctx, getState, setState }` for testing.
306
+
307
+ **`createMockPI`** (command.test.ts) — creates a mock API that captures registered command handlers in a `Map`.
308
+
309
+ ---
310
+
311
+ ## Test Setup
312
+
313
+ A global setup file (`src/__tests__/setup.ts`) mocks the TUI rendering library so tests run without the full TUI dependency:
314
+
315
+ ```typescript
316
+ import { vi } from "vitest";
317
+
318
+ vi.mock("@earendil-works/pi-tui", () => ({
319
+ Text: class Text {
320
+ constructor(public content: string) {}
321
+ render = vi.fn(() => this.content);
322
+ },
323
+ Container: class Container {
324
+ render = vi.fn(() => "");
325
+ },
326
+ }));
327
+ ```
328
+
329
+ This is referenced via `setupFiles` in `vitest.config.ts`.
330
+
331
+ ---
332
+
333
+ ## Running Tests
334
+
335
+ ```bash
336
+ # Run all tests once (CI mode)
337
+ npm test
338
+
339
+ # Run in watch mode (development)
340
+ npx vitest
341
+
342
+ # Run with coverage report
343
+ npx vitest run --coverage
344
+ ```
345
+
346
+ Vitest discovers tests via the `include` pattern in `vitest.config.ts` and enforces **90% coverage thresholds** across all metrics:
347
+
348
+ ```typescript
349
+ import { defineConfig } from "vitest/config";
350
+ export default defineConfig({
351
+ test: {
352
+ include: ["src/__tests__/**/*.test.ts"],
353
+ setupFiles: ["src/__tests__/setup.ts"],
354
+ coverage: {
355
+ provider: "v8",
356
+ reporter: ["text", "lcov"],
357
+ include: ["src/**/*.ts"],
358
+ exclude: ["src/__tests__/**", "src/**/*.test.ts", "src/**/setup.ts", "src/**/helpers/**"],
359
+ thresholds: {
360
+ statements: 90,
361
+ branches: 90,
362
+ functions: 90,
363
+ lines: 90,
364
+ },
365
+ },
366
+ },
367
+ });
368
+ ```
369
+
370
+ ### Current Coverage
371
+
372
+ | File | Statements | Branches | Functions | Lines |
373
+ | ---------------------- | ---------- | -------- | --------- | ------ |
374
+ | **Overall** | 96.03% | 90.6% | 96.26% | 97.09% |
375
+ | `command.ts` | 97.18% | 85.71% | 100% | 98.5% |
376
+ | `hooks.ts` | 100% | 98.5% | 100% | 100% |
377
+ | `index.ts` | 91.22% | 100% | 66.66% | 94.11% |
378
+ | `prompts.ts` | 96.49% | 80.95% | 100% | 100% |
379
+ | `state.ts` | 88.13% | 78.04% | 100% | 90.82% |
380
+ | `tool.ts` | 97.24% | 92.06% | 100% | 97.19% |
381
+ | `config/loading.ts` | 96.42% | 91.17% | 100% | 96.64% |
382
+ | `config/validation.ts` | 99.17% | 97.11% | 100% | 100% |
383
+
384
+ ---
385
+
386
+ ## Remaining Coverage Gaps
387
+
388
+ All major modules now have dedicated test coverage. The remaining uncovered lines are primarily defensive branches and edge cases:
389
+
390
+ - **`state.ts`** (88.13% statements) — uncovered branches include multi-level breakout with more than two nesting levels and some `advancePhase` internal paths.
391
+ - **`prompts.ts`** (80.95% branches) — uncovered branches include some template variable resolution paths and conditional prompt sections.
392
+ - **`command.ts`** (85.71% branches) — one uncovered line in the session name handling.
393
+ - **`index.ts`** (91.22% statements, 66.66% functions) — some event handler wrapper functions are only exercised through specific mock paths.
394
+
395
+ ---
396
+
397
+ ## Adding Tests
398
+
399
+ ### File placement
400
+
401
+ Create new test files in `src/__tests__/` matching the pattern `*.test.ts`. The vitest config (`vitest.config.ts`) only includes files matching `src/__tests__/**/*.test.ts`.
402
+
403
+ ### Structure
404
+
405
+ Follow the `describe`/`it` pattern consistent with existing tests:
406
+
407
+ ```typescript
408
+ import { describe, it, expect } from "vitest";
409
+ import { myFunction } from "../myModule";
410
+
411
+ describe("myFunction", () => {
412
+ it("handles happy path", () => {
413
+ expect(myFunction("input")).toBe("expected");
414
+ });
415
+
416
+ it("handles edge case", () => {
417
+ expect(myFunction("")).toBeNull();
418
+ });
419
+ });
420
+ ```
421
+
422
+ ### Use existing helpers and fixtures
423
+
424
+ When testing modules that consume `WorkflowDefinition`, `WorkflowState`, or `ActiveWorkflow`, import from the shared helpers:
425
+
426
+ ```typescript
427
+ import { makeDefinition, makeActiveState } from "./helpers/fixtures";
428
+ import { createMockAPI, createMockContext } from "./helpers/mocks";
429
+ ```
430
+
431
+ See [Test Helpers](#test-helpers) for the full list of available factories.
432
+
433
+ ### Test edge cases
434
+
435
+ Following the existing patterns, each function's test suite covers:
436
+
437
+ 1. **Happy path** — correct inputs produce expected outputs
438
+ 2. **Invalid inputs** — missing fields, empty arrays, wrong types
439
+ 3. **Boundary conditions** — first phase, last phase, null state, out-of-bounds index
440
+ 4. **State mutations** — verify the object is mutated correctly (tests assert on the same `state` reference after calling `advancePhase`, `loopPhase`, etc.)
441
+
442
+ ### Mocking ExtensionAPI
443
+
444
+ Use `createMockAPI` and `createMockContext` from `helpers/mocks.ts` rather than building mocks by hand:
445
+
446
+ ```typescript
447
+ import { createMockAPI, createMockContext } from "./helpers/mocks";
448
+
449
+ const { api, sendMessage, registerTool } = createMockAPI();
450
+ const ctx = createMockContext();
451
+ ```
452
+
453
+ For testing tools, capture the registered tool definition and test the `execute` callback directly:
454
+
455
+ ```typescript
456
+ registerWorkflowTool(api, getState, getDefinitions, setState);
457
+ const toolConfig = registerTool.mock.calls[0][0];
458
+ const execute = toolConfig.execute as ToolExecuteFn;
459
+
460
+ const result = await execute("call-1", { action: "status" }, undefined, undefined, ctx);
461
+ ```
462
+
463
+ ### Enable coverage
464
+
465
+ Coverage is already configured in `vitest.config.ts` with v8 provider, lcov and text reporters, and 90% thresholds on all metrics. Run:
466
+
467
+ ```bash
468
+ npx vitest run --coverage
469
+ ```
470
+
471
+ CI builds will fail if any metric drops below the threshold.
472
+
473
+ ---
474
+
475
+ ## Related Documentation
476
+
477
+ - [Architecture](architecture.md) — module map, dependency graph, and data flows
478
+ - [State Management](state-management.md) — detailed state machine design and transitions
479
+ - [Subworkflows](subworkflows.md) — nested workflow entry/exit mechanics
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@harms-haus/pi-workflows",
3
+ "version": "1.0.0",
4
+ "description": "Workflow management extension for the pi coding agent",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "files": [
8
+ "src/**/*.ts",
9
+ "!src/__tests__/**",
10
+ "skills/",
11
+ "docs/",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "engines": {
19
+ "node": ">=22.0.0"
20
+ },
21
+ "pi": {
22
+ "extensions": [
23
+ "./src/index.ts"
24
+ ],
25
+ "skills": [
26
+ "./skills/workflow-generation"
27
+ ]
28
+ },
29
+ "author": "harms-haus",
30
+ "keywords": [
31
+ "pi-package",
32
+ "workflow"
33
+ ],
34
+ "scripts": {
35
+ "test": "vitest run",
36
+ "test:watch": "vitest",
37
+ "test:coverage": "vitest run --coverage",
38
+ "typecheck": "tsc --noEmit",
39
+ "lint": "eslint src/",
40
+ "format": "prettier --write src/",
41
+ "format:check": "prettier --check src/",
42
+ "lint:fix": "eslint --fix src/"
43
+ },
44
+ "license": "MIT",
45
+ "peerDependencies": {
46
+ "@earendil-works/pi-coding-agent": "*",
47
+ "@earendil-works/pi-ai": "*",
48
+ "@earendil-works/pi-tui": "*",
49
+ "typebox": "*"
50
+ },
51
+ "dependencies": {
52
+ "yaml": "^2.8.2"
53
+ },
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/harms-haus/pi-workflows.git"
57
+ },
58
+ "devDependencies": {
59
+ "@eslint/js": "^10.0.1",
60
+ "@types/node": "^22.0.0",
61
+ "@vitest/coverage-v8": "^4.1.6",
62
+ "eslint": "^10.4.0",
63
+ "eslint-config-prettier": "^10.1.8",
64
+ "prettier": "^3.8.3",
65
+ "typescript": "^6.0.3",
66
+ "typescript-eslint": "^8.59.3",
67
+ "vitest": "^4.1.6"
68
+ }
69
+ }