Skip to content

Testing and the Gate

Se.Cherkasov edited this page Aug 23, 2026 · 1 revision

Testing and the gate

English · Русский

The test stack

kotlin("test") on the JUnit 5 backend. No Kotest, no MockK, no ktlint — fakes are hand-written and the lint gate is Android lint inside ./gradlew build. Coverage is Kover (./gradlew koverHtmlReport).

./gradlew test allTests   # `test` alone skips the multiplatform modules

Tests live in commonTest unless the behaviour is genuinely platform-specific; UI tests use the shell harness described in UI layer and need a display (CI runs xvfb-run --auto-servernum).

TDD loop

  1. RED — write the failing test first and confirm it fails for the intended reason, not on a compile error.
  2. GREEN — the minimal implementation: contracts in commonMain, platform libraries behind expect/actual, dead code deleted in the same commit.
  3. Gate — the stages the change actually owes.
  4. Review — the agent fan-out.

A new test is re-run with the fix reverted, to prove it actually catches the regression.

The harness

tools/harness/ enforces the loop instead of trusting it:

tools/harness/gate.py status        # what this change is, and what it still owes
tools/harness/gate.py run           # run exactly the stages owed, in order
tools/harness/gate.py red --tests '*Pattern*' --file path/to/Test.kt
tools/harness/gate.py task bug 133  # override the auto-detected kind
tools/harness/gate.py reviewers     # which reviewers this change needs

What a change owes depends on what it is:

Kind Detected by Owes
docs no code in the diff nothing
refactor refactor/ chore/ perf/ branch checks · tests · build · detekt · reviewers
feature feat/ branch, or an unnamed branch with code the same, plus a test touched
bug fix/ bug/ hotfix/ branch, or declared the same, plus a test recorded failing before the fix

Areas add stages: UI or Android in the diff pulls in :androidApp:compileDebugKotlin, server work pulls in a Java reviewer, terminal work a performance reviewer. A declaration can make the gate stricter, never looser.

Why only the runner marks a stage green

The runner executes the stage, reads the exit code, and pins the result to a digest of the tree it ran against. A Gradle run made by hand is not recorded — not because hand runs are forbidden, but because nothing outside the runner can prove which code an exit code belonged to. Editing anything afterwards reopens the gate, because the digest moved; committing does not, and reverting a change restores the green state it had.

Stages: checks (deterministic project rules), tests, build (lint on), detekt, and android when the diff touches UI. Logs land in .git/skerry-gate/<stage>.log.

detekt fails on new findings only; existing ones sit in gradle/detekt-baseline-*.xml, and re-baselining to silence your own finding is not allowed.

The deliberate bypass is SKERRY_GATE_OVERRIDE=1, which means saying out loud why. It does not unprotect main.

The harness has its own tests: python3 tools/harness/selftest.py (~70 cases, two seconds, no Gradle). Changing a rule means changing them too.

Deterministic checks

tools/harness/checks.py blocks a commit on the rules that need no judgement: translations complete in en + ru + zh, no hardcoded UI literal, Txt/Sym instead of raw Text/Icon, design tokens instead of hex, no Kotest or MockK, dependencies through the version catalog, atomicWriteUtf8 on secret paths, a key binding shipping with its Settings row.

Review fan-out

Once the branch is green, reviewers run in parallel against git diff main...HEAD plus the working tree:

Reviewer Looks for
skerry-reviewer this project's own rules — parity, primitives, vault, the abstraction catalogue
skerry-kotlin-reviewer structured concurrency, Compose recomposition, Kotlin idioms for this stack
skerry-security-reviewer the vault and its crypto, untrusted protocol input, the sync/team boundary
silent-failure-hunter swallowed exceptions, bad fallbacks, errors that never propagate
pr-test-analyzer whether the tests cover the behaviour, not just the lines

The first three live in .claude/agents/, so the fan-out ships with the clone. Rules: reviewers are read-only, never switch branches, every finding is either fixed or explicitly rejected with a reason, a behaviour-changing fix goes back through step 1, and two rounds per branch — the third needs the maintainer to ask for it. Findings are kept in .git/skerry-gate/reviews/.

CI

Workflow Trigger What it does
ci.yml pushes and PRs JDK 21, Android SDK, test allTests under xvfb-run, detekt, uploads test reports
release.yml v* tag (or manual) builds .deb/.rpm/.AppImage (x64 + arm64), .msi, .dmg (arm64 + x64), a signed .apk and SHA256SUMS.txt, publishes a draft release
server-image.yml server-v* tag builds the multi-arch server image, smoke-tests that the container becomes healthy, pushes to Docker Hub
codeql.yml schedule and PRs code scanning

CI is not a substitute for the local gate: it runs the same tests, but the gate is what ties a green result to the exact tree being committed.

Clone this wiki locally