@directive-run/claude-plugin 1.16.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.
- package/.claude-plugin/plugin.json +6 -0
- package/LICENSE +26 -0
- package/README.md +113 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +66 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/skills/README.md +9 -0
- package/skills/building-ai-agents/SKILL.md +414 -0
- package/skills/building-ai-agents/ai-adapters.md +257 -0
- package/skills/building-ai-agents/ai-agents-streaming.md +307 -0
- package/skills/building-ai-agents/ai-communication.md +304 -0
- package/skills/building-ai-agents/api-skeleton.md +5 -0
- package/skills/building-ai-agents/examples.md +849 -0
- package/skills/building-ai-orchestrators/SKILL.md +401 -0
- package/skills/building-ai-orchestrators/ai-multi-agent.md +375 -0
- package/skills/building-ai-orchestrators/ai-orchestrator.md +466 -0
- package/skills/building-ai-orchestrators/ai-tasks.md +268 -0
- package/skills/building-ai-orchestrators/api-skeleton.md +5 -0
- package/skills/building-ai-orchestrators/examples.md +1609 -0
- package/skills/building-directive-systems/SKILL.md +499 -0
- package/skills/building-directive-systems/api-skeleton.md +5 -0
- package/skills/building-directive-systems/examples.md +4149 -0
- package/skills/building-directive-systems/multi-module.md +327 -0
- package/skills/building-directive-systems/plugins.md +361 -0
- package/skills/building-directive-systems/react-adapter.md +376 -0
- package/skills/building-directive-systems/system-api.md +463 -0
- package/skills/getting-started-with-directive/SKILL.md +198 -0
- package/skills/getting-started-with-directive/api-skeleton.md +5 -0
- package/skills/getting-started-with-directive/core-patterns.md +240 -0
- package/skills/getting-started-with-directive/examples.md +87 -0
- package/skills/getting-started-with-directive/sitemap.md +202 -0
- package/skills/hardening-ai-systems/SKILL.md +377 -0
- package/skills/hardening-ai-systems/ai-budget-resilience.md +408 -0
- package/skills/hardening-ai-systems/ai-guardrails-memory.md +370 -0
- package/skills/hardening-ai-systems/ai-security.md +400 -0
- package/skills/hardening-ai-systems/api-skeleton.md +5 -0
- package/skills/hardening-ai-systems/examples.md +628 -0
- package/skills/migrating-to-directive/SKILL.md +249 -0
- package/skills/migrating-to-directive/anti-patterns.md +382 -0
- package/skills/migrating-to-directive/api-skeleton.md +5 -0
- package/skills/migrating-to-directive/core-patterns.md +240 -0
- package/skills/migrating-to-directive/examples.md +511 -0
- package/skills/migrating-to-directive/schema-types.md +273 -0
- package/skills/reviewing-directive-code/SKILL.md +232 -0
- package/skills/reviewing-directive-code/anti-patterns.md +382 -0
- package/skills/reviewing-directive-code/api-skeleton.md +5 -0
- package/skills/reviewing-directive-code/core-patterns.md +240 -0
- package/skills/reviewing-directive-code/examples.md +463 -0
- package/skills/reviewing-directive-code/naming.md +321 -0
- package/skills/scaffolding-directive-modules/SKILL.md +218 -0
- package/skills/scaffolding-directive-modules/api-skeleton.md +5 -0
- package/skills/scaffolding-directive-modules/core-patterns.md +240 -0
- package/skills/scaffolding-directive-modules/examples.md +1386 -0
- package/skills/scaffolding-directive-modules/naming.md +321 -0
- package/skills/scaffolding-directive-modules/schema-types.md +273 -0
- package/skills/testing-ai-systems/SKILL.md +398 -0
- package/skills/testing-ai-systems/ai-debug-observability.md +329 -0
- package/skills/testing-ai-systems/ai-mcp-rag.md +293 -0
- package/skills/testing-ai-systems/ai-testing-evals.md +485 -0
- package/skills/testing-ai-systems/api-skeleton.md +5 -0
- package/skills/testing-ai-systems/examples.md +1261 -0
- package/skills/testing-directive-code/SKILL.md +479 -0
- package/skills/testing-directive-code/api-skeleton.md +5 -0
- package/skills/testing-directive-code/examples.md +156 -0
- package/skills/testing-directive-code/history.md +351 -0
- package/skills/testing-directive-code/testing.md +412 -0
- package/skills/writing-directive-constraints/SKILL.md +486 -0
- package/skills/writing-directive-constraints/api-skeleton.md +5 -0
- package/skills/writing-directive-constraints/constraints.md +274 -0
- package/skills/writing-directive-constraints/error-boundaries.md +330 -0
- package/skills/writing-directive-constraints/examples.md +1361 -0
- package/skills/writing-directive-constraints/resolvers.md +367 -0
- package/skills/writing-directive-modules/SKILL.md +342 -0
- package/skills/writing-directive-modules/anti-patterns.md +382 -0
- package/skills/writing-directive-modules/api-skeleton.md +5 -0
- package/skills/writing-directive-modules/core-patterns.md +240 -0
- package/skills/writing-directive-modules/examples.md +1639 -0
- package/skills/writing-directive-modules/naming.md +321 -0
- package/skills/writing-directive-modules/schema-types.md +273 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing-directive-code
|
|
3
|
+
description: "Test Directive modules and systems using createTestSystem, mockResolver, assertFact, assertDerivation, assertRequirement, settleWithFakeTimers, and flushMicrotasks. Use when writing tests for Directive modules, constraints, resolvers, derivations, effects, or time-travel behavior."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Testing Directive Code
|
|
7
|
+
|
|
8
|
+
## Prerequisites
|
|
9
|
+
|
|
10
|
+
This skill applies when the project uses `@directive-run/core`. If not found in `package.json`, suggest installing it: `npm install @directive-run/core`.
|
|
11
|
+
|
|
12
|
+
## When Claude Should Use This Skill
|
|
13
|
+
|
|
14
|
+
**Auto-invoke when the user:**
|
|
15
|
+
- Says "write a test", "add tests for this module", or "how do I test this resolver"
|
|
16
|
+
- Shows a Directive module and asks how to verify its behavior
|
|
17
|
+
- Asks about mocking resolvers, fake timers, or asserting facts in tests
|
|
18
|
+
- Asks about testing time-travel or snapshot behavior
|
|
19
|
+
|
|
20
|
+
**Do NOT invoke when:**
|
|
21
|
+
- Writing the module under test (see `writing-directive-modules.md`)
|
|
22
|
+
- Asking about system composition (see `building-directive-systems.md`)
|
|
23
|
+
- Asking about constraint patterns without a testing context (see `writing-directive-constraints.md`)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Decision Tree: What Are You Testing?
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
What are you testing?
|
|
31
|
+
├── A single module in isolation → createTestSystem(module)
|
|
32
|
+
├── Multiple modules together → createTestSystemFromModules({ a, b })
|
|
33
|
+
├── A constraint fires correctly → Set facts, call assertRequirement
|
|
34
|
+
├── A resolver mutates facts → Mock resolver, trigger, settle, assertFact
|
|
35
|
+
├── A derivation computes correctly → Set facts, call assertDerivation
|
|
36
|
+
├── Async/retry behavior → settleWithFakeTimers(system, vi)
|
|
37
|
+
├── Intermediate state (mid-resolve) → flushMicrotasks()
|
|
38
|
+
└── Time-travel / snapshots → Enable history in createTestSystem options
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Imports
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import {
|
|
47
|
+
createTestSystem,
|
|
48
|
+
createTestSystemFromModules,
|
|
49
|
+
mockResolver,
|
|
50
|
+
flushMicrotasks,
|
|
51
|
+
settleWithFakeTimers,
|
|
52
|
+
assertFact,
|
|
53
|
+
assertDerivation,
|
|
54
|
+
assertRequirement,
|
|
55
|
+
} from "@directive-run/core/testing";
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Creating Test Systems
|
|
61
|
+
|
|
62
|
+
### Single module
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// Minimal – testing defaults: time-travel off, no plugins, sync settling
|
|
66
|
+
const system = createTestSystem(myModule);
|
|
67
|
+
|
|
68
|
+
// With initial state
|
|
69
|
+
const system = createTestSystem(myModule, {
|
|
70
|
+
initialFacts: { count: 5, phase: "loading" },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// With mocked resolvers
|
|
74
|
+
const system = createTestSystem(myModule, {
|
|
75
|
+
mockResolvers: [
|
|
76
|
+
mockResolver("FETCH_USER", async (req, context) => {
|
|
77
|
+
context.facts.user = { id: req.userId, name: "Test User" };
|
|
78
|
+
}),
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Combined
|
|
83
|
+
const system = createTestSystem(myModule, {
|
|
84
|
+
initialFacts: { isAuthenticated: true, profile: null },
|
|
85
|
+
mockResolvers: [
|
|
86
|
+
mockResolver("FETCH_PROFILE", async (req, context) => {
|
|
87
|
+
context.facts.profile = { bio: "Test bio" };
|
|
88
|
+
}),
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Multi-module
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
const system = createTestSystemFromModules(
|
|
97
|
+
{ auth: authModule, cart: cartModule },
|
|
98
|
+
{
|
|
99
|
+
mockResolvers: [
|
|
100
|
+
mockResolver("AUTHENTICATE", async (req, context) => {
|
|
101
|
+
context.facts.auth.token = "test-token";
|
|
102
|
+
context.facts.auth.isAuthenticated = true;
|
|
103
|
+
}),
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Testing Constraints
|
|
112
|
+
|
|
113
|
+
Set facts to satisfy the constraint's `when()`, then assert the requirement was emitted.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
describe("fetchWhenAuth constraint", () => {
|
|
117
|
+
it("emits FETCH_USER when authenticated without profile", () => {
|
|
118
|
+
const system = createTestSystem(userModule);
|
|
119
|
+
|
|
120
|
+
system.facts.isAuthenticated = true;
|
|
121
|
+
system.facts.profile = null;
|
|
122
|
+
|
|
123
|
+
assertRequirement(system, "FETCH_USER");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("does NOT emit when profile already loaded", () => {
|
|
127
|
+
const system = createTestSystem(userModule, {
|
|
128
|
+
initialFacts: {
|
|
129
|
+
isAuthenticated: true,
|
|
130
|
+
profile: { id: "1", name: "Alice" },
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const { requirements } = system.inspect();
|
|
135
|
+
const fetchReqs = requirements.filter((r) => r.type === "FETCH_USER");
|
|
136
|
+
expect(fetchReqs).toHaveLength(0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("emits dynamic requirement with correct payload", () => {
|
|
140
|
+
const system = createTestSystem(userModule, {
|
|
141
|
+
initialFacts: { isAuthenticated: true, userId: "u-42", profile: null },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// assertRequirement can check payload
|
|
145
|
+
assertRequirement(system, "FETCH_USER", { userId: "u-42" });
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Testing Resolvers
|
|
153
|
+
|
|
154
|
+
Mock the resolver, trigger the constraint, settle, then assert fact mutations.
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
describe("fetchUser resolver", () => {
|
|
158
|
+
it("stores fetched user and updates phase", async () => {
|
|
159
|
+
const system = createTestSystem(userModule, {
|
|
160
|
+
mockResolvers: [
|
|
161
|
+
mockResolver("FETCH_USER", async (req, context) => {
|
|
162
|
+
context.facts.user = { id: req.userId, name: "Mocked User" };
|
|
163
|
+
context.facts.phase = "loaded";
|
|
164
|
+
}),
|
|
165
|
+
],
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
system.facts.isAuthenticated = true;
|
|
169
|
+
system.facts.user = null;
|
|
170
|
+
|
|
171
|
+
await system.settle();
|
|
172
|
+
|
|
173
|
+
assertFact(system, "user", { id: expect.any(String), name: "Mocked User" });
|
|
174
|
+
assertFact(system, "phase", "loaded");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("handles resolver error gracefully", async () => {
|
|
178
|
+
const system = createTestSystem(userModule, {
|
|
179
|
+
mockResolvers: [
|
|
180
|
+
mockResolver("FETCH_USER", async () => {
|
|
181
|
+
throw new Error("Network failure");
|
|
182
|
+
}),
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
system.facts.isAuthenticated = true;
|
|
187
|
+
system.facts.user = null;
|
|
188
|
+
|
|
189
|
+
// If error boundary uses "skip", settle resolves normally
|
|
190
|
+
await system.settle();
|
|
191
|
+
assertFact(system, "phase", "error");
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Testing Derivations
|
|
199
|
+
|
|
200
|
+
Set facts, then read the derived value. No need to settle – derivations are synchronous.
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
describe("isOverBudget derivation", () => {
|
|
204
|
+
it("returns true when total exceeds budget", () => {
|
|
205
|
+
const system = createTestSystem(budgetModule, {
|
|
206
|
+
initialFacts: { total: 150, budget: 100 },
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
assertDerivation(system, "isOverBudget", true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("recomputes reactively when facts change", () => {
|
|
213
|
+
const system = createTestSystem(budgetModule, {
|
|
214
|
+
initialFacts: { total: 50, budget: 100 },
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
assertDerivation(system, "isOverBudget", false);
|
|
218
|
+
|
|
219
|
+
system.facts.total = 200;
|
|
220
|
+
|
|
221
|
+
assertDerivation(system, "isOverBudget", true);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Testing with Fake Timers
|
|
229
|
+
|
|
230
|
+
Use `settleWithFakeTimers` when resolvers use retry delays, timeouts, or exponential backoff.
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
import { describe, it, expect, vi } from "vitest";
|
|
234
|
+
import { createTestSystem, settleWithFakeTimers, mockResolver, assertFact } from "@directive-run/core/testing";
|
|
235
|
+
|
|
236
|
+
describe("retry behavior", () => {
|
|
237
|
+
it("retries on failure with exponential backoff", async () => {
|
|
238
|
+
vi.useFakeTimers();
|
|
239
|
+
let attempts = 0;
|
|
240
|
+
|
|
241
|
+
const system = createTestSystem(myModule, {
|
|
242
|
+
mockResolvers: [
|
|
243
|
+
mockResolver("FETCH_DATA", async (req, context) => {
|
|
244
|
+
attempts += 1;
|
|
245
|
+
if (attempts < 3) {
|
|
246
|
+
throw new Error("Temporary failure");
|
|
247
|
+
}
|
|
248
|
+
context.facts.data = "success";
|
|
249
|
+
}),
|
|
250
|
+
],
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
system.facts.needsData = true;
|
|
254
|
+
|
|
255
|
+
// Advances fake timers through retry delays and waits for settlement
|
|
256
|
+
await settleWithFakeTimers(system, vi);
|
|
257
|
+
|
|
258
|
+
expect(attempts).toBe(3);
|
|
259
|
+
assertFact(system, "data", "success");
|
|
260
|
+
|
|
261
|
+
vi.useRealTimers();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Testing Intermediate Async State
|
|
269
|
+
|
|
270
|
+
Use `flushMicrotasks()` to process pending promises without fully settling.
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
it("shows loading phase during resolution", async () => {
|
|
274
|
+
const system = createTestSystem(myModule, {
|
|
275
|
+
mockResolvers: [
|
|
276
|
+
mockResolver("FETCH_DATA", async (req, context) => {
|
|
277
|
+
context.facts.phase = "loading";
|
|
278
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
279
|
+
context.facts.phase = "done";
|
|
280
|
+
}),
|
|
281
|
+
],
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
system.facts.trigger = true;
|
|
285
|
+
|
|
286
|
+
await flushMicrotasks();
|
|
287
|
+
assertFact(system, "phase", "loading");
|
|
288
|
+
|
|
289
|
+
await system.settle();
|
|
290
|
+
assertFact(system, "phase", "done");
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Testing Effects
|
|
297
|
+
|
|
298
|
+
Effects run after reconciliation. Settle first, then check side effects.
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
describe("logPhase effect", () => {
|
|
302
|
+
it("logs when phase changes", async () => {
|
|
303
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
304
|
+
|
|
305
|
+
const system = createTestSystem(myModule, {
|
|
306
|
+
initialFacts: { phase: "idle" },
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
system.facts.phase = "loading";
|
|
310
|
+
await system.settle();
|
|
311
|
+
|
|
312
|
+
expect(consoleSpy).toHaveBeenCalledWith("Phase: loading");
|
|
313
|
+
|
|
314
|
+
consoleSpy.mockRestore();
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Testing Time-Travel
|
|
322
|
+
|
|
323
|
+
Enable time-travel in the test system config. In most cases, prefer `assertFact` over snapshot testing.
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
describe("history", () => {
|
|
327
|
+
it("can undo a fact change", () => {
|
|
328
|
+
const system = createTestSystem(editorModule, {
|
|
329
|
+
history: { maxSnapshots: 20 },
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
system.facts.text = "Hello";
|
|
333
|
+
system.facts.text = "Hello, world";
|
|
334
|
+
|
|
335
|
+
const history = system.history;
|
|
336
|
+
expect(system.facts.text).toBe("Hello, world");
|
|
337
|
+
|
|
338
|
+
history.goBack();
|
|
339
|
+
expect(system.facts.text).toBe("Hello");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("can export and import history", () => {
|
|
343
|
+
const system = createTestSystem(myModule, {
|
|
344
|
+
history: true,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
system.facts.count = 1;
|
|
348
|
+
system.facts.count = 2;
|
|
349
|
+
|
|
350
|
+
const history = system.history;
|
|
351
|
+
const exported = history.export();
|
|
352
|
+
|
|
353
|
+
history.import(exported);
|
|
354
|
+
history.goTo(0);
|
|
355
|
+
expect(system.facts.count).toBe(1);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Critical Anti-Patterns
|
|
363
|
+
|
|
364
|
+
### 1. Testing real resolvers (hitting real APIs)
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
// WRONG – slow, flaky, external dependency
|
|
368
|
+
const system = createTestSystem(myModule);
|
|
369
|
+
system.facts.needsFetch = true;
|
|
370
|
+
await system.settle(); // Makes real HTTP call
|
|
371
|
+
|
|
372
|
+
// CORRECT – always mock resolvers in unit tests
|
|
373
|
+
const system = createTestSystem(myModule, {
|
|
374
|
+
mockResolvers: [mockResolver("FETCH", async (req, context) => {
|
|
375
|
+
context.facts.data = { mocked: true };
|
|
376
|
+
})],
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### 2. Forgetting to settle before asserting async results
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
// WRONG – resolver hasn't completed
|
|
384
|
+
system.facts.trigger = true;
|
|
385
|
+
assertFact(system, "result", "done"); // Fails
|
|
386
|
+
|
|
387
|
+
// CORRECT
|
|
388
|
+
system.facts.trigger = true;
|
|
389
|
+
await system.settle();
|
|
390
|
+
assertFact(system, "result", "done");
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### 3. Resolver parameter naming
|
|
394
|
+
Always use `(req, context)` – never `(req, ctx)` or `(request, context)`. Applies to `mockResolver` callbacks too.
|
|
395
|
+
|
|
396
|
+
### 4. Testing implementation details instead of behavior
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
// WRONG – testing internal resolver call count without behavior check
|
|
400
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
401
|
+
|
|
402
|
+
// CORRECT – test the observable outcome (facts, derivations)
|
|
403
|
+
assertFact(system, "user", { id: "1", name: "Alice" });
|
|
404
|
+
assertDerivation(system, "isLoggedIn", true);
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### 5. Using createSystem instead of createTestSystem
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
// WRONG – production system, no test defaults, runs plugins
|
|
411
|
+
const system = createSystem({ module: myModule });
|
|
412
|
+
|
|
413
|
+
// CORRECT – isolated, no plugins, testing utilities available
|
|
414
|
+
const system = createTestSystem(myModule);
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### 6. Not resetting fake timers
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
// WRONG – leaks fake timers to other tests
|
|
421
|
+
vi.useFakeTimers();
|
|
422
|
+
await settleWithFakeTimers(system, vi);
|
|
423
|
+
// Missing vi.useRealTimers()
|
|
424
|
+
|
|
425
|
+
// CORRECT – always restore in afterEach or at the end of the test
|
|
426
|
+
vi.useFakeTimers();
|
|
427
|
+
await settleWithFakeTimers(system, vi);
|
|
428
|
+
vi.useRealTimers();
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### 7. Enabling time-travel when you don't need it
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
// Prefer assertFact/assertDerivation for most tests.
|
|
435
|
+
// Only enable time-travel when specifically testing undo/redo or snapshot export.
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### 8. Forgetting to destroy the system between tests
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
// CORRECT – use afterEach to clean up
|
|
442
|
+
let system: ReturnType<typeof createTestSystem>;
|
|
443
|
+
|
|
444
|
+
beforeEach(() => {
|
|
445
|
+
system = createTestSystem(myModule);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
afterEach(() => {
|
|
449
|
+
system.destroy();
|
|
450
|
+
});
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Assertion Quick Reference
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
// Assert a fact value
|
|
459
|
+
assertFact(system, "phase", "done");
|
|
460
|
+
assertFact(system, "user", { id: expect.any(String) });
|
|
461
|
+
|
|
462
|
+
// Assert a derivation value
|
|
463
|
+
assertDerivation(system, "isLoading", false);
|
|
464
|
+
assertDerivation(system, "itemCount", 3);
|
|
465
|
+
|
|
466
|
+
// Assert a requirement was emitted
|
|
467
|
+
assertRequirement(system, "FETCH_USER");
|
|
468
|
+
assertRequirement(system, "FETCH_USER", { userId: "u-42" }); // With payload
|
|
469
|
+
|
|
470
|
+
// Inspect raw state
|
|
471
|
+
const { facts, requirements, resolvers, inflight } = system.inspect();
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
## Reference Files
|
|
477
|
+
|
|
478
|
+
- `testing.md` – full testing API, createTestSystem options, multi-module test setup
|
|
479
|
+
- `history.md` – history API, changesets, export/import, performance considerations
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
> Auto-generated from extracted examples. Do not edit manually.
|
|
4
|
+
|
|
5
|
+
## time-machine
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// Example: time-machine
|
|
9
|
+
// Source: examples/time-machine/src/module.ts
|
|
10
|
+
// Pure module file — no DOM wiring
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Time Machine — Directive Module
|
|
14
|
+
*
|
|
15
|
+
* Drawing canvas where each stroke is a fact mutation. Full time-travel:
|
|
16
|
+
* undo/redo, export/import JSON, replay animation, changesets, snapshot slider.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
type ModuleSchema,
|
|
21
|
+
createModule,
|
|
22
|
+
createSystem,
|
|
23
|
+
t,
|
|
24
|
+
} from "@directive-run/core";
|
|
25
|
+
import { devtoolsPlugin } from "@directive-run/core/plugins";
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Types
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
export interface Stroke {
|
|
32
|
+
id: string;
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
color: string;
|
|
36
|
+
size: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface TimelineEntry {
|
|
40
|
+
time: number;
|
|
41
|
+
event: string;
|
|
42
|
+
detail: string;
|
|
43
|
+
type:
|
|
44
|
+
| "stroke"
|
|
45
|
+
| "undo"
|
|
46
|
+
| "redo"
|
|
47
|
+
| "changeset"
|
|
48
|
+
| "export"
|
|
49
|
+
| "import"
|
|
50
|
+
| "replay"
|
|
51
|
+
| "goto";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Timeline (external mutable array, same pattern as fraud-analysis)
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
export const timeline: TimelineEntry[] = [];
|
|
59
|
+
|
|
60
|
+
export function addTimeline(
|
|
61
|
+
event: string,
|
|
62
|
+
detail: string,
|
|
63
|
+
type: TimelineEntry["type"],
|
|
64
|
+
): void {
|
|
65
|
+
timeline.unshift({ time: Date.now(), event, detail, type });
|
|
66
|
+
if (timeline.length > 50) {
|
|
67
|
+
timeline.length = 50;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Schema
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
export const schema = {
|
|
76
|
+
facts: {
|
|
77
|
+
strokes: t.array<Stroke>(),
|
|
78
|
+
currentColor: t.string(),
|
|
79
|
+
brushSize: t.number(),
|
|
80
|
+
changesetActive: t.boolean(),
|
|
81
|
+
changesetLabel: t.string(),
|
|
82
|
+
},
|
|
83
|
+
derivations: {
|
|
84
|
+
strokeCount: t.number(),
|
|
85
|
+
canUndo: t.boolean(),
|
|
86
|
+
canRedo: t.boolean(),
|
|
87
|
+
currentIndex: t.number(),
|
|
88
|
+
totalSnapshots: t.number(),
|
|
89
|
+
},
|
|
90
|
+
events: {
|
|
91
|
+
addStroke: { x: t.number(), y: t.number() },
|
|
92
|
+
setColor: { value: t.string() },
|
|
93
|
+
setBrushSize: { value: t.number() },
|
|
94
|
+
clearCanvas: {},
|
|
95
|
+
},
|
|
96
|
+
requirements: {},
|
|
97
|
+
} satisfies ModuleSchema;
|
|
98
|
+
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// Module
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
const canvasModule = createModule("canvas", {
|
|
104
|
+
schema,
|
|
105
|
+
|
|
106
|
+
init: (facts) => {
|
|
107
|
+
facts.strokes = [];
|
|
108
|
+
facts.currentColor = "#5ba3a3";
|
|
109
|
+
facts.brushSize = 12;
|
|
110
|
+
facts.changesetActive = false;
|
|
111
|
+
facts.changesetLabel = "";
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
derive: {
|
|
115
|
+
strokeCount: (facts) => facts.strokes.length,
|
|
116
|
+
// These will be updated from the time-travel manager
|
|
117
|
+
canUndo: () => false,
|
|
118
|
+
canRedo: () => false,
|
|
119
|
+
currentIndex: () => 0,
|
|
120
|
+
totalSnapshots: () => 0,
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
events: {
|
|
124
|
+
addStroke: (facts, { x, y }) => {
|
|
125
|
+
const stroke: Stroke = {
|
|
126
|
+
id: `s${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
127
|
+
x,
|
|
128
|
+
y,
|
|
129
|
+
color: facts.currentColor,
|
|
130
|
+
size: facts.brushSize,
|
|
131
|
+
};
|
|
132
|
+
facts.strokes = [...facts.strokes, stroke];
|
|
133
|
+
},
|
|
134
|
+
setColor: (facts, { value }) => {
|
|
135
|
+
facts.currentColor = value;
|
|
136
|
+
},
|
|
137
|
+
setBrushSize: (facts, { value }) => {
|
|
138
|
+
facts.brushSize = value;
|
|
139
|
+
},
|
|
140
|
+
clearCanvas: (facts) => {
|
|
141
|
+
facts.strokes = [];
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ============================================================================
|
|
147
|
+
// System with Time-Travel
|
|
148
|
+
// ============================================================================
|
|
149
|
+
|
|
150
|
+
export const system = createSystem({
|
|
151
|
+
module: canvasModule,
|
|
152
|
+
history: { maxSnapshots: 200 },
|
|
153
|
+
trace: true,
|
|
154
|
+
plugins: [devtoolsPlugin({ name: "time-machine" })],
|
|
155
|
+
});
|
|
156
|
+
```
|