3 minute read

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. test("this tool does this thing") — no narrative needed.

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 describe blocks mirror the domain. Hippocampus events, Salience events, Synapse events — each in its section.

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

should reads naturally for expectations about read-only operations. "SearchQuery should default to 10 max results."

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.

Updated: