Skip to content

Domain.Story reference

Source: packages/domain/src/Story.ts

Given / when / then specifications of a domain model, run against the real model in memory: policies run for real, external system calls wait until you resolve them, and a clock you control stamps commands and fires scheduled ones. A story fails on any rejection, external system call or saga defect it doesn’t account for.

import { it } from "@effect/vitest";
import { Story } from "@alchemy.run/domain/Story";
import { Account, InsufficientFunds, MoneyDeposited, MoneyWithdrawn, WithdrawMoney } from "../src/Account.ts";
import { Bank, BankPolicies } from "../src/Bank.ts";
const story = Story.make(Bank, { layer: BankPolicies });
const account = Account.ref("a-1");
it.effect("withdraws within the balance, and refuses beyond it", () =>
story(
Story.given(account, new MoneyDeposited({ amount: 100, balanceAfter: 100 })),
Story.when(account, new WithdrawMoney({ amount: 30 })),
Story.then(account, new MoneyWithdrawn({ amount: 30, balanceAfter: 70 })),
Story.replied({ balance: 70 }),
Story.when(account, new WithdrawMoney({ amount: 100 })),
Story.rejected(new InsufficientFunds({ balance: 70, requested: 100 })),
),
);

Answer an external system call, then move the clock

Story.expectCall(FraudCheck.score, { accountId: account.id, amount: 60_000 }),
Story.resolve(FraudCheck.score, { risk: 0.95 }),
Story.clock("2026-01-02T09:00:00Z"),

Name calls when several are waiting

Story.expectCall("a-1", FraudCheck.score, { accountId: a1.id }),
Story.expectCall("a-2", FraudCheck.score, { accountId: a2.id }),
Story.resolve("a-2", FraudCheck.score, { risk: 0.1 }),
Story.resolve("a-1", FraudCheck.score, { risk: 0.95 }),
Story.saga(ExecuteTransfer, transfer, { _tag: "Running" }),
Story.signal(ExecuteTransfer, transfer, Decision, "approved"),