Skip to content

Development

Tobias Vorwachs edited this page Oct 24, 2026 · 2 revisions

Development

Repository layout

Path What it is
crates/protocol Mumble protocol: TLS control channel, protobuf messages, UDP voice crypto
crates/audio Capture/playback (cpal), Opus, processing chain, jitter buffer, mixer, bitrate controller
crates/store SQLite database, identity certificate, OS secure storage
crates/engine App state, server session, voice, chat, follow, diagnostics. The only thing the app shell talks to
crates/bot mumcord-bot: scripted Mumble bot for tests and manual checks
crates/positional Placeholder for stage 1b (Mumble Link, plugin host)
src-tauri/ Tauri app shell: commands, events, tray, deep links, push-to-talk, window handling
ui/ React 19 + TypeScript frontend (ui/src/features/*, locales in ui/src/locales/{en,es,de})
e2e/ WebdriverIO end-to-end tests, run in Docker
testenv/ Local Mumble servers (Docker) for tests
tools/netsim Network-fault proxy for tests
specs/ Specifications, brainstorming files, task lists, implementation reports
docs/ engine-api.md: the contract between UI and engine
scripts/ generate-notices.sh, smoke/ packaging smoke test

Spec-driven workflow

Specifications are the source of truth. Each epic has specs/{nr}-{name}-specification.md; the Epic 1 spec is specs/01-foundation-specification.md. Only build what the spec says; if it is unclear or contradictory, ask instead of guessing. No scope creep. Every acceptance criterion (AC-…) must be verifiable in the running client against a local test server.

The project ships two Claude Code skills for this: /spec (brainstorming to specification) and /dev (build a specification). See Spec and Dev Workflow.

Ground rules

  • Client only. Never build, require or modify anything on the server side (no companion services, plugins or server config changes). Every feature must work against an unmodified standard Mumble server. No private protocol extensions.
  • Real-time audio. Never block the audio thread: no allocation, locks, I/O or logging in the real-time path.
  • Separation. Protocol handling, audio pipeline and UI are separate modules. The UI never talks to the network directly; it talks to the engine through the Tauri commands.
  • Security and privacy. TLS always on; verify server certificates and warn on changes; protect the client certificate. No personal data, chat contents or audio in logs. Treat everything from the server (names, comments, chat HTML) as untrusted and sanitize before rendering.
  • Secrets never go into the repo. Use git-ignored local env files and CI variables.
  • UI. Small components; separate state/data access from presentation. Keyboard operable, screen-reader labels, respects system theme and scaling.
  • Code. Simple over clever, no dead code, comments explain why not what, never swallow errors silently, strict typing. Rust lints: unwrap_used, todo and dbg_macro warn, and clippy runs with -D warnings.
  • Tests. New logic gets tests. A bug is first reproduced with a test, then fixed. Before "done": build, tests, type checks and linters pass.
  • Never connect to public or third-party Mumble servers for testing.

Checks

cargo fmt --all --check
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo test --workspace --locked
npm run lint          # eslint + prettier --check
npm run typecheck     # tsc -b
npm test              # vitest run

npm run format applies prettier.

Commits

  • Conventional Commits (feat:, fix:, docs:, chore: and so on) with a clear English message.
  • Small and focused: one logical change per commit. Stage specific files, never git add -A.
  • Branch off the default branch. main is protected: changes go in through a pull request, and the required checks (CI and E2E) must pass before merging. Direct pushes to main are not possible.
  • Tags starting with v are protected too, so release tags cannot be moved or deleted casually.

Releasing

Releases are built from tags. Bump the version (package.json, package-lock.json, Cargo.toml, Cargo.lock), merge that through a pull request into main, then push a tag vX.Y.Z on main. The Release workflow checks the tag against the versions, builds the Windows installer, the universal macOS DMG and the Linux AppImage, smoke-tests each, and creates a draft GitHub Release with the files and SHA256SUMS. Review and publish the draft by hand. The full steps, including what to do when the workflow fails, are in RELEASING.md in the repository.

Local test servers (testenv)

Requires Docker with compose, openssl and sqlite3. Set up once:

cp testenv/.env.example testenv/.env   # then edit the passwords; .env is git-ignored

Start and stop:

testenv/up.sh <v15|v14|v13|limit> [--cert A|B]
testenv/down.sh [service]
testenv/reset.sh [v15|v14|v13|limit] [--cert A|B]
Service Server Port (TCP+UDP)
v15 Mumble 1.5.735 64738
v14 Mumble 1.4.230 64739
v13 Mumble 1.3.x (Debian bullseye package, built locally on first start) 64740
limit 1.5.735 with Listen refused (listenersperchannel=0, listenersperuser=0) 64741

The superuser is SuperUser with the SUPERUSER_PASSWORD from .env. Channels Lobby, Raid and AFK are created on first start. Autoban is disabled so tests can reconnect freely. up.sh --cert B swaps the server certificate to test the "certificate changed" warning. reset.sh wipes the database (only SuperUser stays registered) and interrupts connected clients.

mumcord-bot

A scripted Mumble client for tests. Each run connects once, runs one subcommand (or with script one per stdin line), and exits non-zero on the first error.

cargo run -p mumcord-bot -- --help

Global options include --host, --port (default 64738), --name, --cert (persistent certificate name), --superuser, --timeout and --hold. Subcommands include connect, join, say, pm, rename, avatar, comment, register, listen, unlisten, mute, deaf, play, record, expect (user, user-gone, message, denied and audio checks), sleep and script. Run --help for the full, current list.

netsim

A local TCP+UDP forwarding proxy that simulates UDP packet loss, latency, jitter, a UDP blackout and TCP connection kills, adjustable at runtime. It needs no root and behaves the same in CI.

cargo run -p netsim -- run --listen 127.0.0.1:64739 --target 127.0.0.1:64738 --control 127.0.0.1:64740

Those are the defaults. Note that they overlap with the v14 and v13 test server ports, so pass other values when those servers run (the E2E setup uses 64800 for the proxy and 64801 for control). Control it with:

netsim ctl loss [up|down|both] <pct>
netsim ctl latency <ms>
netsim ctl jitter <ms>
netsim ctl block-udp on|off
netsim ctl kill-tcp
netsim ctl reset
netsim ctl status

Use --control <addr> on ctl if the proxy listens on another control address.

Clone this wiki locally