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¶
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 underPostaci/NPC/Werbin/; - a canonical-header session in
Świat gry/Werbin/Sesje lokalne.md, crediting Roman0,15PU; - a legacy
.robot/res/pu-sessions.mdledger.
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¶
- 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 theItafter the contract ('aborts the whole batch on one unresolved name'). - Add one contract-layer route test (one success, one
403). - 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