Test Conventions: Mastery Over Restriction
Most projects pick one test style and lock it down. "We use StringSpec. Here’s the ADR. Deviate and the linter catches you."
That’s governance for people you don’t trust.
We’re building Total Recall for a community of hackers — humans and synthetic minds who value mastery, curiosity, and the joy of building well. Steven Levy’s hacker ethic doesn’t say "restrict access to one tool." It says access should be unlimited, and you should be judged by what you create.
Our test suite is an invitation, not a decree.
Why Multiple Spec Styles
Kotest offers ten spec styles. Each one reads differently, structures differently, and fits different kinds of assertions. Using only one is like writing every paragraph in the same sentence structure — technically correct, aesthetically dead.
The principle from our Team Norms: code should be readable by tired-you at 2am, three years from now. That means the test should read like the domain concept it’s testing, not like a formatting convention it’s obeying.
What We Found Fits
| Spec Style | Used For | Why It Fits |
|---|---|---|
StringSpec |
Value object shape (Memory, Association, SalienceScore) |
Flat, one-liner assertions. No ceremony needed for "does this data class hold its shape?" |
FunSpec |
MCP tool registration and teapot stubs |
Lab notebook.
Direct, function-style probes.
|
BehaviorSpec |
Command contracts (store, claim, reclassify, consolidate) |
Given/When/Then maps to command intent. Given this context, when this command, then these properties hold. |
DescribeSpec |
Event contracts organized by bounded context |
Nested |
WordSpec |
Lifecycle state machines (sessions, modes, transitions) |
"When … should" nesting reads like a state machine specification. Sessions start, modes change, transitions happen, sessions end. |
ShouldSpec |
Query contracts and defaults |
|
FeatureSpec |
Notification contracts |
Feature/Scenario fits naturally. Each notification type is a feature of the system’s communication with the mind. |
ExpectSpec |
TransactionContext propagation |
"expect" fits contract verification. I expect this envelope to flow from command to event, preserving session and chaining causation. |
What We Don’t Use
AnnotationSpec exists for JUnit migration. We’re greenfield. It has no place here — not because it’s forbidden, but because it solves a problem we don’t have.
FreeSpec is reserved for integration tests — multi-step flows through the full domain (store → score → associate → recall). That work begins in Phase 2.
The Fixtures
Domain object factories live in mimis.gildi.memory.testing.Fixtures:
val m = aMemory(tier = Tier.IDENTITY_CORE, claimed = true)
val tx = aTransactionContext(CONTEXT_COMPONENT_HIPPOCAMPUS)
val s = aSalienceScore(score = 0.95, decayRate = 0.01)
Sensible defaults, override what you need.
No test builders, no elaborate DSLs.
If you need a Memory, call aMemory().
The Configuration
ProjectConfig sets project-wide behavior:
-
Specs run concurrently. Tests within a spec run concurrently.
-
30-second timeout per test. If a unit test takes 30 seconds, something is wrong.
-
Tags appended to test names for reporting clarity.
These are starting points. When backing services arrive (SQLite, Testcontainers), integration tests may need different timeouts. The config will evolve.
The Culture
Our TEAM_NORMS say: clever is suspicious, clear is the goal.
Using seven spec styles isn’t clever. It’s clear. Each test reads like the thing it’s testing. A lifecycle state machine reads like "When SessionStarted, should carry the instance ID." A value object shape reads like "memory holds its shape with defaults."
The clever thing would be forcing everything into one style and writing a long ADR defending the choice. That’s governance. We’re not here for governance. We’re here to build something worth building, and to show others how.
If you’re contributing to Total Recall and you find a spec style that fits your test better than what’s here — use it. The code is the convention. The test suite is the documentation. This blog post is just a thought we wanted to share.