Files
ssh-netbox-wrapper/testenv/README.md
T
Sebastian UnterschützandClaude Opus 5.5 699b9469b6 test: check what a real terminal renders for multi, zellij, and the TUI
tests/16-render.sh runs tmux, zellij, and the TUI in xterm on Xvfb,
takes screenshots, and reads them back with tesseract: remote prompts
in all panes, borders and titles with the hosts, [SYNC] appearing and
disappearing, panes side by side, broadcast output in every pane and
input in one pane only without it, windows split by panes_per_window,
the status bar, and TUI marks. Screenshots stay in testenv/.artifacts.

OCR reads three preprocessing variants (plain, stretched for dim gray,
brightest channel for colored text) and normalizes characters that
DejaVu Sans Mono makes ambiguous (1/l, C/().

The screenshots showed tmux cutting the session name in the status bar
to "[netssh-pv"; netssh multi now widens status-left-length for its
own session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 13:14:34 +01:00

5.6 KiB

netssh test environment

A local Docker environment for testing every netssh feature against real services: two NetBox instances, SSH targets on three networks, a jump host, and a client with ssh, sftp, scp, tmux, zellij, bash, zsh, and fish. It is for local use only and is not part of CI/CD.

testenv/up.sh            # build, start, seed NetBox (first start ~6 min, then ~1 min)
testenv/test.sh          # run all tests; testenv/test.sh tunnel runs only matching files
testenv/shell.sh         # interactive shell in the client, netssh configured
testenv/down.sh          # stop and delete everything (volumes, generated state)

test.sh rebuilds the client image first, so the tests always run the current source. NO_BUILD=1 testenv/test.sh skips that. Exit code 1 if any check fails.

Requirements: Docker with Compose v2, about 3 GB RAM, ports 18000/18001 on 127.0.0.1 free.

Topology

public 10.231.0.0/24 + fd00:30::/64   mgmt 10.231.1.0/24           prod 10.231.2.0/24
 client .10 ────────────┬─ bastion .30 ───────────────────────────────┬─ (only via bastion)
 netbox-fsn .20         │                 fsn1-pve1..3 .11-.13 (mgmt0) │  fsn1-pve1..3 .11-.13 (eth0)
 netbox-lab .21         │                 lab-db1 .41, lab-web1 .42    │  web1 .21, web2 .22
 legacy-box .50         client .10 ───────┘
 v6-host fd00:30::60
NetBox Host Kind Primary IP Other Tags
fsn fsn1-pve1..3.hoster.test device, cluster pve-fsn1 eth0 = prod (needs bastion) mgmt0 = mgmt (direct) pve
fsn bastion.hoster.test device 10.231.0.30 bastion
fsn v6-host device fd00:30::60 (IPv6 only) ipv6
fsn web1, web2 VM, cluster pve-fsn1 prod (needs bastion) web
lab lab-db1 device 10.231.1.41 db
lab web1 VM, cluster lab-k8s 10.231.1.42 web
— legacy-box (10.231.0.50) static host
  • fsn uses a v2 token (nbt_…), lab a legacy v1 token; both come from .state/env.
  • web1 exists in both instances (ambiguity, instance/host).
  • Docker DNS resolves the *.hoster.test names on the mgmt network (hostname strategy).
  • Every target runs sshd on ports 22 and 2222 with users ops and admin, and an HTTP server on its own 127.0.0.1:8081 for tunnel tests ("hello from HOST").
  • NetBox UI: http://127.0.0.1:18000 (fsn), http://127.0.0.1:18001 (lab), login admin / admin.

Files

Path Purpose
compose.yaml All services and networks.
client.Dockerfile, client-entrypoint.sh Client image; builds netssh from the repository.
targets/ SSH target image (sshd config, entrypoint).
seed/seed.py Fills both NetBox instances via the REST API (idempotent), writes .state/.
tests/run.sh, tests/lib.sh Test runner and helpers (check, expect_out, expect_eq, expect_fail, wait_for).
tests/NN-*.sh Test files; each runs in a fresh HOME.
tests/drive.py Drives netssh configure in huh's accessible mode.
.keys/, .state/ Generated test key and seed output (git-ignored).
.artifacts/ Screenshots from tests/16-render.sh (git-ignored).

Writing tests

Each tests/NN-name.sh is sourced by run.sh in a subshell with a new HOME, XDG_CONFIG_HOME, and XDG_CACHE_HOME. Write the netssh config with config <<'EOF' … EOF (both NetBox instances plus your YAML) or config_only (no NetBox), then use the helpers:

config <<'EOF'
ssh: {default_user: ops}
EOF
expect_eq   "connect"   "fsn1-pve1" netssh fsn/fsn1-pve1.hoster.test hostname
expect_out  "dry run"   "-J ops@10.231.0.30" netssh --dry-run fsn/web1
expect_fail "ambiguous" "found 2 exact matches" netssh web1

ssh always reads /home/tester/.ssh/config (OpenSSH ignores $HOME): it uses the test key, BatchMode, StrictHostKeyChecking accept-new, and LogLevel ERROR. Tests that need their own known_hosts set ssh.options.UserKnownHostsFile. The TUI test runs netssh inside tmux and reads the screen with tmux capture-pane. The multi test (15-multi.sh) opens panes in real tmux and zellij sessions and checks that broadcast typing reaches every node.

What a user sees: 16-render.sh

The render test runs tmux, zellij, and the TUI in a real terminal emulator — xterm on a virtual X server (Xvfb) — takes screenshots, and reads them back with OCR (tesseract). It checks what is actually drawn: remote prompts in all panes, borders and titles with the host names, [SYNC] appearing and disappearing, panes side by side, broadcast output in every pane, input in one pane only without broadcast, the status bar, and the TUI marks.

testenv/test.sh render          # then look at the screenshots:
ls testenv/.artifacts/          # tmux-1.png, zellij-2.png, tui-3.png, …

Helpers in lib.sh: start_screen/stop_screen, term CMD… (full-screen xterm), shot NAME, screen_shows NAME TEXT…, screen_count NAME TEXT. OCR reads each screenshot in three preprocessing variants (plain, stretched for dim gray, brightest channel for colored text) and normalizes characters it confuses in DejaVu Sans Mono (1/l, C/(), so checks stay robust without becoming vague.

Not covered

  • The installer (install.sh) downloads release assets from Gitea; it is tested by the Go tests with local fixtures.
  • macOS paths and behaviour.
  • netssh uninstall runs only with --dry-run, so the client keeps its binary.
  • Rendering is checked in xterm with DejaVu Sans Mono. Other terminals and fonts may differ, e.g. zellij's Powerline arrows need a Nerd Font.