Skip to content

Spec flow testing - improve iteration speed by optimizing local tests #17

Description

@rustyrazorblade

Slow tests can kill the pace of development. Add a skill to identify the fast tests in a repo using the native tools for a language, and tag the tests that can run quickly.

For example, Gradle has a test report. Use this to tag the fast tests. Fast varies by repo. Look to maximize benefits (top 70-90% of tests to start).

Rust has nextest.

When using the TDD loop, run the fast tests by default. Use CI as feedback to add additional tests that fail to the local tests, in order to keep development velocity high.

Clarification: core tag vs additional file

Two distinct mechanisms, not one:

  • core — static, structural tag. Fast tests only (no container,
    no I/O). Committed to the repo as an annotation on the test itself. Same
    for every branch and developer. Always runs during local TDD cycles.

  • additionalnot a tag. A file, maintained per patch, scoped to
    the current branch/worktree. Local working state, not committed — a
    different branch has a different additional file, or none at all.

Local inner loop = core-tagged tests + whatever's listed in the
additional file for this worktree.

How the additional file gets populated

Computed per patch, from the current diff. Given the files/modules touched
by the in-progress patch, look up which expensive (Docker/Testcontainers)
tests are relevant, based on a mapping of diff → related tests (e.g.
module boundaries, file-pattern rules), and write that set into the file.

What CI failures actually do

A CI failure on a test that wasn't in additional for that patch is
signal that the diff → test mapping under-selected for that kind of
change — not a reason to add that test to a permanent list. The mapping
itself gets corrected/refined so that a future patch touching similar
files gets the right test written into its own per-patch additional
file.

This is the manual, rule-based equivalent of what ML-driven predictive
test selection tools do automatically from historical build data — the
mapping is the thing that improves over time, not the file's contents.

Persistence implications

  • core — lives as annotations in test source, committed, shared.
  • additional — a gitignored file at the worktree root, uncommitted,
    local. Git worktrees already give independent working directories, so
    this naturally scopes correctly per branch without needing to key it by
    branch name explicitly.
  • The diff → test mapping — the part that actually accumulates
    knowledge over time — is the piece that needs to be committed and
    shared, since it's what future patches (and future sessions/developers)
    benefit from, unlike the per-patch additional file's contents.

additional is ephemeral by design. It's scoped to the worktree and the
branch's lifetime — gitignored, never committed, and not expected to
survive past merge. Nothing carries forward to the next branch; each new
patch starts with additional empty and rebuilds it from CI feedback
during that branch's own lifetime. This is intentional, not an oversight
— the branch boundary is what keeps the working set from growing
unbounded.

additional is ephemeral by design. It's scoped to the worktree and the
branch's lifetime — gitignored, never committed, and not expected to
survive past merge. Nothing carries forward to the next branch; each new
patch starts with additional empty and rebuilds it from CI feedback
during that branch's own lifetime. This is intentional, not an oversight
— the branch boundary is what keeps the working set from growing
unbounded.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions