Development¶
Guide for building, testing, and contributing to Signal Fish Server.
The production correctness audit records reviewed contracts, reproducible findings, and the remaining audit work.
Prerequisites¶
- Rust 1.91.0 or later (see
rust-versioninCargo.toml) - No system libraries required for the default build
CI Tooling Parity¶
The devcontainer is expected to include the same core CLI tooling used by CI, pre-commit support scripts, and local test workflows, including:
yq(YAML code-block validation)taplo(TOML code-block validation)rg/ ripgrep (search tooling)fd(modern file discovery)- Docker CLI support in VS Code devcontainers
- Latest Codex, OpenCode, and Nanocoder terminal clients
- GitHub and Z.AI MCP servers for every supported coding-agent frontend
To keep rebuilds reliable on Windows + WSL + Docker Desktop and avoid known DNS failures in the feature install path:
.devcontainer/devcontainer.jsonkeepsdocker-outside-of-dockerconfigured with"moby": false..devcontainer/Dockerfileinstalls heavy cargo tools viacargo-binstall..devcontainer/post-create.shskipscargo check --all-featureswarm-up by default for faster startup. SetSIGNAL_FISH_WARM_CARGO_CHECK=1to enable it.- Agent CLI refreshes use a version-check fast path and run behind the editor
attach point. Set
SIGNAL_FISH_SKIP_AGENT_REFRESH=1only when working in a constrained/offline environment and an installed version is sufficient.
Global npm packages use /home/vscode/.npm-global, owned by vscode. Run
npm install --global <package> directly; do not use sudo.
The root and browser-client node_modules named volumes are also initialized
for the vscode user, so ordinary local npm install commands need no sudo.
Before opening a fresh clone, create .env.local in the repository root.
An empty file is sufficient if you do not use authenticated MCP servers.
For MCP access, add GITHUB_PERSONAL_ACCESS_TOKEN and Z_AI_API_KEY using
Docker env-file syntax: one unquoted KEY=value per line, without export.
The file is gitignored and excluded from the Docker build context. CI creates
an empty file before starting its test container.
The devcontainer imports this file for GitHub and other environment consumers.
Z.AI additionally reads the repository .env.local at each MCP startup, with
file values taking precedence over inherited Z_AI_API_KEY. Missing files or
missing keys fall back to the environment; an explicit empty key fails closed.
The launcher accepts dotenv quotes and comments without executing shell code.
For Docker compatibility, keep entries unquoted when using the devcontainer.
Restart Z.AI MCP servers after editing the key; no container rebuild is needed.
All supported frontends use .devcontainer/zai-mcp.mjs over stdio. Vision runs
the preinstalled binary; the remote servers use the MCP SDK bundled with the
installed Vision package for HTTP sessions and SSE. There is no startup download.
Claude Code, Nanocoder and OpenCode resolve the launcher from the project root;
VS Code uses its workspace path and Codex gets an absolute path during setup.
Codex setup migrates only repository-marked entries and preserves custom tables.
For an existing container, apply the migration once with:
Restart the affected MCP servers or agent after migration. Never paste keys into bug reports or committed config.
This defaults to .env.local; --env-file PATH selects another file.
Without --live, it makes no network requests. The live check uses the same
launcher as frontends and checks initialize, initialized notification, and
list-tools without invoking search or vision tools.
The startup RCA identified a credential-loading gap: Docker's env-file import was a creation-time snapshot:
editing .env.local could leave GUI agents and terminals with stale or empty
credentials. Native remote configurations relied on those inherited values, and
the old diagnostic stopped before the notification failure reported by Codex.
The inherited key matched .env.local during this investigation, so missing
credentials do not explain the reported failure in this session. That key passed
the complete direct HTTP handshake and all four shared launcher checks; the original frontend transport failure was not
independently reproduced. The shared launcher removes both the stale environment dependency
and frontend-specific HTTP handling. A valid key and Z.AI account access remain
required.
Validate parity at any time with:
Run full CI configuration validation (including tooling parity) with:
Building¶
Debug Build¶
Release Build¶
Optimized and stripped for production.
With Optional Features¶
# TLS support
cargo build --features tls
# Legacy full-mesh mode (UNAUTHENTICATED, unguarded; local interop testing only,
# never expose to the public internet)
cargo build --features legacy-fullmesh
# All features
cargo build --all-features
Running¶
Development¶
Validate a Custom Config¶
# The server automatically loads config.json from the working directory.
# -c is the short form of --validate-config: validate and exit without serving.
cargo run -- -c
To serve after validation succeeds, run cargo run without -c. Configuration
file selection is automatic; -c does not accept a path argument.
Validate Config¶
Print Resolved Config¶
Testing¶
Fast Local Iteration¶
Run the owning target during edits:
The editor keeps automatic save checks off so a full Clippy sweep cannot hold
Cargo's build lock during scoped agent checks. Editor analysis and completion
stay enabled. Use the Rust Analyzer Run flycheck command for a manual full
editor check, or set rust-analyzer.checkOnSave to true in the workspace settings.
See Rust Analyzer configuration.
Before publication, run cargo fmt and cargo clippy --all-targets
--all-features -- -D warnings once. Hosted CI runs the full test suites.
The commands below are for full verification or focused failure reproduction.
Run All Tests¶
Test with All Features¶
Run Specific Test¶
Test with Output¶
Integration Tests¶
E2E Tests¶
Linting¶
Format Check¶
Apply Formatting¶
Clippy (Default)¶
Clippy (All Features)¶
Markdown Linting¶
Check markdown files for formatting issues, missing language identifiers, and inconsistencies:
Auto-fix markdown issues where possible:
Common markdown linting rules enforced by CI:
- MD040: All code blocks must have language identifiers (e.g.,
```bashnot just```) - MD046: Use fenced code blocks consistently
- MD013: Lines should not exceed 120 characters (except tables)
- MD044: Proper capitalization of technical terms (JavaScript, GitHub, WebSocket, etc.)
See .markdownlint.json for the complete rule configuration.
Spell Checking¶
Check for typos in code and documentation:
Technical terms that are commonly flagged as typos are configured in .typos.toml. If a legitimate
technical term is flagged, add it to the [default.extend-words] section.
Benchmarks¶
For the correctness-first ARM campaign, use the audit and experiment ledger. It records subsystem coverage, measurement controls, and the next runner contract.
View results in target/criterion/report/index.html.
Measure steady-state heap traffic in the protocol-v3 relay fan-out and classified outbound queue:
The allocation harness uses a warmed current-thread runtime, prebuilt shared payload, and warmed classified queues to isolate coordinator routing, fan-out, and enqueue costs. Recipient snapshots and any actually-backpressured wait set are rebuilt inside each measured call; healthy recipients must resolve through the synchronous queue fast path, and the benchmark fails above four allocation operations per relay plus one fixed operation for the complete 4,096-relay sample. The harness excludes the inbound handler's stamp and message construction and its outer builder allocation. It repeats each sample five times and fails if the samples drift or if the attempt, enqueue, and receiver-drain ledgers do not prove every expected delivery. Its allocator uses sequentially consistent counters, so its output is an allocation baseline, not a latency benchmark; use the ordinary Criterion benchmarks for timing.
Measure the complete ingress-to-wire projection across frozen-v2 raw binary, protocol-v3 JSON and MessagePack, and mixed-format recipient cohorts:
This required CI benchmark checks five identical samples per 2-, 8-, and 16-player cell. It proves exact wire digests, codec-operation counts, delivery ledgers, queue drainage, and allocation-operation, reallocation, and allocated- byte ceilings. When projection work repeats within a relay's recipient set, it uses one 472-byte lazy frame cache; single-recipient, non-repeating, and frozen-v2 raw-passthrough relays do not allocate it.
The ignored real-WebSocket saturation diagnostic retains the exact 16-player rates used for release-profile comparisons without adding them to default CI:
cargo test --release --locked --all-features \
--test sixteen_player_matrix_e2e \
sixteen_player_relay_saturation_diagnostic_preserves_exact_delivery \
-- --exact --ignored --nocapture --test-threads=1
It runs 960 and 1,920 messages per second per sender, validates the complete delivery ledger, and prints throughput, latency, backpressure, and RSS observations. Compare revisions on the same idle machine in alternating order; the output is diagnostic rather than a portable capacity threshold. When the base revision predates this named test, apply only the harness diff to its clean worktree before building so both binaries execute identical inputs:
base_worktree=/path/to/clean/base-worktree
git diff 875057d -- tests/sixteen_player_matrix_e2e.rs |
git -C "$base_worktree" apply -
Do not interpret a zero-test filter result from the unmodified base as a comparison run.
Hosted-runner timing policy is measured separately by the
Relay Timing Observations workflow. Its daily/manual matrix runs five complete
all-feature clean-grid repetitions in a dedicated process on Linux, Windows,
and macOS, keeps every delivery, conformance, zero-backpressure, and
zero-eviction oracle, but deliberately does not gate the observed wall clock.
Each job retains raw output, one JSONL row per completed cell, and an explicit
attempt manifest for 30 days, the repository's configured maximum, including
RED-run artifacts. At the nominal daily cadence, that covers the 20-allocation
cohort plus a 10-day audit margin. Because missed or delayed schedules can
stretch the cohort, maintainers audit it incrementally and download an attempt
before its artifact expires when collection will exceed 30 days. Records
identify the event, attempt, commit, exact Rust toolchain, workload schema,
runner image, and completion state.
Issue #274's platform decision uses the first 20 consecutive scheduled,
first-attempt allocations per operating system after relay-clean-v1 lands
(100 requested observations per cell). Manual, pull-request, and rerun samples
are diagnostic only. The GitHub workflow-run ledger is the denominator: a RED,
cancelled, missing-artifact, incomplete, different-toolchain, or different
workload-version attempt invalidates that enablement cohort instead of being
dropped in favor of a later green run. A replacement cohort requires a
documented cause and a new workload version before collection restarts.
The dedicated-process observations may justify only an equivalently isolated, all-feature PR timing job; they cannot set a threshold for the concurrently loaded broad Nextest job. The current 250 ms ceiling is a candidate for that isolated job only if every eligible observation is below it and the largest retains at least 2x headroom (at most 125 ms). Otherwise the platform stays correctness-only. A new ceiling must not be invented from one outlier or one shared-runner sample.
The relay-clean-v1 cohort decides issue #274's lane placement:
- macOS stays correctness-only, permanently. Its dedicated-process distribution contains observations above the 125 ms headroom ceiling (worst p99 ≈ 165 ms) while every correctness oracle stayed green — direct evidence that hosted-macOS wall clock tracks shared-tenancy scheduling, not relay behavior.
- Linux and Windows keep their existing 250 ms p99 wall-clock ceiling in the broad Nextest matrix, unchanged (the gate excludes only macOS). The cohort validates keeping it: worst dedicated-process observations were ≈ 12 ms (Linux) and ≈ 24 ms (Windows) with zero backpressure, at least 5x inside the 125 ms candidacy bar.
- No new dedicated isolated PR timing job is added for any platform: no platform needs more gating than it already has, correctness oracles run everywhere, and issue #379's path-aware per-PR fan-out proposal was replayed against merge history and closed as not planned (2026-09-12).
Issue #274's comment history retains the full per-allocation audit (manifests, toolchain/workload pins, and the per-OS distributions, including the final cohort allocation).
P56's H14 validation uses the existing scenario-profiles job in
Verification Nightly, preserving the same profile-CI runner and precursor
context as the post-fix baseline. The exact
unsupported_message_pack_fallback_does_not_flap_weaker_recipient selector
runs after unrelated scenario or relay-matrix failures when checkout, the Rust
toolchain, and Nextest setup succeeded. Its raw log and versioned attempt
manifest are uploaded immediately afterward for the repository's 30-day
maximum, before a later experiment can fail or consume the job timeout.
That retention covers the 20-allocation cohort plus a 10-day margin only at the
nominal daily cadence; maintainers audit incrementally and download attempts
before expiry if schedule gaps stretch collection beyond 30 days.
The fixed cohort is h14-capacity-v1: the first 20 consecutive scheduled,
first-attempt allocations after the P56 production fix. Eligibility depends
only on the scheduled event and first run attempt, never on test outcome,
artifact presence, or completeness. A RED, cancelled, skipped, missing, or
incomplete attempt therefore breaks acceptance instead of creating a sliding
green-only window; reruns and manual/PR runs are diagnostic. Scheduled run
31070254464 was manually audited before manifests existed and remains the
first eligible observation because this evidence-only instrumentation changes
neither production code, workload, runner context, nor test oracle. A cohort
restart requires a documented causal fix or workload change and a new contract
version.
Code Coverage¶
Open coverage/index.html to view results.
Docker Development¶
Build Image¶
Build with Cache¶
docker build -t signal-fish-server --cache-from ghcr.io/ambiguous-interactive/signal-fish-server:latest .
Run Image¶
The image ships the secure compiled defaults (metrics auth and the app-ID allowlist on). For a local trial, opt into the open development posture explicitly:
docker run --rm -p 3536:3536 \
-e SIGNAL_FISH__SECURITY__ENFORCE_APP_ID_ALLOWLIST=false \
-e SIGNAL_FISH__SECURITY__REQUIRE_METRICS_AUTH=false \
signal-fish-server
With Custom Config¶
Project Structure¶
signal-fish-server/
├── src/
│ ├── main.rs # Binary entry point
│ ├── lib.rs # Library crate root
│ ├── server.rs # EnhancedGameServer core
│ ├── auth/ # Public app-ID allowlist
│ ├── config/ # Configuration
│ ├── coordination/ # Room coordination
│ ├── database/ # Database trait + impl
│ ├── protocol/ # Message types
│ ├── security/ # TLS and crypto
│ ├── server/ # Room service logic
│ └── websocket/ # WebSocket handlers
├── tests/ # Integration tests
├── benches/ # Benchmarks
├── config.example.json # Example config
├── Cargo.toml
└── Dockerfile
Adding a New Feature¶
- Write tests first
- Implement the feature
- Run full test suite
- Lint and format
-
Update documentation
-
Add doc comments to public APIs
- Update CHANGELOG.md
- Update README.md if user-facing
Debug Logging¶
Set log level:
Trace level (very verbose):
Module-specific logging:
Profiling¶
CPU Profiling¶
Memory Profiling¶
Common Development Tasks¶
Add a Protocol Message¶
- Add enum variant to
ClientMessageorServerMessageinsrc/protocol/messages.rs - Implement serialization/deserialization
- Add handler in
src/server.rsorsrc/server/submodule - Add tests in
tests/integration_tests.rs - Update protocol documentation in
docs/protocol.md
Add a Configuration Option¶
- Add field to appropriate config struct in
src/config/ - Add default value in
src/config/defaults.rs - Add validation in
src/config/validation.rs - Update
config.example.json - Add tests for default, custom, and invalid values
Add a New Endpoint¶
- Add route in
src/websocket/routes.rs - Implement handler function
- Add tests in
tests/e2e_tests.rs - Update endpoint documentation
Testing Strategy¶
Unit Tests¶
Place in the same file as the code:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_room_code_generation() {
let code = generate_room_code(6);
assert_eq!(code.len(), 6);
}
}
Integration Tests¶
Place in tests/ directory:
#[tokio::test]
async fn test_create_and_join_room() {
let server = create_test_server().await;
// Test multi-step flows
}
E2E Tests¶
Test full WebSocket flows:
#[tokio::test]
async fn test_websocket_connection() {
let addr = spawn_test_server().await;
let ws = connect_websocket(&addr).await;
// Test complete session
}
MSRV and Toolchain Management¶
Minimum Supported Rust Version (MSRV)¶
The project MSRV is defined in Cargo.toml (rust-version = "1.91.0"). This is the oldest
Rust compiler version guaranteed to build the project.
Verifying MSRV Consistency¶
Before committing changes that update the MSRV, verify all configuration files are consistent:
This script validates that the following files all use the same Rust version:
Cargo.toml(source of truth)rust-toolchain.toml(developer toolchain)clippy.toml(MSRV-aware lints)Dockerfile(production build environment)
Updating MSRV¶
When a dependency requires a newer Rust version, follow the MSRV update checklist:
- Update all configuration files:
Cargo.toml:rust-version = "1.XX.0"rust-toolchain.toml:channel = "1.XX.0"clippy.toml:msrv = "1.XX.0"-
Dockerfile:FROM rust:1.XX-bookworm -
Verify consistency:
- Test with new MSRV:
- Update documentation:
- Update this file's Prerequisites section
- Update
CHANGELOG.md - Document reason for MSRV bump in commit message
See MSRV Management for comprehensive MSRV management guidance.
Continuous Integration¶
The project uses GitHub Actions for CI. All PRs must pass:
cargo fmt --check- Code formattingcargo clippy --all-targets --all-features -- -D warnings- Rust lintingcargo test --all-features- All testscargo build --release- Release build- MSRV verification - Validates MSRV consistency and builds with exact MSRV
- Markdown linting - Validates markdown files for formatting and best practices
- Spell checking - Checks for typos in code and documentation
- YAML validation - Validates workflow files and configuration
- Actionlint - Validates GitHub Actions workflow syntax
Running All CI Checks Locally¶
Before pushing, run all CI checks locally:
# Format check
cargo fmt --check
# Clippy
cargo clippy --all-targets --all-features -- -D warnings
# Tests
cargo test --all-features
# Markdown linting
./scripts/check-markdown.sh
# Spell checking (install with: cargo install typos-cli)
typos
# MSRV consistency
./scripts/check-msrv-consistency.sh
# Fast hook-equivalent policy checks over unstaged work
pwsh -NoLogo -NoProfile -NonInteractive -File scripts/hooks/pre-commit.ps1 -Worktree
pwsh -NoLogo -NoProfile -NonInteractive -File scripts/hooks/pre-push.ps1 -Worktree
# Canonical local preflight, including hook readiness, LLM policy checks, markdown,
# workflow hygiene, docs/changelog consistency, and policy test suites.
./scripts/run-local-ci.sh
Enable git hooks as a final last-resort guard:
Release Process¶
Use the two reviewed workflows in the release runbook:
- Dispatch Prepare Release, review its generated version/changelog pull request, and merge only after required checks pass.
- Dispatch Release - Publish Crate from the default branch. It selects the reviewed version-introduction commit and creates the immutable annotated tag, crate, GitHub Release, binaries, SBOM, and versioned container artifacts.
- Run the independent public-artifact verification from the release runbook.
Do not edit release metadata or create and push version tags by hand during the normal release path.
Next Steps¶
- Library Usage - Embedding the server
- Architecture - System design