@directive-run/knowledge 1.13.0 → 1.15.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/README.md +50 -15
- package/ai/ai-adapters.md +2 -0
- package/ai/ai-agents-streaming.md +182 -149
- package/ai/ai-budget-resilience.md +299 -132
- package/ai/ai-communication.md +215 -197
- package/ai/ai-debug-observability.md +253 -173
- package/ai/ai-guardrails-memory.md +185 -153
- package/ai/ai-mcp-rag.md +199 -199
- package/ai/ai-multi-agent.md +247 -153
- package/ai/ai-orchestrator.md +344 -114
- package/ai/ai-security.md +282 -180
- package/ai/ai-tasks.md +3 -1
- package/ai/ai-testing-evals.md +357 -256
- package/core/anti-patterns.md +9 -2
- package/core/constraints.md +6 -2
- package/core/core-patterns.md +2 -0
- package/core/error-boundaries.md +2 -0
- package/core/history.md +2 -0
- package/core/multi-module.md +8 -3
- package/core/naming.md +15 -6
- package/core/plugins.md +2 -0
- package/core/react-adapter.md +250 -174
- package/core/resolvers.md +2 -0
- package/core/schema-types.md +2 -0
- package/core/system-api.md +2 -0
- package/core/testing.md +251 -143
- package/examples/checkers.ts +15 -16
- package/examples/contact-form.ts +2 -2
- package/examples/counter-react.ts +1 -1
- package/examples/counter-svelte.ts +1 -1
- package/examples/counter-vue.ts +1 -1
- package/examples/counter.ts +1 -1
- package/examples/data-triggers.ts +4 -4
- package/examples/feature-flags.ts +2 -2
- package/examples/form-wizard.ts +2 -2
- package/examples/newsletter.ts +2 -2
- package/examples/server.ts +2 -2
- package/examples/shopping-cart.ts +1 -1
- package/examples/topic-guard.ts +1 -1
- package/package.json +3 -3
- package/sitemap.md +26 -3
- package/examples/debounce-constraints.ts +0 -95
- package/examples/multi-module.ts +0 -58
package/core/testing.md
CHANGED
|
@@ -1,245 +1,286 @@
|
|
|
1
1
|
# Testing
|
|
2
2
|
|
|
3
|
+
> Covers `@directive-run/core/testing` — `createTestSystem`, mock resolvers, assertion helpers, coverage tracker, test observer.
|
|
4
|
+
|
|
3
5
|
Testing utilities for Directive modules and systems. Import from `@directive-run/core/testing`.
|
|
4
6
|
|
|
5
|
-
## Decision
|
|
7
|
+
## Decision tree
|
|
6
8
|
|
|
7
9
|
```
|
|
8
10
|
What are you testing?
|
|
9
|
-
├── A single module in isolation
|
|
10
|
-
├── Multiple modules together
|
|
11
|
-
├── A constraint fires correctly
|
|
12
|
-
├── A resolver mutates facts
|
|
13
|
-
├── A derivation computes correctly
|
|
14
|
-
├── Async settling
|
|
15
|
-
|
|
11
|
+
├── A single module in isolation → createTestSystem({ module })
|
|
12
|
+
├── Multiple modules together → createTestSystem({ modules: {...} })
|
|
13
|
+
├── A constraint fires correctly → set facts, system.assertRequirement(type)
|
|
14
|
+
├── A resolver mutates facts → mocks.resolvers[TYPE], dispatch, await waitForIdle, assertFactSet
|
|
15
|
+
├── A derivation computes correctly → set facts, read system.derive.x
|
|
16
|
+
├── Async settling with retry/timeouts → await system.waitForIdle() (uses real timers)
|
|
17
|
+
├── Coverage tracking → createCoverageTracker(system)
|
|
18
|
+
└── Observation event capture → createTestObserver(system)
|
|
16
19
|
```
|
|
17
20
|
|
|
18
|
-
## Creating
|
|
21
|
+
## Creating a test system
|
|
22
|
+
|
|
23
|
+
`createTestSystem` takes an options OBJECT — the same shape as `createSystem` — and adds an optional `mocks.resolvers` map keyed by requirement type. There is no separate `createTestSystemFromModules` factory; the same overload handles both single-module (`module:`) and namespaced (`modules:`) systems.
|
|
19
24
|
|
|
20
25
|
```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);
|
|
26
|
+
import { createTestSystem } from "@directive-run/core/testing";
|
|
35
27
|
|
|
36
|
-
//
|
|
37
|
-
const system = createTestSystem(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
28
|
+
// Single module
|
|
29
|
+
const system = createTestSystem({
|
|
30
|
+
module: counterModule,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Namespaced (multi-module)
|
|
34
|
+
const system = createTestSystem({
|
|
35
|
+
modules: { auth: authModule, cart: cartModule },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// With mocks — keyed by requirement type, value is a MockResolverOptions
|
|
39
|
+
const system = createTestSystem({
|
|
40
|
+
module: userModule,
|
|
41
|
+
mocks: {
|
|
42
|
+
resolvers: {
|
|
43
|
+
FETCH_USER: {
|
|
44
|
+
resolve: (req, context) => {
|
|
45
|
+
context.facts.user = { id: req.userId, name: "Test User" };
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
42
50
|
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
A `TestSystem` extends the production `SingleModuleSystem<S>` / `NamespacedSystem<Modules>` with extra fields for observability:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
system.waitForIdle(maxWait?); // wait for all in-flight resolvers (default 5000ms; throws on timeout)
|
|
57
|
+
system.eventHistory; // every dispatched event
|
|
58
|
+
system.resolverCalls; // Map<requirementType, Requirement[]>
|
|
59
|
+
system.allRequirements; // every requirement generated (both resolved and pending)
|
|
60
|
+
system.getFactsHistory(); // FactChangeRecord[] since start / last reset
|
|
61
|
+
system.resetFactsHistory();
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Assertions are METHODS on the test system
|
|
65
|
+
|
|
66
|
+
Assertion helpers live on the test-system instance, not as top-level imports. `assertFact` / `assertDerivation` / `assertRequirement` as top-level imports do not exist.
|
|
43
67
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
})] },
|
|
50
|
-
);
|
|
68
|
+
```typescript
|
|
69
|
+
system.assertRequirement("FETCH_USER"); // a requirement of this type was generated
|
|
70
|
+
system.assertResolverCalled("FETCH_USER", 1); // the resolver was called N times
|
|
71
|
+
system.assertFactSet("phase", "loaded"); // the fact was set to a specific value
|
|
72
|
+
system.assertFactChanges("count", 3); // the fact was changed N times
|
|
51
73
|
```
|
|
52
74
|
|
|
53
|
-
|
|
75
|
+
For derivation assertions, just read the derivation value and use your test framework's expect / equality helpers:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { expect } from "vitest";
|
|
79
|
+
expect(system.derive.isOverBudget).toBe(true);
|
|
80
|
+
```
|
|
54
81
|
|
|
55
|
-
|
|
82
|
+
## Testing constraints
|
|
56
83
|
|
|
57
84
|
```typescript
|
|
58
85
|
import { describe, it, expect } from "vitest";
|
|
59
|
-
import { createTestSystem
|
|
86
|
+
import { createTestSystem } from "@directive-run/core/testing";
|
|
60
87
|
|
|
61
88
|
describe("fetchWhenAuth constraint", () => {
|
|
62
89
|
it("emits FETCH_USER when authenticated without profile", () => {
|
|
63
|
-
const system = createTestSystem(userModule);
|
|
90
|
+
const system = createTestSystem({ module: userModule });
|
|
64
91
|
|
|
65
|
-
// Set facts to satisfy the constraint's when()
|
|
66
92
|
system.facts.isAuthenticated = true;
|
|
67
93
|
system.facts.profile = null;
|
|
68
94
|
|
|
69
|
-
|
|
70
|
-
assertRequirement(system, "FETCH_USER");
|
|
95
|
+
system.assertRequirement("FETCH_USER");
|
|
71
96
|
});
|
|
72
97
|
|
|
73
|
-
it("does NOT emit when already has profile", () => {
|
|
74
|
-
const system = createTestSystem(
|
|
98
|
+
it("does NOT emit when already has a profile", () => {
|
|
99
|
+
const system = createTestSystem({
|
|
100
|
+
module: userModule,
|
|
75
101
|
initialFacts: {
|
|
76
102
|
isAuthenticated: true,
|
|
77
103
|
profile: { id: "1", name: "Alice" },
|
|
78
104
|
},
|
|
79
105
|
});
|
|
80
106
|
|
|
81
|
-
// No requirement should exist
|
|
82
107
|
const inspection = system.inspect();
|
|
83
|
-
const fetchReqs = inspection.
|
|
84
|
-
(r) => r.type === "FETCH_USER",
|
|
85
|
-
);
|
|
108
|
+
const fetchReqs = inspection.unmet.filter((r) => r.type === "FETCH_USER");
|
|
86
109
|
expect(fetchReqs).toHaveLength(0);
|
|
87
110
|
});
|
|
88
111
|
});
|
|
89
112
|
```
|
|
90
113
|
|
|
91
|
-
|
|
114
|
+
`SystemInspection` exposes `unmet`, `inflight`, and other typed accessors — there is no flat `inspection.requirements` field. Filter the right list (`unmet` for pending requirements, `inflight` for in-progress).
|
|
92
115
|
|
|
93
|
-
|
|
116
|
+
## Testing resolvers
|
|
94
117
|
|
|
95
118
|
```typescript
|
|
96
119
|
describe("fetchUser resolver", () => {
|
|
97
120
|
it("stores fetched user in facts", async () => {
|
|
98
|
-
const system = createTestSystem(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
121
|
+
const system = createTestSystem({
|
|
122
|
+
module: userModule,
|
|
123
|
+
mocks: {
|
|
124
|
+
resolvers: {
|
|
125
|
+
FETCH_USER: {
|
|
126
|
+
resolve: (req, context) => {
|
|
127
|
+
context.facts.user = { id: req.userId, name: "Mocked User" };
|
|
128
|
+
context.facts.phase = "loaded";
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
106
133
|
});
|
|
107
134
|
|
|
108
135
|
// Trigger the constraint that emits FETCH_USER
|
|
109
136
|
system.facts.isAuthenticated = true;
|
|
110
137
|
system.facts.user = null;
|
|
111
138
|
|
|
112
|
-
// Wait for resolver to
|
|
113
|
-
await system.
|
|
139
|
+
// Wait for the resolver to finish (default 5s timeout)
|
|
140
|
+
await system.waitForIdle();
|
|
114
141
|
|
|
115
|
-
|
|
116
|
-
|
|
142
|
+
system.assertFactSet("user", { id: expect.anything(), name: "Mocked User" });
|
|
143
|
+
system.assertFactSet("phase", "loaded");
|
|
117
144
|
});
|
|
118
145
|
});
|
|
119
146
|
```
|
|
120
147
|
|
|
121
|
-
|
|
148
|
+
`mocks.resolvers[TYPE]` accepts a `MockResolverOptions` — the most common shape is `{ resolve: (req, context) => {…} }`. You can also pass full mock-resolver behaviors (`onCall`, `respondWith`, etc.) — see `MockResolverOptions` for the full surface.
|
|
122
149
|
|
|
123
|
-
|
|
150
|
+
For programmatic control of when a mock resolves (e.g. holding a request to test loading states), use the `mockResolver(type)` factory:
|
|
124
151
|
|
|
125
152
|
```typescript
|
|
126
|
-
|
|
127
|
-
it("returns true when total exceeds budget", () => {
|
|
128
|
-
const system = createTestSystem(budgetModule, {
|
|
129
|
-
initialFacts: { total: 150, budget: 100 },
|
|
130
|
-
});
|
|
153
|
+
import { mockResolver } from "@directive-run/core/testing";
|
|
131
154
|
|
|
132
|
-
|
|
133
|
-
});
|
|
155
|
+
const mock = mockResolver("FETCH_USER");
|
|
134
156
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
157
|
+
const system = createTestSystem({
|
|
158
|
+
module: userModule,
|
|
159
|
+
mocks: {
|
|
160
|
+
resolvers: { FETCH_USER: { resolve: mock.handler } },
|
|
161
|
+
},
|
|
162
|
+
});
|
|
139
163
|
|
|
140
|
-
|
|
141
|
-
|
|
164
|
+
system.facts.isAuthenticated = true;
|
|
165
|
+
|
|
166
|
+
// Now mock.pending contains the in-flight request — resolve it manually
|
|
167
|
+
expect(mock.calls).toHaveLength(1);
|
|
168
|
+
mock.pending[0].resolve({ id: "u1", name: "Alice" });
|
|
169
|
+
|
|
170
|
+
await system.waitForIdle();
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Testing derivations
|
|
142
174
|
|
|
175
|
+
Set facts, read the derived value via `system.derive`.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
describe("isOverBudget derivation", () => {
|
|
143
179
|
it("recomputes when facts change", () => {
|
|
144
|
-
const system = createTestSystem(
|
|
180
|
+
const system = createTestSystem({
|
|
181
|
+
module: budgetModule,
|
|
145
182
|
initialFacts: { total: 50, budget: 100 },
|
|
146
183
|
});
|
|
147
184
|
|
|
148
|
-
|
|
185
|
+
expect(system.derive.isOverBudget).toBe(false);
|
|
149
186
|
|
|
150
187
|
system.facts.total = 200;
|
|
151
188
|
|
|
152
|
-
|
|
189
|
+
expect(system.derive.isOverBudget).toBe(true);
|
|
153
190
|
});
|
|
154
191
|
});
|
|
155
192
|
```
|
|
156
193
|
|
|
157
|
-
## Async
|
|
194
|
+
## Async testing with fake timers
|
|
158
195
|
|
|
159
|
-
|
|
196
|
+
`waitForIdle` uses real timers by default. For tests that depend on retry backoff / TTLs / timers without burning wall-clock seconds, use `createFakeTimers()` (a vitest/jest-compatible controller) or your test framework's fake-timer integration.
|
|
160
197
|
|
|
161
198
|
```typescript
|
|
162
|
-
import { describe, it, vi } from "vitest";
|
|
163
|
-
import { createTestSystem
|
|
199
|
+
import { describe, it, expect, vi } from "vitest";
|
|
200
|
+
import { createTestSystem } from "@directive-run/core/testing";
|
|
164
201
|
|
|
165
202
|
describe("retry behavior", () => {
|
|
166
203
|
it("retries on failure with exponential backoff", async () => {
|
|
167
204
|
vi.useFakeTimers();
|
|
168
205
|
let attempts = 0;
|
|
169
206
|
|
|
170
|
-
const system = createTestSystem(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
207
|
+
const system = createTestSystem({
|
|
208
|
+
module: myModule,
|
|
209
|
+
mocks: {
|
|
210
|
+
resolvers: {
|
|
211
|
+
FETCH_DATA: {
|
|
212
|
+
resolve: async (req, context) => {
|
|
213
|
+
attempts += 1;
|
|
214
|
+
if (attempts < 3) {
|
|
215
|
+
throw new Error("Temporary failure");
|
|
216
|
+
}
|
|
217
|
+
context.facts.data = "success";
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
180
222
|
});
|
|
181
223
|
|
|
182
224
|
system.facts.needsData = true;
|
|
183
225
|
|
|
184
|
-
//
|
|
185
|
-
await
|
|
226
|
+
// Advance through retry delays, then drain
|
|
227
|
+
await vi.runAllTimersAsync();
|
|
228
|
+
await system.waitForIdle();
|
|
186
229
|
|
|
187
230
|
expect(attempts).toBe(3);
|
|
188
|
-
|
|
231
|
+
system.assertFactSet("data", "success");
|
|
189
232
|
|
|
190
233
|
vi.useRealTimers();
|
|
191
234
|
});
|
|
192
235
|
});
|
|
193
236
|
```
|
|
194
237
|
|
|
195
|
-
##
|
|
238
|
+
## Microtask flush
|
|
196
239
|
|
|
197
|
-
|
|
240
|
+
For checking intermediate state without fully settling, use `flushMicrotasks()`.
|
|
198
241
|
|
|
199
242
|
```typescript
|
|
243
|
+
import { flushMicrotasks } from "@directive-run/core/testing";
|
|
244
|
+
|
|
200
245
|
it("processes intermediate state", async () => {
|
|
201
|
-
const system = createTestSystem(myModule);
|
|
246
|
+
const system = createTestSystem({ module: myModule });
|
|
202
247
|
|
|
203
248
|
system.facts.trigger = true;
|
|
204
249
|
|
|
205
|
-
//
|
|
250
|
+
// Process one round of microtasks without waiting for resolvers
|
|
206
251
|
await flushMicrotasks();
|
|
207
252
|
|
|
208
|
-
|
|
209
|
-
assertFact(system, "phase", "loading");
|
|
253
|
+
system.assertFactSet("phase", "loading");
|
|
210
254
|
|
|
211
|
-
|
|
212
|
-
await system.settle();
|
|
255
|
+
await system.waitForIdle();
|
|
213
256
|
|
|
214
|
-
|
|
257
|
+
system.assertFactSet("phase", "done");
|
|
215
258
|
});
|
|
216
259
|
```
|
|
217
260
|
|
|
218
|
-
## Coverage
|
|
219
|
-
|
|
220
|
-
Track which constraints, resolvers, effects, and derivations are exercised:
|
|
261
|
+
## Coverage tracking
|
|
221
262
|
|
|
222
263
|
```typescript
|
|
223
264
|
import { createCoverageTracker } from "@directive-run/core/testing";
|
|
224
265
|
|
|
225
|
-
const
|
|
266
|
+
const tracker = createCoverageTracker(system);
|
|
226
267
|
|
|
227
|
-
await run(async () => {
|
|
268
|
+
await tracker.run(async () => {
|
|
228
269
|
system.facts.userId = 123;
|
|
229
|
-
await system.
|
|
270
|
+
await system.waitForIdle();
|
|
230
271
|
});
|
|
231
272
|
|
|
232
|
-
const coverage = report();
|
|
233
|
-
// coverage.constraintCoverage
|
|
234
|
-
// coverage.resolverCoverage
|
|
235
|
-
// coverage.effectCoverage
|
|
236
|
-
// coverage.derivationCoverage
|
|
237
|
-
// coverage.constraintsMissed
|
|
273
|
+
const coverage = tracker.report();
|
|
274
|
+
// coverage.constraintCoverage — 0-1
|
|
275
|
+
// coverage.resolverCoverage — 0-1
|
|
276
|
+
// coverage.effectCoverage — 0-1
|
|
277
|
+
// coverage.derivationCoverage — 0-1
|
|
278
|
+
// coverage.constraintsMissed — Set<string> of ids never triggered
|
|
238
279
|
```
|
|
239
280
|
|
|
240
|
-
## Test
|
|
281
|
+
## Test observer
|
|
241
282
|
|
|
242
|
-
|
|
283
|
+
Capture observation events for assertion-based testing:
|
|
243
284
|
|
|
244
285
|
```typescript
|
|
245
286
|
import { createTestObserver } from "@directive-run/core/testing";
|
|
@@ -247,52 +288,119 @@ import { createTestObserver } from "@directive-run/core/testing";
|
|
|
247
288
|
const observer = createTestObserver(system);
|
|
248
289
|
|
|
249
290
|
system.facts.count = 5;
|
|
250
|
-
await system.
|
|
291
|
+
await system.waitForIdle();
|
|
251
292
|
|
|
252
293
|
const evals = observer.ofType("constraint.evaluate");
|
|
253
294
|
expect(evals).toHaveLength(1);
|
|
254
295
|
|
|
255
|
-
observer.clear(); //
|
|
256
|
-
observer.dispose();
|
|
296
|
+
observer.clear(); // reset captured events
|
|
297
|
+
observer.dispose(); // stop capturing
|
|
257
298
|
```
|
|
258
299
|
|
|
259
|
-
##
|
|
300
|
+
## Anti-patterns
|
|
260
301
|
|
|
261
|
-
###
|
|
302
|
+
### `createTestSystem(myModule)` (module as positional arg)
|
|
262
303
|
|
|
263
304
|
```typescript
|
|
264
|
-
// WRONG
|
|
305
|
+
// WRONG — createTestSystem takes an options object, not a positional module
|
|
265
306
|
const system = createTestSystem(myModule);
|
|
266
|
-
system.facts.needsFetch = true;
|
|
267
|
-
await system.settle(); // Makes real HTTP call
|
|
268
307
|
|
|
269
|
-
// CORRECT
|
|
308
|
+
// CORRECT — wrap in { module: ... }
|
|
309
|
+
const system = createTestSystem({ module: myModule });
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### `createTestSystemFromModules({ a, b })`
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// WRONG — there is no separate factory; the same createTestSystem handles both forms
|
|
316
|
+
const system = createTestSystemFromModules({ auth, cart });
|
|
317
|
+
|
|
318
|
+
// CORRECT — pass `modules:`
|
|
319
|
+
const system = createTestSystem({ modules: { auth, cart } });
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Importing assertions as top-level helpers
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// WRONG — assertFact / assertDerivation / assertRequirement are not top-level exports
|
|
326
|
+
import { assertFact, assertDerivation, assertRequirement } from "@directive-run/core/testing";
|
|
327
|
+
assertRequirement(system, "FETCH_USER");
|
|
328
|
+
assertFact(system, "phase", "loaded");
|
|
329
|
+
|
|
330
|
+
// CORRECT — they live on the test system instance
|
|
331
|
+
system.assertRequirement("FETCH_USER");
|
|
332
|
+
system.assertFactSet("phase", "loaded");
|
|
333
|
+
|
|
334
|
+
// For derivation reads, use expect() directly
|
|
335
|
+
expect(system.derive.isOverBudget).toBe(true);
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### `mockResolvers: [mockResolver(TYPE, fn)]` (positional array)
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
// WRONG — mocks are keyed by type, not an array; and mockResolver only takes the type
|
|
270
342
|
const system = createTestSystem(myModule, {
|
|
271
|
-
mockResolvers: [
|
|
272
|
-
context
|
|
273
|
-
|
|
343
|
+
mockResolvers: [
|
|
344
|
+
mockResolver("FETCH_USER", async (req, context) => { /* ... */ }),
|
|
345
|
+
],
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// CORRECT — mocks.resolvers is a Record<type, MockResolverOptions>
|
|
349
|
+
const system = createTestSystem({
|
|
350
|
+
module: myModule,
|
|
351
|
+
mocks: {
|
|
352
|
+
resolvers: {
|
|
353
|
+
FETCH_USER: {
|
|
354
|
+
resolve: async (req, context) => { /* ... */ },
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
},
|
|
274
358
|
});
|
|
275
359
|
```
|
|
276
360
|
|
|
277
|
-
###
|
|
361
|
+
### `inspection.requirements`
|
|
278
362
|
|
|
279
363
|
```typescript
|
|
280
|
-
// WRONG
|
|
281
|
-
|
|
282
|
-
|
|
364
|
+
// WRONG — SystemInspection has `unmet` and `inflight`, not a flat `requirements`
|
|
365
|
+
inspection.requirements.filter((r) => r.type === "FETCH_USER")
|
|
366
|
+
|
|
367
|
+
// CORRECT — filter the right list
|
|
368
|
+
inspection.unmet.filter((r) => r.type === "FETCH_USER") // pending
|
|
369
|
+
inspection.inflight.filter((r) => r.type === "FETCH_USER") // in-progress
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Calling `system.settle()` instead of `system.waitForIdle()`
|
|
283
373
|
|
|
284
|
-
|
|
285
|
-
|
|
374
|
+
`system.settle(maxWait?)` exists on production systems. Test systems inherit it, but the recommended assertion-friendly drain on a `TestSystem` is `waitForIdle` — it shares the same semantics and integrates with the resolver-call tracking used by `assertResolverCalled`.
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
// FINE — both work, but waitForIdle is the testing-flavored verb
|
|
286
378
|
await system.settle();
|
|
287
|
-
|
|
379
|
+
await system.waitForIdle();
|
|
288
380
|
```
|
|
289
381
|
|
|
290
|
-
###
|
|
382
|
+
### `ctx` instead of `context`
|
|
291
383
|
|
|
292
384
|
```typescript
|
|
293
385
|
// WRONG
|
|
294
|
-
|
|
386
|
+
resolve: (req, ctx) => { ctx.facts.x = 1; }
|
|
295
387
|
|
|
296
388
|
// CORRECT
|
|
297
|
-
|
|
389
|
+
resolve: (req, context) => { context.facts.x = 1; }
|
|
298
390
|
```
|
|
391
|
+
|
|
392
|
+
## Quick reference
|
|
393
|
+
|
|
394
|
+
| API | Path | Purpose |
|
|
395
|
+
|---|---|---|
|
|
396
|
+
| `createTestSystem(options)` | `@directive-run/core/testing` | Single overload; pass `module:` or `modules:` |
|
|
397
|
+
| `mockResolver(type)` | `@directive-run/core/testing` | Programmatic mock with `calls` / `pending` / `handler` |
|
|
398
|
+
| `createFakeTimers()` | `@directive-run/core/testing` | Vitest/Jest-compatible timer controller |
|
|
399
|
+
| `flushMicrotasks()` | `@directive-run/core/testing` | Advance one microtask round |
|
|
400
|
+
| `createCoverageTracker(system)` | `@directive-run/core/testing` | `run(fn)` + `report()` for constraint/resolver/effect/derivation hit rate |
|
|
401
|
+
| `createTestObserver(system)` | `@directive-run/core/testing` | Capture observation events for assertions |
|
|
402
|
+
| `system.assertRequirement(type)` | instance method | Assert a requirement was generated |
|
|
403
|
+
| `system.assertResolverCalled(type, n?)` | instance method | Assert a resolver was called |
|
|
404
|
+
| `system.assertFactSet(key, value?)` | instance method | Assert a fact value |
|
|
405
|
+
| `system.assertFactChanges(key, n)` | instance method | Assert the change count for a fact |
|
|
406
|
+
| `system.waitForIdle(maxWait?)` | instance method | Wait for in-flight resolvers (default 5000ms) |
|
package/examples/checkers.ts
CHANGED
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
|
|
24
24
|
import {
|
|
25
25
|
type AgentLike,
|
|
26
|
-
CircuitBreakerOpenError,
|
|
27
26
|
type InputGuardrailData,
|
|
28
27
|
type NamedGuardrail,
|
|
29
28
|
type RunResult,
|
|
@@ -31,26 +30,26 @@ import {
|
|
|
31
30
|
createAgentOrchestrator,
|
|
32
31
|
createLengthStreamingGuardrail,
|
|
33
32
|
createMessageBus,
|
|
34
|
-
createMultiAgentOrchestrator,
|
|
35
|
-
createOutputSchemaGuardrail,
|
|
36
33
|
createSemanticCache,
|
|
37
34
|
createSlidingWindowStrategy,
|
|
38
35
|
createStreamingRunner,
|
|
39
36
|
createTestEmbedder,
|
|
40
37
|
estimateCost,
|
|
41
|
-
parallel,
|
|
42
38
|
} from "@directive-run/ai";
|
|
43
39
|
import type { CacheStats } from "@directive-run/ai";
|
|
40
|
+
import { createOutputSchemaGuardrail } from "@directive-run/ai/guardrails";
|
|
44
41
|
import {
|
|
42
|
+
createMultiAgentOrchestrator,
|
|
43
|
+
parallel,
|
|
44
|
+
} from "@directive-run/ai/multi-agent";
|
|
45
|
+
import {
|
|
46
|
+
CircuitBreakerOpenError,
|
|
45
47
|
type CircuitState,
|
|
48
|
+
createAgentMetrics,
|
|
46
49
|
createCircuitBreaker,
|
|
47
50
|
createOTLPExporter,
|
|
48
|
-
} from "@directive-run/core/plugins";
|
|
49
|
-
// createObservability is alpha (not in bundle) — direct source import
|
|
50
|
-
import {
|
|
51
|
-
createAgentMetrics,
|
|
52
51
|
createObservability,
|
|
53
|
-
} from "
|
|
52
|
+
} from "@directive-run/core/plugins";
|
|
54
53
|
import {
|
|
55
54
|
analysisAgent,
|
|
56
55
|
chatAgent,
|
|
@@ -103,7 +102,7 @@ export interface CheckersAI {
|
|
|
103
102
|
cacheStats: CacheStats;
|
|
104
103
|
busMessageCount: number;
|
|
105
104
|
};
|
|
106
|
-
|
|
105
|
+
destroy(): void;
|
|
107
106
|
/** Escape hatch for dashboard rendering */
|
|
108
107
|
readonly observability: ReturnType<typeof createObservability> | null;
|
|
109
108
|
}
|
|
@@ -323,7 +322,7 @@ export function createCheckersAI(): CheckersAI {
|
|
|
323
322
|
|
|
324
323
|
function buildMoveInput(
|
|
325
324
|
board: Board,
|
|
326
|
-
|
|
325
|
+
_player: Player,
|
|
327
326
|
legalMoves: Move[],
|
|
328
327
|
humanMoveDesc?: string,
|
|
329
328
|
): string {
|
|
@@ -564,7 +563,7 @@ export function createCheckersAI(): CheckersAI {
|
|
|
564
563
|
};
|
|
565
564
|
}
|
|
566
565
|
|
|
567
|
-
function
|
|
566
|
+
function destroy(): void {
|
|
568
567
|
clearInterval(otlpInterval);
|
|
569
568
|
// Flush OTLP one final time
|
|
570
569
|
try {
|
|
@@ -574,9 +573,9 @@ export function createCheckersAI(): CheckersAI {
|
|
|
574
573
|
} catch {
|
|
575
574
|
// Best-effort flush on dispose
|
|
576
575
|
}
|
|
577
|
-
orchestrator.
|
|
578
|
-
multi.
|
|
579
|
-
obs.
|
|
576
|
+
orchestrator.destroy();
|
|
577
|
+
multi.destroy();
|
|
578
|
+
void obs.destroy();
|
|
580
579
|
}
|
|
581
580
|
|
|
582
581
|
return {
|
|
@@ -584,7 +583,7 @@ export function createCheckersAI(): CheckersAI {
|
|
|
584
583
|
sendChat,
|
|
585
584
|
reset,
|
|
586
585
|
getState,
|
|
587
|
-
|
|
586
|
+
destroy,
|
|
588
587
|
get observability() {
|
|
589
588
|
return obs;
|
|
590
589
|
},
|
package/examples/contact-form.ts
CHANGED
|
@@ -260,7 +260,7 @@ const contactForm = createModule("contact-form", {
|
|
|
260
260
|
resolvers: {
|
|
261
261
|
sendMessage: {
|
|
262
262
|
requirement: "SEND_MESSAGE",
|
|
263
|
-
resolve: async (
|
|
263
|
+
resolve: async (_req, context) => {
|
|
264
264
|
log(
|
|
265
265
|
`Sending: ${context.facts.name} <${context.facts.email}> [${context.facts.subject}]`,
|
|
266
266
|
);
|
|
@@ -285,7 +285,7 @@ const contactForm = createModule("contact-form", {
|
|
|
285
285
|
|
|
286
286
|
resetAfterDelay: {
|
|
287
287
|
requirement: "RESET_AFTER_DELAY",
|
|
288
|
-
resolve: async (
|
|
288
|
+
resolve: async (_req, context) => {
|
|
289
289
|
log("Auto-resetting in 3 seconds...");
|
|
290
290
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
291
291
|
context.facts.name = "";
|