Skip to main content
Version: 1.0

Test results

What dispat's test suite did, as opposed to what it reached.

Measured on 2026-08-16 at commit a81004f930cb, by the release that published this site.

The suite runs 2,066 test functions and 29 fuzz targets, 1,576 subtests between them, across 24 packages, in 1m 5s. All of them pass.

Race-clean: the integration suite passes again under go test -race, as its own pass in the table below.

By module

ModuleTestsFuzzSubtestsTimeResult
pkg/ccme11135107.1spassed
pkg/manifest19000.0spassed
pkg/models300120.0spassed
pkg/scanner986840.2spassed
pkg/writer182201650.3spassed
services/dispat1,160067012.2spassed
tests/integration448012844.5spassed
tests/integration (-race)448012846.0spassed
tools/testreport18071.1spassed

Each row is one go test invocation: the tests script of one package, run through testreport test, which keeps the run's go test -json stream for exactly this. tests and fuzz count test functions, never subtests. A suite that counts every t.Run as a test reports a number several times the one it can defend, so the subtests are counted beside them instead.

The integration suite appears twice: once instrumented for coverage, once under the race detector. CI runs it that way too, rather than with -count. The totals above count it once, because the race pass is the same tests again and adding it in would claim several hundred tests this repository does not have.

The two layers

The failure semantics are the tool's main promise, so they are tested at two independent layers.

Unit tests live in each module and run against in-memory fakes. What they assert is stated in each test's own name and doc comment; there is no separate catalogue.

