| name | testing-directive-code |
| 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. |
Testing Directive Code
Prerequisites
This skill applies when the project uses @directive-run/core. If not found in package.json, suggest installing it: npm install @directive-run/core.
When Claude Should Use This Skill
Auto-invoke when the user:
- Says "write a test", "add tests for this module", or "how do I test this resolver"
- Shows a Directive module and asks how to verify its behavior
- Asks about mocking resolvers, fake timers, or asserting facts in tests
- Asks about testing time-travel or snapshot behavior
Do NOT invoke when:
- Writing the module under test (see
writing-directive-modules.md)
- Asking about system composition (see
building-directive-systems.md)
- Asking about constraint patterns without a testing context (see
writing-directive-constraints.md)
Decision Tree: What Are You Testing?
What are you testing?
├── A single module in isolation → createTestSystem(module)
├── Multiple modules together → createTestSystemFromModules({ a, b })
├── A constraint fires correctly → Set facts, call assertRequirement
├── A resolver mutates facts → Mock resolver, trigger, settle, assertFact
├── A derivation computes correctly → Set facts, call assertDerivation
├── Async/retry behavior → settleWithFakeTimers(system, vi)
├── Intermediate state (mid-resolve) → flushMicrotasks()
└── Time-travel / snapshots → Enable history in createTestSystem options
Imports
import {
createTestSystem,
createTestSystemFromModules,
mockResolver,
flushMicrotasks,
settleWithFakeTimers,
assertFact,
assertDerivation,
assertRequirement,
} from "@directive-run/core/testing";
Creating Test Systems
Single module
const system = createTestSystem(myModule);
const system = createTestSystem(myModule, {
initialFacts: { count: 5, phase: "loading" },
});
const system = createTestSystem(myModule, {
mockResolvers: [
mockResolver("FETCH_USER", async (req, context) => {
context.facts.user = { id: req.userId, name: "Test User" };
}),
],
});
const system = createTestSystem(myModule, {
initialFacts: { isAuthenticated: true, profile: null },
mockResolvers: [
mockResolver("FETCH_PROFILE", async (req, context) => {
context.facts.profile = { bio: "Test bio" };
}),
],
});
Multi-module
const system = createTestSystemFromModules(
{ auth: authModule, cart: cartModule },
{
mockResolvers: [
mockResolver("AUTHENTICATE", async (req, context) => {
context.facts.auth.token = "test-token";
context.facts.auth.isAuthenticated = true;
}),
],
},
);
Testing Constraints
Set facts to satisfy the constraint's when(), then assert the requirement was emitted.
describe("fetchWhenAuth constraint", () => {
it("emits FETCH_USER when authenticated without profile", () => {
const system = createTestSystem(userModule);
system.facts.isAuthenticated = true;
system.facts.profile = null;
assertRequirement(system, "FETCH_USER");
});
it("does NOT emit when profile already loaded", () => {
const system = createTestSystem(userModule, {
initialFacts: {
isAuthenticated: true,
profile: { id: "1", name: "Alice" },
},
});
const { requirements } = system.inspect();
const fetchReqs = requirements.filter((r) => r.type === "FETCH_USER");
expect(fetchReqs).toHaveLength(0);
});
it("emits dynamic requirement with correct payload", () => {
const system = createTestSystem(userModule, {
initialFacts: { isAuthenticated: true, userId: "u-42", profile: null },
});
assertRequirement(system, "FETCH_USER", { userId: "u-42" });
});
});
Testing Resolvers
Mock the resolver, trigger the constraint, settle, then assert fact mutations.
describe("fetchUser resolver", () => {
it("stores fetched user and updates phase", async () => {
const system = createTestSystem(userModule, {
mockResolvers: [
mockResolver("FETCH_USER", async (req, context) => {
context.facts.user = { id: req.userId, name: "Mocked User" };
context.facts.phase = "loaded";
}),
],
});
system.facts.isAuthenticated = true;
system.facts.user = null;
await system.settle();
assertFact(system, "user", { id: expect.any(String), name: "Mocked User" });
assertFact(system, "phase", "loaded");
});
it("handles resolver error gracefully", async () => {
const system = createTestSystem(userModule, {
mockResolvers: [
mockResolver("FETCH_USER", async () => {
throw new Error("Network failure");
}),
],
});
system.facts.isAuthenticated = true;
system.facts.user = null;
await system.settle();
assertFact(system, "phase", "error");
});
});
Testing Derivations
Set facts, then read the derived value. No need to settle – derivations are synchronous.
describe("isOverBudget derivation", () => {
it("returns true when total exceeds budget", () => {
const system = createTestSystem(budgetModule, {
initialFacts: { total: 150, budget: 100 },
});
assertDerivation(system, "isOverBudget", true);
});
it("recomputes reactively when facts change", () => {
const system = createTestSystem(budgetModule, {
initialFacts: { total: 50, budget: 100 },
});
assertDerivation(system, "isOverBudget", false);
system.facts.total = 200;
assertDerivation(system, "isOverBudget", true);
});
});
Testing with Fake Timers
Use settleWithFakeTimers when resolvers use retry delays, timeouts, or exponential backoff.
import { describe, it, expect, vi } from "vitest";
import { createTestSystem, settleWithFakeTimers, mockResolver, assertFact } from "@directive-run/core/testing";
describe("retry behavior", () => {
it("retries on failure with exponential backoff", async () => {
vi.useFakeTimers();
let attempts = 0;
const system = createTestSystem(myModule, {
mockResolvers: [
mockResolver("FETCH_DATA", async (req, context) => {
attempts += 1;
if (attempts < 3) {
throw new Error("Temporary failure");
}
context.facts.data = "success";
}),
],
});
system.facts.needsData = true;
await settleWithFakeTimers(system, vi);
expect(attempts).toBe(3);
assertFact(system, "data", "success");
vi.useRealTimers();
});
});
Testing Intermediate Async State
Use flushMicrotasks() to process pending promises without fully settling.
it("shows loading phase during resolution", async () => {
const system = createTestSystem(myModule, {
mockResolvers: [
mockResolver("FETCH_DATA", async (req, context) => {
context.facts.phase = "loading";
await new Promise((resolve) => setTimeout(resolve, 100));
context.facts.phase = "done";
}),
],
});
system.facts.trigger = true;
await flushMicrotasks();
assertFact(system, "phase", "loading");
await system.settle();
assertFact(system, "phase", "done");
});
Testing Effects
Effects run after reconciliation. Settle first, then check side effects.
describe("logPhase effect", () => {
it("logs when phase changes", async () => {
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
const system = createTestSystem(myModule, {
initialFacts: { phase: "idle" },
});
system.facts.phase = "loading";
await system.settle();
expect(consoleSpy).toHaveBeenCalledWith("Phase: loading");
consoleSpy.mockRestore();
});
});
Testing Time-Travel
Enable time-travel in the test system config. In most cases, prefer assertFact over snapshot testing.
describe("history", () => {
it("can undo a fact change", () => {
const system = createTestSystem(editorModule, {
history: { maxSnapshots: 20 },
});
system.facts.text = "Hello";
system.facts.text = "Hello, world";
const history = system.history;
expect(system.facts.text).toBe("Hello, world");
history.goBack();
expect(system.facts.text).toBe("Hello");
});
it("can export and import history", () => {
const system = createTestSystem(myModule, {
history: true,
});
system.facts.count = 1;
system.facts.count = 2;
const history = system.history;
const exported = history.export();
history.import(exported);
history.goTo(0);
expect(system.facts.count).toBe(1);
});
});
Critical Anti-Patterns
1. Testing real resolvers (hitting real APIs)
const system = createTestSystem(myModule);
system.facts.needsFetch = true;
await system.settle();
const system = createTestSystem(myModule, {
mockResolvers: [mockResolver("FETCH", async (req, context) => {
context.facts.data = { mocked: true };
})],
});
2. Forgetting to settle before asserting async results
system.facts.trigger = true;
assertFact(system, "result", "done");
system.facts.trigger = true;
await system.settle();
assertFact(system, "result", "done");
3. Resolver parameter naming
Always use (req, context) – never (req, ctx) or (request, context). Applies to mockResolver callbacks too.
4. Testing implementation details instead of behavior
expect(mockFn).toHaveBeenCalledTimes(1);
assertFact(system, "user", { id: "1", name: "Alice" });
assertDerivation(system, "isLoggedIn", true);
5. Using createSystem instead of createTestSystem
const system = createSystem({ module: myModule });
const system = createTestSystem(myModule);
6. Not resetting fake timers
vi.useFakeTimers();
await settleWithFakeTimers(system, vi);
vi.useFakeTimers();
await settleWithFakeTimers(system, vi);
vi.useRealTimers();
7. Enabling time-travel when you don't need it
8. Forgetting to destroy the system between tests
let system: ReturnType<typeof createTestSystem>;
beforeEach(() => {
system = createTestSystem(myModule);
});
afterEach(() => {
system.destroy();
});
Assertion Quick Reference
assertFact(system, "phase", "done");
assertFact(system, "user", { id: expect.any(String) });
assertDerivation(system, "isLoading", false);
assertDerivation(system, "itemCount", 3);
assertRequirement(system, "FETCH_USER");
assertRequirement(system, "FETCH_USER", { userId: "u-42" });
const { facts, requirements, resolvers, inflight } = system.inspect();
Reference Files
testing.md – full testing API, createTestSystem options, multi-module test setup
history.md – history API, changesets, export/import, performance considerations