@directive-run/knowledge 0.2.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/LICENSE +21 -0
- package/README.md +63 -0
- package/ai/ai-adapters.md +250 -0
- package/ai/ai-agents-streaming.md +269 -0
- package/ai/ai-budget-resilience.md +235 -0
- package/ai/ai-communication.md +281 -0
- package/ai/ai-debug-observability.md +243 -0
- package/ai/ai-guardrails-memory.md +332 -0
- package/ai/ai-mcp-rag.md +288 -0
- package/ai/ai-multi-agent.md +274 -0
- package/ai/ai-orchestrator.md +227 -0
- package/ai/ai-security.md +293 -0
- package/ai/ai-tasks.md +261 -0
- package/ai/ai-testing-evals.md +378 -0
- package/api-skeleton.md +5 -0
- package/core/anti-patterns.md +382 -0
- package/core/constraints.md +263 -0
- package/core/core-patterns.md +228 -0
- package/core/error-boundaries.md +322 -0
- package/core/multi-module.md +315 -0
- package/core/naming.md +283 -0
- package/core/plugins.md +344 -0
- package/core/react-adapter.md +262 -0
- package/core/resolvers.md +357 -0
- package/core/schema-types.md +262 -0
- package/core/system-api.md +271 -0
- package/core/testing.md +257 -0
- package/core/time-travel.md +238 -0
- package/dist/index.cjs +111 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/examples/ab-testing.ts +385 -0
- package/examples/ai-checkpoint.ts +509 -0
- package/examples/ai-guardrails.ts +319 -0
- package/examples/ai-orchestrator.ts +589 -0
- package/examples/async-chains.ts +287 -0
- package/examples/auth-flow.ts +371 -0
- package/examples/batch-resolver.ts +341 -0
- package/examples/checkers.ts +589 -0
- package/examples/contact-form.ts +176 -0
- package/examples/counter.ts +393 -0
- package/examples/dashboard-loader.ts +512 -0
- package/examples/debounce-constraints.ts +105 -0
- package/examples/dynamic-modules.ts +293 -0
- package/examples/error-boundaries.ts +430 -0
- package/examples/feature-flags.ts +220 -0
- package/examples/form-wizard.ts +347 -0
- package/examples/fraud-analysis.ts +663 -0
- package/examples/goal-heist.ts +341 -0
- package/examples/multi-module.ts +57 -0
- package/examples/newsletter.ts +241 -0
- package/examples/notifications.ts +210 -0
- package/examples/optimistic-updates.ts +317 -0
- package/examples/pagination.ts +260 -0
- package/examples/permissions.ts +337 -0
- package/examples/provider-routing.ts +403 -0
- package/examples/server.ts +316 -0
- package/examples/shopping-cart.ts +422 -0
- package/examples/sudoku.ts +630 -0
- package/examples/theme-locale.ts +204 -0
- package/examples/time-machine.ts +225 -0
- package/examples/topic-guard.ts +306 -0
- package/examples/url-sync.ts +333 -0
- package/examples/websocket.ts +404 -0
- package/package.json +65 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# System API
|
|
2
|
+
|
|
3
|
+
The system is created with `createSystem()` and is the runtime that orchestrates modules, constraints, resolvers, and plugins.
|
|
4
|
+
|
|
5
|
+
## Decision Tree: "How do I interact with the system?"
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
What do you want to do?
|
|
9
|
+
├── Read/write state → system.facts.fieldName
|
|
10
|
+
├── Read computed values → system.derive.derivationName
|
|
11
|
+
├── Dispatch user actions → system.events.eventName(payload)
|
|
12
|
+
├── React to changes → system.subscribe() or system.watch()
|
|
13
|
+
├── Wait for a condition → system.when()
|
|
14
|
+
├── Wait for all async to finish → system.settle()
|
|
15
|
+
├── Debug/inspect current state → system.inspect()
|
|
16
|
+
├── Control lifecycle → system.start() / system.stop() / system.destroy()
|
|
17
|
+
└── Multi-module access → system.facts.moduleName.fieldName
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Creating a System
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { createSystem } from "@directive-run/core";
|
|
24
|
+
import { loggingPlugin, devtoolsPlugin } from "@directive-run/core/plugins";
|
|
25
|
+
|
|
26
|
+
// Single module — direct access to facts/derive/events
|
|
27
|
+
const system = createSystem({
|
|
28
|
+
module: myModule,
|
|
29
|
+
plugins: [loggingPlugin(), devtoolsPlugin()],
|
|
30
|
+
debug: { timeTravel: true, maxSnapshots: 100 },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Multi-module — namespaced access
|
|
34
|
+
const system = createSystem({
|
|
35
|
+
modules: {
|
|
36
|
+
auth: authModule,
|
|
37
|
+
cart: cartModule,
|
|
38
|
+
ui: uiModule,
|
|
39
|
+
},
|
|
40
|
+
plugins: [devtoolsPlugin()],
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Facts: Reading and Writing State
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// Single module
|
|
48
|
+
system.facts.count = 5;
|
|
49
|
+
const val = system.facts.count;
|
|
50
|
+
|
|
51
|
+
// Multi-module — access through module namespace
|
|
52
|
+
system.facts.auth.token = "abc123";
|
|
53
|
+
system.facts.cart.items = [];
|
|
54
|
+
const token = system.facts.auth.token;
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Facts are proxy-based. Mutations are tracked automatically and trigger derivation recomputation, constraint evaluation, and effect execution.
|
|
58
|
+
|
|
59
|
+
## Derivations: Reading Computed Values
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// Single module
|
|
63
|
+
const loading = system.derive.isLoading;
|
|
64
|
+
const display = system.derive.displayName;
|
|
65
|
+
|
|
66
|
+
// Multi-module
|
|
67
|
+
const isAdmin = system.derive.auth.isAdmin;
|
|
68
|
+
const total = system.derive.cart.totalPrice;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Derivations are read-only. They recompute lazily when their tracked facts change.
|
|
72
|
+
|
|
73
|
+
## Events: Dispatching Actions
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// Single module
|
|
77
|
+
system.events.increment();
|
|
78
|
+
system.events.setUser({ user: { id: "1", name: "Alice" } });
|
|
79
|
+
|
|
80
|
+
// Multi-module
|
|
81
|
+
system.events.auth.login({ email: "alice@example.com" });
|
|
82
|
+
system.events.cart.addItem({ productId: "p1", quantity: 2 });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Events are synchronous. They mutate facts in the event handler, which triggers the reactive pipeline.
|
|
86
|
+
|
|
87
|
+
## Subscribing to Changes
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// Subscribe to specific keys (facts or derivations)
|
|
91
|
+
const unsub = system.subscribe(["count", "isLoading"], () => {
|
|
92
|
+
console.log(system.facts.count, system.derive.isLoading);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Watch a single value with old/new
|
|
96
|
+
system.watch("count", (newVal, oldVal) => {
|
|
97
|
+
console.log(`Count: ${oldVal} -> ${newVal}`);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Unsubscribe
|
|
101
|
+
unsub();
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Waiting for Conditions
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// Wait until a condition is true
|
|
108
|
+
await system.when((facts) => facts.phase === "done");
|
|
109
|
+
|
|
110
|
+
// With timeout — throws if condition not met in time
|
|
111
|
+
await system.when((facts) => facts.phase === "done", { timeout: 5000 });
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Settling: Waiting for Async Completion
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
system.start();
|
|
118
|
+
|
|
119
|
+
// Wait for all resolvers and async constraints to complete
|
|
120
|
+
await system.settle();
|
|
121
|
+
|
|
122
|
+
// With timeout
|
|
123
|
+
await system.settle(5000); // Throws if not settled in 5s
|
|
124
|
+
|
|
125
|
+
// Check settlement state synchronously
|
|
126
|
+
if (system.isSettled) {
|
|
127
|
+
console.log("All resolvers complete");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Subscribe to settlement changes
|
|
131
|
+
const unsub = system.onSettledChange(() => {
|
|
132
|
+
console.log("Settlement:", system.isSettled);
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Reading by Key
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// Read fact or derivation by string key
|
|
140
|
+
const count = system.read("count");
|
|
141
|
+
const isLoading = system.read("isLoading");
|
|
142
|
+
|
|
143
|
+
// Multi-module — use dot notation
|
|
144
|
+
const token = system.read("auth.token");
|
|
145
|
+
const total = system.read("cart.totalPrice");
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Inspecting System State
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const inspection = system.inspect();
|
|
152
|
+
|
|
153
|
+
// Full fact snapshot
|
|
154
|
+
inspection.facts;
|
|
155
|
+
// { count: 5, phase: "done", user: { id: "1", name: "Alice" } }
|
|
156
|
+
|
|
157
|
+
// Derivation values
|
|
158
|
+
inspection.derivations;
|
|
159
|
+
// { isLoading: false, displayName: "Alice" }
|
|
160
|
+
|
|
161
|
+
// Active requirements
|
|
162
|
+
inspection.requirements;
|
|
163
|
+
// [{ id: "req-1", type: "FETCH_USER", userId: "1" }]
|
|
164
|
+
|
|
165
|
+
// Constraint definitions and state
|
|
166
|
+
inspection.constraintDefs;
|
|
167
|
+
// [{ id: "fetchWhenAuth", priority: 0, disabled: false }]
|
|
168
|
+
|
|
169
|
+
// Resolver statuses
|
|
170
|
+
inspection.resolvers;
|
|
171
|
+
// { fetchUser: { state: "success", duration: 150 } }
|
|
172
|
+
|
|
173
|
+
// Currently inflight resolvers
|
|
174
|
+
inspection.inflight;
|
|
175
|
+
// [{ id: "req-2", resolverId: "fetchData", startedAt: 1709000000 }]
|
|
176
|
+
|
|
177
|
+
// Unmet requirements (no matching resolver)
|
|
178
|
+
inspection.unmet;
|
|
179
|
+
|
|
180
|
+
// Explain why a requirement exists
|
|
181
|
+
const explanation = system.explain("req-123");
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Lifecycle
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// Start — begins constraint evaluation and reconciliation
|
|
188
|
+
system.start();
|
|
189
|
+
|
|
190
|
+
// Stop — pauses evaluation, cancels inflight resolvers
|
|
191
|
+
system.stop();
|
|
192
|
+
|
|
193
|
+
// Destroy — cleans up all resources, subscriptions, plugins
|
|
194
|
+
system.destroy();
|
|
195
|
+
|
|
196
|
+
// Lifecycle order:
|
|
197
|
+
// createSystem() → system.start() → ... → system.stop() → system.destroy()
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`system.start()` is auto-called in most cases. Call it explicitly when you need to set up subscriptions before the first evaluation cycle.
|
|
201
|
+
|
|
202
|
+
## Constraint Control at Runtime
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// Disable a constraint — it won't be evaluated
|
|
206
|
+
system.constraints.disable("fetchWhenReady");
|
|
207
|
+
|
|
208
|
+
// Check if disabled
|
|
209
|
+
system.constraints.isDisabled("fetchWhenReady"); // true
|
|
210
|
+
|
|
211
|
+
// Re-enable — triggers re-evaluation on next cycle
|
|
212
|
+
system.constraints.enable("fetchWhenReady");
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Common Mistakes
|
|
216
|
+
|
|
217
|
+
### Reading facts before settling
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// WRONG — resolver hasn't completed, facts are stale
|
|
221
|
+
system.start();
|
|
222
|
+
console.log(system.facts.user); // null
|
|
223
|
+
|
|
224
|
+
// CORRECT — wait for async resolution
|
|
225
|
+
system.start();
|
|
226
|
+
await system.settle();
|
|
227
|
+
console.log(system.facts.user); // { id: "1", name: "Alice" }
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Casting facts/derivations unnecessarily
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
// WRONG — the schema already provides types
|
|
234
|
+
const profile = system.facts.profile as ResourceState<Profile>;
|
|
235
|
+
|
|
236
|
+
// CORRECT — types are inferred from the schema
|
|
237
|
+
const profile = system.facts.profile;
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Using single-line returns without braces
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
// WRONG
|
|
244
|
+
system.watch("phase", (val) => { if (val === "done") return; });
|
|
245
|
+
|
|
246
|
+
// CORRECT
|
|
247
|
+
system.watch("phase", (val) => {
|
|
248
|
+
if (val === "done") {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Forgetting to destroy
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// WRONG — resources leak
|
|
258
|
+
function setupSystem() {
|
|
259
|
+
const system = createSystem({ module: myModule });
|
|
260
|
+
system.start();
|
|
261
|
+
|
|
262
|
+
return system;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// CORRECT — clean up when done
|
|
266
|
+
const system = createSystem({ module: myModule });
|
|
267
|
+
system.start();
|
|
268
|
+
// ... when finished:
|
|
269
|
+
system.stop();
|
|
270
|
+
system.destroy();
|
|
271
|
+
```
|
package/core/testing.md
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
Testing utilities for Directive modules and systems. Import from `@directive-run/core/testing`.
|
|
4
|
+
|
|
5
|
+
## Decision Tree: "How should I test this?"
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
What are you testing?
|
|
9
|
+
├── A single module in isolation → createTestSystem(module)
|
|
10
|
+
├── Multiple modules together → createTestSystemFromModules({ a, b })
|
|
11
|
+
├── A constraint fires correctly → Set facts, check requirements
|
|
12
|
+
├── A resolver mutates facts → Mock the resolver, dispatch, assert facts
|
|
13
|
+
├── A derivation computes correctly → Set facts, read derived value
|
|
14
|
+
├── Async settling behavior → settleWithFakeTimers(system, vi)
|
|
15
|
+
└── An effect runs → Set facts, assert side effects
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Creating Test Systems
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import {
|
|
22
|
+
createTestSystem,
|
|
23
|
+
createTestSystemFromModules,
|
|
24
|
+
mockResolver,
|
|
25
|
+
flushMicrotasks,
|
|
26
|
+
settleWithFakeTimers,
|
|
27
|
+
assertFact,
|
|
28
|
+
assertDerivation,
|
|
29
|
+
assertRequirement,
|
|
30
|
+
} from "@directive-run/core/testing";
|
|
31
|
+
|
|
32
|
+
// Single module — same API as createSystem, with testing defaults
|
|
33
|
+
// (time-travel off, no plugins, synchronous settling)
|
|
34
|
+
const system = createTestSystem(myModule);
|
|
35
|
+
|
|
36
|
+
// With options
|
|
37
|
+
const system = createTestSystem(myModule, {
|
|
38
|
+
initialFacts: { count: 5, phase: "loading" },
|
|
39
|
+
mockResolvers: [mockResolver("FETCH_USER", async (req, context) => {
|
|
40
|
+
context.facts.user = { id: req.userId, name: "Test User" };
|
|
41
|
+
})],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Multi-module
|
|
45
|
+
const system = createTestSystemFromModules(
|
|
46
|
+
{ auth: authModule, cart: cartModule },
|
|
47
|
+
{ mockResolvers: [mockResolver("AUTHENTICATE", async (req, context) => {
|
|
48
|
+
context.facts.auth.token = "test-token";
|
|
49
|
+
})] },
|
|
50
|
+
);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Testing Constraints
|
|
54
|
+
|
|
55
|
+
Set facts to trigger the constraint's `when()`, then assert the requirement was emitted.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { describe, it, expect } from "vitest";
|
|
59
|
+
import { createTestSystem, assertRequirement } from "@directive-run/core/testing";
|
|
60
|
+
|
|
61
|
+
describe("fetchWhenAuth constraint", () => {
|
|
62
|
+
it("emits FETCH_USER when authenticated without profile", () => {
|
|
63
|
+
const system = createTestSystem(userModule);
|
|
64
|
+
|
|
65
|
+
// Set facts to satisfy the constraint's when()
|
|
66
|
+
system.facts.isAuthenticated = true;
|
|
67
|
+
system.facts.profile = null;
|
|
68
|
+
|
|
69
|
+
// Assert the requirement was emitted
|
|
70
|
+
assertRequirement(system, "FETCH_USER");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("does NOT emit when already has profile", () => {
|
|
74
|
+
const system = createTestSystem(userModule, {
|
|
75
|
+
initialFacts: {
|
|
76
|
+
isAuthenticated: true,
|
|
77
|
+
profile: { id: "1", name: "Alice" },
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// No requirement should exist
|
|
82
|
+
const inspection = system.inspect();
|
|
83
|
+
const fetchReqs = inspection.requirements.filter(
|
|
84
|
+
(r) => r.type === "FETCH_USER",
|
|
85
|
+
);
|
|
86
|
+
expect(fetchReqs).toHaveLength(0);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Testing Resolvers
|
|
92
|
+
|
|
93
|
+
Mock the resolver, trigger a requirement, and assert fact mutations.
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
describe("fetchUser resolver", () => {
|
|
97
|
+
it("stores fetched user in facts", async () => {
|
|
98
|
+
const system = createTestSystem(userModule, {
|
|
99
|
+
mockResolvers: [
|
|
100
|
+
mockResolver("FETCH_USER", async (req, context) => {
|
|
101
|
+
// Simulate API response
|
|
102
|
+
context.facts.user = { id: req.userId, name: "Mocked User" };
|
|
103
|
+
context.facts.phase = "loaded";
|
|
104
|
+
}),
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Trigger the constraint that emits FETCH_USER
|
|
109
|
+
system.facts.isAuthenticated = true;
|
|
110
|
+
system.facts.user = null;
|
|
111
|
+
|
|
112
|
+
// Wait for resolver to complete
|
|
113
|
+
await system.settle();
|
|
114
|
+
|
|
115
|
+
assertFact(system, "user", { id: expect.any(String), name: "Mocked User" });
|
|
116
|
+
assertFact(system, "phase", "loaded");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Testing Derivations
|
|
122
|
+
|
|
123
|
+
Set facts, then read the derived value.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
describe("isOverBudget derivation", () => {
|
|
127
|
+
it("returns true when total exceeds budget", () => {
|
|
128
|
+
const system = createTestSystem(budgetModule, {
|
|
129
|
+
initialFacts: { total: 150, budget: 100 },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
assertDerivation(system, "isOverBudget", true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("returns false when under budget", () => {
|
|
136
|
+
const system = createTestSystem(budgetModule, {
|
|
137
|
+
initialFacts: { total: 50, budget: 100 },
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
assertDerivation(system, "isOverBudget", false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("recomputes when facts change", () => {
|
|
144
|
+
const system = createTestSystem(budgetModule, {
|
|
145
|
+
initialFacts: { total: 50, budget: 100 },
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
assertDerivation(system, "isOverBudget", false);
|
|
149
|
+
|
|
150
|
+
system.facts.total = 200;
|
|
151
|
+
|
|
152
|
+
assertDerivation(system, "isOverBudget", true);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Async Testing with Fake Timers
|
|
158
|
+
|
|
159
|
+
Use `settleWithFakeTimers` when resolvers have retry delays or timeouts.
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { describe, it, vi } from "vitest";
|
|
163
|
+
import { createTestSystem, settleWithFakeTimers } from "@directive-run/core/testing";
|
|
164
|
+
|
|
165
|
+
describe("retry behavior", () => {
|
|
166
|
+
it("retries on failure with exponential backoff", async () => {
|
|
167
|
+
vi.useFakeTimers();
|
|
168
|
+
let attempts = 0;
|
|
169
|
+
|
|
170
|
+
const system = createTestSystem(myModule, {
|
|
171
|
+
mockResolvers: [
|
|
172
|
+
mockResolver("FETCH_DATA", async (req, context) => {
|
|
173
|
+
attempts += 1;
|
|
174
|
+
if (attempts < 3) {
|
|
175
|
+
throw new Error("Temporary failure");
|
|
176
|
+
}
|
|
177
|
+
context.facts.data = "success";
|
|
178
|
+
}),
|
|
179
|
+
],
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
system.facts.needsData = true;
|
|
183
|
+
|
|
184
|
+
// Advances fake timers through retry delays and settles
|
|
185
|
+
await settleWithFakeTimers(system, vi);
|
|
186
|
+
|
|
187
|
+
expect(attempts).toBe(3);
|
|
188
|
+
assertFact(system, "data", "success");
|
|
189
|
+
|
|
190
|
+
vi.useRealTimers();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Flushing Microtasks
|
|
196
|
+
|
|
197
|
+
Use `flushMicrotasks()` when you need to process pending promises without fully settling.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
it("processes intermediate state", async () => {
|
|
201
|
+
const system = createTestSystem(myModule);
|
|
202
|
+
|
|
203
|
+
system.facts.trigger = true;
|
|
204
|
+
|
|
205
|
+
// Flush pending microtasks without waiting for full settlement
|
|
206
|
+
await flushMicrotasks();
|
|
207
|
+
|
|
208
|
+
// Check intermediate state
|
|
209
|
+
assertFact(system, "phase", "loading");
|
|
210
|
+
|
|
211
|
+
// Now settle fully
|
|
212
|
+
await system.settle();
|
|
213
|
+
|
|
214
|
+
assertFact(system, "phase", "done");
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Common Mistakes
|
|
219
|
+
|
|
220
|
+
### Testing real resolvers instead of mocking
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// WRONG — tests hit real APIs, slow and flaky
|
|
224
|
+
const system = createTestSystem(myModule);
|
|
225
|
+
system.facts.needsFetch = true;
|
|
226
|
+
await system.settle(); // Makes real HTTP call
|
|
227
|
+
|
|
228
|
+
// CORRECT — mock the resolver
|
|
229
|
+
const system = createTestSystem(myModule, {
|
|
230
|
+
mockResolvers: [mockResolver("FETCH", async (req, context) => {
|
|
231
|
+
context.facts.data = { mocked: true };
|
|
232
|
+
})],
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Forgetting to settle before asserting async results
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// WRONG — resolver hasn't completed yet
|
|
240
|
+
system.facts.trigger = true;
|
|
241
|
+
assertFact(system, "result", "done"); // Fails!
|
|
242
|
+
|
|
243
|
+
// CORRECT — wait for resolution
|
|
244
|
+
system.facts.trigger = true;
|
|
245
|
+
await system.settle();
|
|
246
|
+
assertFact(system, "result", "done");
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Using ctx instead of context in mock resolvers
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// WRONG
|
|
253
|
+
mockResolver("FETCH", async (req, ctx) => { /* ... */ }),
|
|
254
|
+
|
|
255
|
+
// CORRECT
|
|
256
|
+
mockResolver("FETCH", async (req, context) => { /* ... */ }),
|
|
257
|
+
```
|