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, projections and queries run for real, calls to stubbed external systems wait until you answer them, calls to the others run through their adapters while the story records them, and a clock you control stamps commands and fires scheduled ones. A story fails on any committed event, rejection, external system call or saga defect it doesn’t account for.
Story: Writing a story
Section titled “Story: Writing a story”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 })), ),);Story: External systems and the clock
Section titled “Story: External systems and the clock”Answer an external system call, then move the clock
Story.call(FraudCheck.score, { accountId: account.id, amount: 60_000 }),Story.succeed(FraudCheck.score, { risk: 0.95 }),Story.clock("2026-01-02T09:00:00Z"),Story.advance("1 day"),Name calls when several are waiting
Story.call("a-1", FraudCheck.score, { accountId: a1.id }),Story.call("a-2", FraudCheck.score, { accountId: a2.id }),Story.succeed("a-2", FraudCheck.score, { risk: 0.1 }),Story.succeed("a-1", FraudCheck.score, { risk: 0.95 }),Assert a real call to a system that is not stubbed
const story = Story.make(Bank, { layer: BankHandlers, stub: [FraudCheck] });
story( Story.when(account, new RecordSettlement({ settlementId: "st_1", amount: 500 })), Story.call(Payments.refund, { settlementId: "st_1", amount: 500 }), Story.returned(Payments.refund, { refundId: "re_st_1" }),).pipe(Effect.provide(StripePayments));Fail a call with the operation’s declared error, or a defect
Story.call(LockVendor.deleteCode, { codeId: "pc_1" }),Story.fail(LockVendor.deleteCode, new LockVendorRefused({ reason: "gateway-lost" })),Story.call(LockVendor.deleteCode, { codeId: "pc_2" }),Story.die(LockVendor.deleteCode, new Error("socket closed")),Story: Queries
Section titled “Story: Queries”Story.provide(CurrentActor, sam),Story.query(AccountLedger, { account: account.id }, [{ kind: "MoneyDeposited", amount: 100 }]),Story.provide(CurrentActor, alex),Story.query(AccountLedger, { account: account.id }),Story.rejected(new NotOwner()),Story: Sagas
Section titled “Story: Sagas”Story.saga(ExecuteTransfer, transfer, { _tag: "Running" }),Story.signal(ExecuteTransfer, transfer, Decision, "approved"),