Skip to content

Commit 37212f1

Browse files
apankov1claude
andauthored
chore(skills): add event sourcing test patterns (#20)
## Summary - `model-based-testing`: Add Event Replay Testing (Given-When-Then) section + 2 violations - `zod-contract-testing`: Add 2 violations for schema versioning and boundary contracts ### New violations | Skill | Violation | Severity | |-------|-----------|----------| | `model-based-testing` | `missing_event_replay_test` | must-fail | | `model-based-testing` | `missing_given_when_then_coverage` | should-fail | | `zod-contract-testing` | `missing_schema_version_test` | must-fail | | `zod-contract-testing` | `missing_boundary_contract_test` | should-fail | Companion to apankov1/ai-balls#3885. ## Test plan - [ ] Review Given-When-Then examples for correctness - [ ] Verify violation severity levels are appropriate 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f8405ee commit 37212f1

2 files changed

Lines changed: 73 additions & 0 deletions

File tree

skills/model-based-testing/SKILL.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,62 @@ assert.deepEqual(terminals, ['published']);
174174

175175
---
176176

177+
## Event Replay Testing (Given-When-Then)
178+
179+
For event-sourced aggregates, state machines are driven by event replay. The Given-When-Then pattern tests the full cycle:
180+
181+
```
182+
Given: [list of historical events] → establishes state
183+
When: [command] → triggers decision
184+
Then: [list of new events] → asserts outcome
185+
```
186+
187+
### Pattern
188+
189+
```typescript
190+
describe('Game aggregate', () => {
191+
function givenEvents(events: GameEvent[]): GameState {
192+
return events.reduce(evolve, initialState);
193+
}
194+
195+
it('rejects move on completed game', () => {
196+
// Given: game completed
197+
const state = givenEvents([
198+
{ type: 'game_started', payload: { players: ['p1', 'p2'] } },
199+
{ type: 'move_executed', payload: { player: 'p1', row: 0, col: 0 } },
200+
{ type: 'game_won', payload: { winner: 'p1' } },
201+
]);
202+
203+
// When: another move attempted
204+
// Then: should throw
205+
expect(() => decide(state, { type: 'MAKE_MOVE', player: 'p2', row: 1, col: 1 }))
206+
.toThrow('Game is already completed');
207+
});
208+
209+
it('produces correct events for valid move', () => {
210+
// Given: game in progress
211+
const state = givenEvents([
212+
{ type: 'game_started', payload: { players: ['p1', 'p2'] } },
213+
]);
214+
215+
// When: valid move
216+
const newEvents = decide(state, { type: 'MAKE_MOVE', player: 'p1', row: 0, col: 0 });
217+
218+
// Then: move event produced
219+
expect(newEvents).toEqual([
220+
{ type: 'move_executed', payload: { player: 'p1', row: 0, col: 0 } },
221+
]);
222+
});
223+
});
224+
```
225+
226+
### Why This Matters
227+
228+
- **Decoupled from persistence**: Tests don't need databases, storage, or mocks
229+
- **Replay safety**: Proves that historical events produce correct state
230+
- **Schema evolution**: Add upcasted events to `Given` to verify migration
231+
- **Deterministic**: Pure functions — no async, no side effects
232+
177233
## Violation Rules
178234

179235
### missing_transition_coverage
@@ -192,6 +248,14 @@ Transitions that modify context MUST have assertions verifying exact changes and
192248
Terminal states (no outgoing transitions) MUST be explicitly identified and tested.
193249
**Severity**: should-fail
194250

251+
### missing_event_replay_test
252+
Event-sourced aggregates MUST have tests that replay historical events and verify resulting state. Without replay tests, schema evolution and upcasters can silently corrupt state.
253+
**Severity**: must-fail
254+
255+
### missing_given_when_then_coverage
256+
Event-sourced command handlers MUST have Given-When-Then tests covering: (1) valid commands producing correct events, (2) invalid commands on valid state, (3) valid commands on invalid/terminal state.
257+
**Severity**: should-fail
258+
195259
---
196260

197261
## Companion Skills
@@ -213,5 +277,6 @@ This skill provides **testing utilities** for state machines, not state machine
213277
| Guard truth table | Boolean guard functions | 2^N rows for N boolean inputs |
214278
| Context mutation | Transitions with side effects | `assertContextMutation(before, after, expected)` |
215279
| Terminal states | Lifecycle endpoints | `getTerminalStates(machine)` |
280+
| Given-When-Then | Event-sourced aggregates | `givenEvents([...]) → decide(state, cmd) → assertEvents(result)` |
216281

217282
See [patterns.md](./references/patterns.md) for XState integration, complex guard examples, and hibernation safety testing.

skills/zod-contract-testing/SKILL.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,14 @@ Zod parsing MUST happen at system boundaries (API handlers, WebSocket messages,
245245
Use `Schema.parse()` or `Schema.safeParse()`, NEVER `as Type` casts for external data.
246246
**Severity**: must-fail
247247

248+
### missing_schema_version_test
249+
Event schemas with `schemaVersion` field MUST have contract tests verifying that each version parses correctly and upcasters produce valid output for the target version.
250+
**Severity**: must-fail
251+
252+
### missing_boundary_contract_test
253+
Typed RPC interfaces used for cross-service or cross-DO communication MUST have contract tests verifying request/response schemas parse correctly at both caller and callee boundaries.
254+
**Severity**: should-fail
255+
248256
---
249257

250258
## Companion Skills

0 commit comments

Comments
 (0)