Code reviews
A lock test needs a valid chain of related rows before it can check the lock itself. This example shows how to keep that repeated branch in a helper while leaving numbering and time predictable.
Mirrors a real code-review app: the seed is an organization and an owner, and the graph is CodeRepository → PullRequestRecord → CodeReviewRecord → ActiveCodeReviewLock. Source: tests/Zeeq.Biome.Tests/CodeReviews.
The blueprints
// Scope is declared on the sequence: Under(repo) and
// blueprint variants share its progression.
internal static readonly Sequence<int> Numbers = Sequence
.Of(1, n => n + 1)
.Per<CodeRepository>();
internal static readonly Blueprint<CodeReviewSeed, CodeRepository> Repository =
Blueprint.For<CodeReviewSeed, CodeRepository>(ctx =>
new()
{
Id = ctx.NewId("repo"),
Organization = ctx.Seed.Organization,
CreatedAt = ctx.Now,
}
);
// Leave Number unset; derive it first, then Title and Branch from its final value.
internal static readonly ChildBlueprint<
CodeReviewSeed,
CodeRepository,
PullRequestRecord
> PullRequest = Blueprint
.ChildOf<CodeReviewSeed, CodeRepository, PullRequestRecord>(
(repo, ctx) => new() { Id = ctx.NewId("pr"), Repository = repo }
)
.Derive(pr => pr.Number, Numbers)
.Derive(pr => pr.Title, (pr, _) => $"PR {pr.Number}")
.Derive(pr => pr.Branch, (pr, _) => $"feature/{pr.Number}");
// Wire the typed PR parent and walk recorded ancestors to the repository.
internal static readonly ChildBlueprint<
CodeReviewSeed,
PullRequestRecord,
CodeReviewRecord
> Review = Blueprint.ChildOf<
CodeReviewSeed,
PullRequestRecord,
CodeReviewRecord
>(
(pr, ctx) =>
new()
{
Id = ctx.NewId("review"),
PullRequest = pr,
Repository = ctx.Ancestor<CodeRepository>(),
CreatedAt = ctx.Now,
}
);
A locked review in one line
The scenario helper builds the four-level default chain. The test asserts on the lock and the row order in the store.
var store = new RecordingStore();
var biome = TestBiome.Start(out var seed, store);
await biome.AddLockedReview(out var activeLock).BuildAsync();
// The leaf references its ancestors; seed rows precede the four created rows.
await Assert
.That(activeLock.Repository)
.IsSameReferenceAs(activeLock.Review.PullRequest.Repository);
await Assert.That(store.Entities.Count).IsEqualTo(6);
var seed = new CodeReviewSeed(new Organization(), new User());
var now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Ticks - now.Ticks % 10, TimeSpan.Zero);
var repo = new CodeRepository
{
Id = $"repo_{Guid.CreateVersion7():N}",
Organization = seed.Organization,
CreatedAt = now,
};
var pr = new PullRequestRecord
{
Id = $"pr_{Guid.CreateVersion7():N}",
Repository = repo,
Number = 1,
Title = "PR 1",
Branch = "feature/1",
};
var review = new CodeReviewRecord
{
Id = $"review_{Guid.CreateVersion7():N}",
PullRequest = pr,
Repository = repo,
CreatedAt = now,
};
var activeLock = new ActiveCodeReviewLock
{
Review = review,
Repository = repo,
ExpiresAt = now.AddMinutes(5),
};
db.AddRange(seed.Owner, seed.Organization, repo, pr, review, activeLock);
await db.SaveChangesAsync();
await Assert
.That(activeLock.Repository)
.IsSameReferenceAs(activeLock.Review.PullRequest.Repository);
Numbers resume across calls and variants
The same Numbers sequence is shared by the closed variant, and .Under(repos) fans out across both repositories.
var biome = TestBiome.Start(out _);
var closed = Blueprints.PullRequest.With(PullRequests.Closed);
await biome
.AddCodeRepositories(out var repos, 2)
.AddPullRequests(out var first, 2)
.Under(repos)
.Add(closed, out var second, count: 2)
.Under(repos[0])
.AddPullRequest(out var fifth)
.BuildAsync();
// first: 1, 2, 1, 2 (numbering restarts per repository)
// second: 3, 4, 3, 4 (the variant resumes the same sequence)
// fifth: 5, Title "PR 5", Branch "feature/5"
Explicit values are reserved first
Explicit numbers are skipped by the sequence, even when they come later in the same step:
await TestBiome
.Start(out _)
.AddCodeRepository(out var repo)
.AddPullRequests(
out var prs,
_ => { },
pr => pr.Number = 1,
pr => pr.Number = 3,
_ => { }
)
.Under(repo)
.AddPullRequest(out var next)
.BuildAsync();
// prs: 2, 1, 3, 4 next: 5
Commas, plus, and transients
await TestBiome
.Start(out _)
.AddCodeRepository(out _)
.AddPullRequests(
out var prs,
PullRequests.Closed,
PullRequests.Closed + PullRequests.Merged
) // one PR with both traits
.BuildAsync();
// prs.Length == 2; only prs[1].Merged is true
Transient inputs stay inspectable but are not saved:
var store = new RecordingStore();
var biome = TestBiome.Start(out _, store);
biome.AddCodeRepository(out _).AddPullRequests(out var prs, 2);
// marks the most recent Add step, whichever builder you call it on
await biome.Transient().BuildAsync();
// biome.All<PullRequestRecord>().Count == 2, but the store holds none
Time
A shared FakeTimeProvider is read live, and every source of now is rounded to 1 µs:
var instant = new DateTimeOffset(2026, 10, 11, 12, 0, 0, TimeSpan.Zero).AddTicks(
17
);
var time = new FakeTimeProvider(instant);
var biome = TestBiome.Start(out _, time: time);
biome.AddCodeRepository(out var first);
time.Advance(TimeSpan.FromHours(1));
await biome
.AddOne(
Blueprints.Repository,
out var second,
(repo, ctx) => repo.CreatedAt = ctx.Ago(TimeSpan.FromMinutes(5))
)
.AddPullRequest(out _)
.AddCodeReview(out _)
.AddActiveLock(out var activeLock)
.BuildAsync();
// first.CreatedAt == instant.AddTicks(-7) (rounded to 1 µs)
// second.CreatedAt == instant.AddTicks(-7).AddMinutes(55)
// activeLock.ExpiresAt == instant.AddTicks(-7).AddMinutes(65)
Ago and FromNow derive from the rounded now, so a value read back from Postgres equals the value in memory.