The black-box integration suite is a separate Go module that structurally cannot import services/dispat/internal/*. It compiles the real binary, drives it against disposable git repositories exactly as a user's shell would, and asserts on the three outputs a release run actually has:

  • git state: tags (their objects, messages and targets), commits, changelog files;
  • JSON log events: --log-format json, the machine-readable contract CI ingests;
  • execution timelines: nanosecond-resolution intervals recorded by a purpose-built probe, wherever timing rather than mere ordering is the claim.

Every composition claim lives there and nowhere else. services/dispat hosts no end-to-end tests at all.

Stability

Ordering assertions are structural, so they cannot flake. Overlap assertions use sleeps one to two orders of magnitude above process-launch jitter. Every concurrency claim is verified three independent ways before it is believed. Repeated runs (go test ./... -count 5) are the manual stability check.

What the suite covers

One test file per goal, in the order the test plan numbers them. The plan carries the claim-by-claim matrix, a row per test saying which goal it proves; this page is the summary.

FileArea
plan_test.goPlan logic across consecutive runs: cancels, holds, pins and their guards, catch-up, blocked consumers, prerelease trains, convergence.
versioning_test.goAll seven space versioning modes side by side: fixed rides, sparse alignment, the partial modes sharing only the major or the major and minor (patches and minors staying with their own package, a shared bump riding the rest, a sparse member rejoining on its next change), one shared train and its graduation, failed-ride catch-up under both a full and a partial mode, holds and pins under a shared version, pins that stay inside the shared part, and the W235/W236/W237 conflict resolutions (competing pins, divergent channels, mixed shared depths).
fatal_test.goThe repository-scoped fatal errors, each constructed for real: a dependency cycle (E200), duplicate version tags on different commits (E191), and a shallow clone (E196). All refuse to release with the code in the events and nothing executed.
ignorescope_test.goChange-scope ignore: an all-ignored commit releasing nothing and reporting W131, one ordinary file bringing the package back, the repository/space/package levels adding up with a package re-including for itself alone, the .dispatignore file and the ignore key agreeing, --since selecting from the same resolution, a scope still addressing the package, the release commit still staging ignored files, and an impossible pattern failing the load.
edgecases_test.goThe interactions between features that are each right on their own: an exact Release-As naming a prerelease (accepted at the required bump, still refused below it, and moving a whole versioning group), a release window where provider and consumer each changed with no propagation syntax written and both auto-versioning strategies still reconcile the manifests, the flow.version asymmetry that is the other side of it, a package joining a versioning group with no version and one joining ahead of it (W233), and the boundary where revertOnFail stops: at the publish.
concurrency_test.goBuild/publish concurrency budgets: peaks reached and respected, budgets independent per stage. Every claim checked three independent ways.
order_test.goThe dependency graph driving script execution order, under both isBuildWaitingPublish modes; version-task placement and environment.
interrupt_test.goGraceful shutdown: a SIGINT mid-run cancels (not fails) the in-flight and pending packages, tags nothing unfinished, and the next run releases them at the version they were owed.
hooks_test.goThe script frames and their authority: the full nine-hook per-package frame in order, the gating/warn-only split (a failing postBuild fails the package, a failing postPublish and the whole announce frame only warn), the flow.onFail/flow.onSkip outcome scripts with their specifics, login counted per space rather than per script text and its blast radius, script outputs accumulating across stages and hooks including onFail, the run-level hook frame against a real remote, and revertOnFail reaching a package skipped after its version stage already ran.
config_test.goConfig loading, resolution and the options that only change something at runtime: the --config fallback names and how far resolution climbs, .dispatexclude choosing between two config files in one folder, the ascent past a space folder's file, the rejections the space layers carry, a flag beating the file at runtime, custom shells, the inert custom object, the commitErrors policy, initials baselines, tag-format round trips, and the parser options (custom types, default propagation depth, strictTypes, parser.quiet and the --quiet-parser override).
env_test.goThe static env layers: top level, space and package merging with the most local winning, keys keeping their exact case through the binary, a value's $DISPAT_VERSION reference expanding to the package's own version, the two in-folder layers, a space's env reaching its login script, and the three refusals that make the feature trustworthy (the reserved DISPAT_ prefix, an = in a name, an empty name).
levels_test.goThe configuration ladder from the root down: one root flow reaching every space with a space and a package replacing single entries, the three-state booleans letting a space say false against a root true, a root versioning applying under each space's own group, a standalone package folding through the same defaults, and the space level carrying changelog and src.
overrides_test.goPer-package overrides and versioning groups through the binary: a packages entry replacing one flow entry, the in-folder config file winning as the most local layer, .dispatexclude exclusions, a declared versionGroups group spanning two spaces to one version and another sharing only the major (W234 rides, convergence), per-package record policies, the concurrency weight serialising a heavy build, the config ascent past an in-folder file, scripts defined at each of the three levels, one flow.build resolving to a different command per package, a space's own packages entries, the space folder's config file, the full six-layer ladder decided by the nearest layer, and dependency edges declared at the two space layers.
packages_test.goThe top-level packages section: standalone packages with a path releasing outside every space, standalone config errors, provider lists in entries and in-folder files ordering the graph, dispat compute editing each declaration in the file that holds it, and a package src narrowing file-derived change detection.
spacedeps_test.goDependency edges declared by a space: an edge in the root file's space entry and one in the space folder's own config file both reaching the graph, a cross-space edge declared by either end, the refusal of an edge touching neither end of the space it sits in, and compute --write correcting an edge inside the space's object while sending a new one to the root object.
records_test.goThe release records as artefacts: changelog accumulation above pre-dispat content, custom file/title/sections, annotated tags (object type, message, target), commit mode's release commit + tag placement + push to a bare remote, push skipping remote-existing tags, the PACKAGE_<KEY> exported-commit tag pin, commit.verify=false, the changelog.channels/github.channels restriction across a beta and its graduation, the W224 already-published skip, and a fully failed run leaving history untouched. Plus the GitHub recorder driven through a real channel transition and a build's exported attachments arriving as assets.
commands_test.goThe init and preview commands through the binary: the starter config the very next status loads, and pending notes on stdout for a selected package and for all, with prerelease windowing; plus the CLI surface itself: per-command --help scoping and the platform in --version.
run_test.goThe dispat run command: graph-ordered execution (also under concurrency), per-package resolution through the three scripts levels and the selection each one implies, both spellings narrowing to the folder they are invoked from, the --package/--space terms, the --since window and the --consumers expansion and how the three compose, both --on-error policies, the concurrency budget, cross-package output carrying, skipping and the error cases, and the arguments after -- reaching every covered package's script while a bare word stays a usage error.
standalone_test.goThe standalone step commands: changelog idempotence (W226), the in-flow nested commands landing the changelog inside the tagged commit with the outer run skipping via W226/W223, commit --tag/--push with identity flags, --tag-name naming the tag for the nested fixed-group case, the DISPAT_OUTPUT commit pin, folder narrowing, the github step publishing from an announce stage with its attachments and its selection rules, autoversion convergence with a single syncLock pass, and the window flags the steps share with dispat run including the tagged-package-drops-off-the-window pitfall and its --since all answer.
filter_test.goThe --package/--space/--group selection every package command shares: the term spellings and their globs, a space term staying in its space, a standalone package belonging to none, unmatched terms failing with the mirror hint, the invocation folder standing in for the terms nobody typed (deepest match wins, explicit terms beat it), the filter narrowing a window and never widening it, and the same selection on preview, the step commands and compute. Plus partial releases: release and status narrowing the plan, a consumer withheld when its provider is left out (W230), a versioning group released split with a warning (W231) and healed by the next run, --strict refusing either before anything is built, folder inference on a release, and the records following what actually went out. Plus the group term: a group spanning a space and a standalone package, a space that versions as its own group, the glob spellings, the errors that name the flag which would have reached the term, the group narrowing every other command, and a release of a whole group that cannot split one.
if_test.godispat if through the binary: one chain answering differently in four environments, all six condition spellings, the chosen script's exit code becoming the command's and --on-failure replacing it, nothing matching with no --else running nothing at all, the script running in the invocation folder, another dispat if nesting inside a branch, and the reserved command word with its usage exits taken before any config is read.
exec_test.godispat exec through the binary: one subject deciding both the level read and the environment given, the exact mode refusing a name from another level and --fallback walking the layers with the nearer one still winning, the declared env layered file under space under package, --script-from crossing the text and the context, --env both reaching the release variables outside a release while the default computes no plan at all, the exit code and --on-failure, the in-flow case inheriting a run's DISPAT_* variables with no flag, the arguments after -- reaching the script but never --on-failure, and every malformed invocation exiting 2.
selfupdate_test.godispat self-update through the binary, against a fake releases API and two versions built for real: the download checked against the published size and checksum, run once, and moved into place, with the replaced binary kept beside it; --check as a gate that changes nothing; already-current and --force; --release reaching a named version and refusing an unpublished one; --rollback rotating so it is reversible; a checksum mismatch and a missing platform binary both leaving the working binary untouched; prereleases opted into and left again; the backup expiring after its week; and the update notice appearing on an ordinary command, staying out of JSON, and not being asked for when the config says no.
compute_test.goThe compute command through the binary: the detect/apply/check loop with backup and convergence, keep/removal semantics, edges edited in the space entry and the space file that hold them, and the W220 ambiguity as a JSON event. Also the baselines it derives from the manifests' own versions: seeded initials applied in the same single write as the edges, existing entries never rewritten, packages with a readable release tag skipped and ones with an unparseable tag or no commits at all covered, W225 for manifests that disagree, and the YAML and TOML config paths.
autoversion_test.goNative auto-versioning through the binary, under either strategy or neither: range/version rewriting under the match policy with the W192/W197/W203/W221 diagnostics across three runs and commit.include staging the regenerated root lock file; literal replace rules over a Gradle script and a README with W222 for a rule that matched nothing; manifestNames making an unreadable package visible to compute and to auto-versioning alike; and the serialised syncLock slot keeping its budget when it is the whole of the version stage.
manifests_test.goThe scanner, writer and replacer commands through the binary: all three running with no config file, commit or plan; the listings and the JSON events; the partial-parse, missing-edit and matched-nothing outcomes reaching the exit code plainly and under --strict; a format-preserving rewrite proved byte-for-byte on disk across two ecosystems; the replacer replacing every occurrence and chaining its replacements in order; the redirect round trip read back by the scanner; and the reserved command words leaving dispat run scanner alone.
autowriter_test.goThe autowriter command through the binary: one edit set applied to every package the plan selects, the manifests found by scanning each covered package, byte-for-byte writes that converge on a second pass, {version} resolved against the freshly computed plan (and refused when it names no package), the root/all manifest scopes, --only-updated following the plan into a clean no-op, the cross-sweep --strict, the redirect round trip, syncLock running exactly where a manifest changed, and every refusal reaching the exit code.
autoreplacer_test.goThe autoreplacer command through the binary: a {provider} pattern rendered once per workspace package a covered package declares, a package-scoped pattern writing the covered package's own version, globs selecting inside the package folder the sweep handed over, --only-updated and --consumers narrowing and widening the fan-out, a nested package left to its owner so no file is written twice, convergence told apart from never-matched under --strict, and the reserved command word.
docker_test.goDocker through the binary: compute deriving an image chain from a FROM line and writing it to the config, a release reconciling the consumer's FROM and COPY --from tags and a compose file's image and build.tags to the versions just computed (the package's own for what it builds, the provider's for what it pulls), stage aliases and port mappings left alone, and the config-free scanner/writer commands over both formats with a digest-pinned base skipped rather than failed.
guard_test.goThe release guards: run.allowBranch refusing a foreign branch and a detached HEAD (nothing tagged, status untouched, globs reaching slashed names), and the push-mode behind-remote check refusing a stale checkout up front, then releasing cleanly once pulled. Both are proven off by default, and commit.verify: false is shown skipping the behind check and carrying the run to its rejected push.
lock_test.goThe release lock, read from the bare remote the fixtures push to: the tag present while a run works and gone from both copies afterwards, a run refused by a lock somebody else holds without disturbing it (commit.force included), two real releases against one remote where only the first proceeds, the lock taken with the release push turned off entirely, the remoteless refusal and the DISPAT_UNSAFE_DISABLE_LOCK values that lift it, the tag given back after a failed package, a guard refusal and a SIGINT alike, a stale local tag ignored, a stranded cleanup reported without touching the exit code, every other command proven to take no lock, and a {version} tag format still reading past the lock tag when it plans.
corrections_test.goCorrecting a release record from a later commit: a misclassified commit restated before it ships so the package releases a patch and the changelog names what it corrects, a correction of released work reported as a no-op nothing can hide (W209 through --quiet-parser), two corrections of one target resolving newest-first (W210), a correction of a correction voiding it (W215) so the original returns, a record scoped (*) narrowed for one package and left standing for the other, a widening correction refused (E213), Deletes: * clearing one scope only, the three target errors (E210/E211/E212), a deleted record taking its propagation and its versioning-group ride with it, and the Reverts half: both entries leaving the changelog while the bump keeps the major (W212), an unreachable target staying informational (W213), and a discarded revert giving the entry it hid back.
multiref_test.goA $ref naming several files: fragments merged in the order they are named with the later file winning a shared key, record lines from two files arriving as one list, environment variable case surviving the merge, the keys beside the reference outranking every file it named, every fragment on the traced record, the refusals stopping the run before any work (no files, a name that is not a file, files holding different kinds, a missing file, a cycle closed by the second file), and compute --write refusing a merged key while still writing through a list that names one file.
channels_test.goThe channels a record reaches: changelog.channels/github.channels naming one prerelease channel, every prerelease, or the stable line alone, driven across a beta, an rc and a graduation with the releases themselves still tagged and published; a line's own channels splitting one configured footer between the betas and the stable entry with the sections unfiltered and the GitHub body agreeing; channels combining with the package filters; two policies differing only in a line's channels keeping their own releasers; the config refusals; the skip reported as an info event naming the channel; and dispat preview --changelog --github showing each body under its own entry format.

Regression and bug fences

Two planner properties carry dedicated guard tests: a rejected Release-As pin must not swallow a sibling bump, and a propagated graduation transition must graduate the dependants. See the regression fences in the test plan.

Every defect found by review carries a named guard test, listed with the failure it fences in the bug fences table. Some of those tests live in the unit suites rather than in the integration one, because that is where the defect is cheapest to pin; the table says which is which.