Skip to content

Run the tests

This guide shows how to run the Pester suites, what each suite covers, how the fixture repo works, and the mock policy every new test must follow. Everything runs under Pester v5 from the module root.

Run the suites

Invoke-Pester ./tests/ -Output Detailed

The module's GitLab pipeline runs the same suites on every push - see Set up pipelines.

Know the four suites

Suite File What it proves
Smoke tests/Nerthus.Smoke.Tests.ps1 Every subsystem end to end at the service level - the C# substrate, the Markdown parser, the per-subsystem services against fixture bytes
Sync tests/Sync.Tests.ps1 The in-daemon git sync: scheduler tick (converge and publish), mr mode, the substrate probe and status service, the write gate's SyncStale/SchemaTooNew refusals - real disposable git fixtures, self-skips when git is absent
Contract tests/contract/Api.Contract.Tests.ps1 The wiring: each Describe spawns a real nerthusd on a fresh fixture clone, adopts it over POST /import, drives routes with raw HTTP, tears down in AfterAll (POST /admin/shutdown, kill fallback)
Adoption rehearsal tests/integration/Adoption.Rehearsal.Tests.ps1 The pre-deployment gate against a real, disposable clone of repozytorium-fabularne - env-gated, see below

Contract assertions are status codes, envelopes, capability 403s, dry-run no-write, the token lifecycle, and the middleware order - 401 before 404 before 403 before the write gate, as specified in the API reference. Contract tests stay targeted at wiring; exhaustive logic coverage belongs in the service layer.

The smoke suite is the authoritative coverage inventory. Its Describe names read as contract statements: Temporal scalar writes (history is never destroyed), Session edit preserves custom narrative content, Token store (hashed ids, no raw values on disk). To see what covers a subsystem, grep the test file for that subsystem's vocabulary.

The adoption rehearsal (env-gated)

Set NERTHUS_REHEARSAL_REPO to the path of a disposable clone and run the file - it spawns a daemon, previews then applies the import, runs the integrity quartet (entities, sessions, currency reconciliation, override lint), and proves the re-import is byte-identical. Without the variable the whole file skips, so CI runs it as a no-op.

Warning

Never point NERTHUS_REHEARSAL_REPO at a working copy you care about - the rehearsal adopts for real.

Use the fixture repo

The smoke suite's BeforeAll dot-sources the full daemon service set (except daemon/services/Sync.ps1, which its own suite loads), compiles the C# substrate (Add-Type is a hard requirement - there is no PowerShell fallback), and builds a miniature, real-shaped Polish fixture repo in a temp directory via the shared builder tests/FixtureRepo.ps1 (New-NerthusFixtureRepo - the same fixture bytes feed the contract suite):

  • a seed Gracze.md (consumed by the import's bootstrap path) - player, the PU triple (STARTOWE/SUMA/NADMIAR), two narrator-note bullets, Margonem id, webhook;
  • a Postaci/Gracze/ character sheet and an NPC under Postaci/NPC/Werbin/;
  • a canonical-header session in Świat gry/Werbin/Sesje lokalne.md, crediting Roman 0,15 PU;
  • a legacy .robot/res/pu-sessions.md ledger.

Every repo a test creates is deleted in its paired teardown.

Follow the mock policy

Every test redirects one seam - the repo root (services take -RepoRoot / a $Daemon hashtable pointed at the fixture) - and leaves the data path real.

Concern In tests Why
Repo root the temp fixture repo The single isolation seam
Clock / "now" fixture dates chosen so "now" never matters, or explicit -ActiveOn/-At Monthly PU and temporal projection must be deterministic
Discord delivery never enabled No real webhook calls in tests
Game-log fetch file:// fixture URLs No live host calls
Discord JWKS a locally generated RSA key passed straight to the verifier (-Jwks) Verify the real RS256 signature path without the network
Markdown parser REAL (Nerthus.MarkdownScanner on fixture bytes) It is the behavior under test
Nerthus.* C# types REAL (all of them, via Add-Type) No shadow exists
.NET statics REAL ([System.IO.File]::…) Fixtures are the input; mocking them tests nothing
Name index REAL (built from fixture entities) Resolution stages must run against true tokens

A resolution test that mocked the declension engine would prove nothing about Polish declension; it must feed Solmyrze to the real engine and assert it lands on Solmyra - see Name resolution.

Copy the proven patterns

Write tests assert on the bytes written back, never on return values alone, and always cover the read-only refusal:

It 'refuses to author in read-only mode (write gate)' {
    $script:SD.Mode = 'ReadOnly'
    { Add-NerthusSessionService -Daemon $script:SD -File 'Wątki/RO.md' -Date '2025-06-02' -Title 'X' -Narrator 'Y' -Apply } |
        Should -Throw '*SchemaTooOld*'
}

Importer tests assert the idempotency contract directly: read the emitted nerthus.entities.md with [System.IO.File]::ReadAllBytes(), re-run the import, and assert the bytes are identical - see Adoption for the contract.

PU math uses [decimal] to match real values (0,15 parses to [decimal]0.15; grant decimals are kept as-is).

Add tests for a new route + cmdlet

  1. Add service tests to the smoke suite (or a sibling *.Tests.ps1) against a fixture repo - happy path, edge cases, read-only refusal, Polish-diacritic data. Name the It after the contract ('aborts the whole batch on one unresolved name').
  2. Add one contract-layer route test (one success, one 403).
  3. Keep the API reference in sync - a new route or cmdlet missing from the canonical cmdlet ⇄ route mapping is a doc bug.

The full feature recipe is in Add a feature.

Avoid the forbidden patterns

  • Should -BeIn @(200, 403) on a write route's status - it green-lights both the intended and the accidental outcome. Assert the exact code.
  • Mocking the parser, a Nerthus.* type, or a .NET static. Feed them fixture bytes instead.
  • Leaving a daemon or temp repo behind. Every setup pairs with its teardown.
  • Asserting on call counts / internal variable names. Test behavior (input → bytes / status / owner), not implementation.
  • Real network egress - no live Discord webhook, game-log host, or Margonem key in any test.

See also

  • Architecture - the daemon-as-data-owner contract and the C# substrate
  • Adoption - importer idempotency, preview == apply
  • PU model - fail-early, cap/overflow math, the dated timeline
  • Sessions model - the universal header key and entity-driven distribution
  • API reference - the route table, envelopes, and middleware chain that contract tests must track