Skip to content

Contributing and Testing

josh goble edited this page Aug 16, 2026 · 1 revision

Contributing and testing

The most useful contribution is not code

Keelarr has been proven on two hosts: a QNAP with a real library, and a Mac with nothing on it. That is not "runs anywhere", and every feature added before that gap closes is one nobody has confirmed works elsewhere.

A report that it worked closes an issue as well as a bug report does. Right now nobody can say either way about Linux.

Nine more scenarios are listed in docs/testing.md, each written to be closeable without a conversation first.

Running the code

npm install
npm test

No framework, no build step, no transpiler. Plain ES modules, node:test, and dependency injection through *Impl constructor parameters. 432 tests, and the whole suite runs without Docker.

npm run dev    # watch mode
npm run demo   # simulated stack, no Docker socket, no auth

How the code is laid out

src/server.js            listens, then initialises — in that order, deliberately
  create-http-app.js     routes, and the auth gate over /api/*
    keelarr-app-service  composes seven app services, one concern each
      host-profile       detection, validation, settings, deploy/.env
      dashboard          the state the UI renders
      import             read-only scan of foreign containers
      managed-stack      install, deploy, upgrade, update checks
      cutover            adoption cutover and revert
      removal            removal with retention options
      wiring             what should be connected, and connecting it

Underneath: runtime.js (Docker and Compose), generator.js (Compose files), status.js (the dashboard row), jobs.js, store.js, health.js, logger.js.

wiring/ splits further — topology.js classifies networks, attach.js resolves addresses, reconcile.js compares what is against what should be, payloads.js builds writes from each app's schema, prerequisites.js reports what only the operator can supply.

Two rules the codebase holds to

1. Never claim something you have not verified

If it cannot be determined, say so. Reachability is tri-state. A prerequisite check returning null means "no opinion", never "none". A job's status is derived from its steps rather than from the handler returning normally.

Nearly every guard in this codebase exists because a cheerful default hid a real failure, and the unit suite stayed green through all of them.

2. Tests stub every injected implementation that touches Docker or the filesystem

Stubbing one layer is not enough if a layer underneath still reaches the real machine.

One test stubbed the settings layer but left the controller-env layer real. Running npm test inspected the developer's actual Docker, found their running controller, and overwrote that controller's deploy/.env with QNAP paths from a test fixture — silently, because the write happened in the test process and never reached the app's log.

Comments explain why, not what

If the reason a line exists is a defect found the hard way, the comment says so. That is what stops it being "simplified" back:

// A total deadline and a silence deadline answer different questions, so a
// command uses one or the other rather than both: bounded work gets
// `timeoutMs`, open-ended transfers get `idleTimeoutMs`.

Tests should be falsifiable

A test that cannot fail proves nothing. When adding one, check it fails against the bug it describes — break the code deliberately and watch it go red.

Two examples worth copying:

  • modules-load.test.js imports every module under src/. It was added after a rename left an orphaned file and 380 tests passed while the app could not boot. Verified by moving a module away and watching it fail.
  • The job registry's settled() tests fail if settled resolves early. Checked by making it resolve immediately: three tests went red.

Avoid time-based waits

Do not poll for a number of event-loop ticks. It is not a measure of how long work takes — it measures how fast the loop spins, which gets worse the busier the machine is.

A helper that gave up after 500 setImmediate ticks passed locally every time and failed on CI 19 milliseconds into a healthy job. Use jobs.settled(jobId), which waits on the work itself.

Submitting a change

  1. npm test passes
  2. New behaviour has a test, and you have watched that test fail
  3. Comments explain the reasoning behind anything non-obvious
  4. If it changes what is proven, update docs/testing.md — and keep automated coverage and live evidence in their separate columns

Licence

PolyForm Shield 1.0.0. Use, modify and redistribute freely, including at work. The one restriction is selling a product that competes with Keelarr. Source-available, not OSI-approved open source — worth knowing before you invest time.

Clone this wiki locally