Skip to content

Spike: Docker Compose VNC test servers for compatibility testing - #334

Merged
sibson merged 8 commits into
mainfrom
claude/spike-server-fleet
Aug 13, 2026
Merged

Spike: Docker Compose VNC test servers for compatibility testing#334
sibson merged 8 commits into
mainfrom
claude/spike-server-fleet

Conversation

@sibson

@sibson sibson commented Aug 13, 2026

Copy link
Copy Markdown
Owner

What this is

Viability spike for Tier 1 of the server compatibility plan: a Docker-Compose-defined set of Linux VNC servers usable identically for local dev and CI. Verdict: viable — green end to end (~70s: image builds + healthcheck-gated startup, all per-server tests pass, screenshots captured and published, clean teardown).

Contents

  • tests/servers/Dockerfile — one multi-stage build: a base stage with what every server shares (X client to draw, EXPOSE, HEALTHCHECK) plus a stage per server, so adding a server is a stage rather than another near-copy. Built on debian:bookworm-slim so versions come from apt in a distro we pin, not from whatever an upstream desktop image bundles.
  • tests/servers/docker-compose.yml — services tigervnc (no auth, :5931), tigervnc-auth (VNC password, :5932), x11vnc (Xvfb-backed, :5933), each selecting its stage with target:. The two TigerVNC services are the same image, distinguished only by VNC_PASSWORD.
  • tests/servers/{tigervnc,x11vnc}-entrypoint.sh + shared draw-content.sh — the TigerVNC entrypoint serves SecurityTypes None or VncAuth depending on VNC_PASSWORD; draw-content.sh draws an X client and signals readiness once its window is mapped.
  • tests/servers/servers.mk (included from Makefile) — servers-up, servers-down, test-servers, screenshots
  • tests/functional/test_servers.py — parameterized connect → type → capture test per server; skips cleanly when a server port isn't open
  • tests/functional/capture_screenshots.py — screenshots every running server and builds the gallery; used by both make screenshots and CI
  • .github/workflows/spike-servers.yml — branch-push + manual-dispatch workflow proving the servers on ubuntu-latest

Screenshots are kept, not thrown away

  • captures land in tests/servers/screenshots/ (gitignored, VNCDOTOOL_SCREENSHOT_DIR overrides) instead of a tempfile, so they survive the run
  • the capture script writes a self-contained index.html gallery with every server's screenshot inlined — one file to open, locally or out of the CI artifact
  • in CI it also appends a server/port/resolution/size table to the job summary, so the run page shows what was captured without downloading the artifact
  • make screenshots does all of the above locally against whatever servers are up

What the per-server test proves

Passing means, in order: the RFB handshake completed for that server's security type (None, or VNC password auth), a key event was accepted, a framebuffer update was decoded to PNG, and that PNG is both the size the server serves and not a single flat colour. Previously an all-black framebuffer passed on PNG magic alone.

Findings worth keeping

  • api.connect() leaves the Twisted reactor thread running until api.shutdown() is called — without it, test processes/CI steps hang forever after passing. Hit twice during the spike; now guarded with tearDownModule and an explicit shutdown in the capture script. Also fresh evidence for the plan's Phase 1 "fail loudly, never hang" workstream.
  • An open RFB port is not readiness. Xvnc accepts connections the instant it launches, before anything is drawn, so a port-only healthcheck reports healthy against a blank framebuffer — a latent race that only surfaced once the test started asserting captures aren't flat. Readiness now also requires the drawn-content marker.
  • Debian/Ubuntu package split: vncpasswd lives in tigervnc-tools, needed alongside tigervnc-standalone-server.
  • Runners start with an empty Docker cache, so image builds are paid in full every run (~35s of ~70s) — layer caching only helps within a run.

Spike status

This is spike-quality code proving the approach. Follow-ups are recorded in docs/server-compatibility-plan.md: publish the screenshot gallery to GitHub Pages, stop paying the per-run image build tax (gha cache or GHCR), pin images by digest, fold into the main CI workflow, extend coverage (TightVNC, QEMU, TLS variants), deepen what the scenario suite asserts, and wire captures into the fixture/recording pipeline.

claude added 2 commits August 13, 2026 18:11
Adds a compatibility test bed of small, version-pinned Linux VNC servers
(tigervnc no-auth, tigervnc VNC-password-auth, x11vnc) built from in-repo
Dockerfiles, driven by tests/servers/fleet.mk (servers-up/servers-down/
test-fleet, included from the main Makefile) and exercised by
tests/functional/test_fleet.py, which connects with vncdotool.api.connect,
sends a key, and captures a screenshot per server -- skipping cleanly per
server when its port isn't up. Wires a spike-fleet GitHub Actions workflow
that builds/starts the fleet with compose healthchecks, runs the tests, and
uploads captured screenshots as artifacts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STWkReabCK9sJW2qQ45brM
The inline artifact-capture script never called vncdotool.api.shutdown(),
so the background Twisted reactor thread api.connect() starts kept the
step's python process alive indefinitely after the loop finished. Also cap
the step at 3 minutes so a similar regression fails fast instead of eating
the whole job timeout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STWkReabCK9sJW2qQ45brM
Comment thread .github/workflows/spike-fleet.yml Outdated
Comment thread .github/workflows/spike-fleet.yml Outdated
claude added 2 commits August 13, 2026 21:39
Address review on the server spike:

- rename "fleet" throughout: spike-servers.yml workflow, servers.mk,
  tests/functional/test_servers.py, make test-servers
- replace the inline heredoc python in the workflow with
  tests/functional/capture_screenshots.py

Screenshots are now easy to look at rather than thrown away in a
tempfile: tests write them to tests/servers/screenshots (override with
VNCDOTOOL_SCREENSHOT_DIR), the capture script builds a self-contained
index.html gallery of every server, and in CI it writes a summary table
to the job summary so the run page shows what was captured without
downloading the artifact. `make screenshots` does the same locally.

Also make the per-server test say what it proves: the capture must match
the size the server serves and must not be a single flat colour, so a
handshake that yields no framebuffer content fails instead of passing on
PNG magic alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJeE1y3aj3j6L7ByKEtaZj
@sibson sibson changed the title Spike: Docker Compose VNC server fleet for compatibility testing Spike: Docker Compose VNC test servers for compatibility testing Aug 13, 2026
claude added 2 commits August 13, 2026 21:50
Running it as a script puts tests/functional on sys.path but not the repo
root, so the CI step failed with ModuleNotFoundError: vncdotool -- CI
installs requirements-dev.txt, not the package itself. Put the repo root
on sys.path too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJeE1y3aj3j6L7ByKEtaZj
Screenshots are only reachable from the web as a zipped artifact today.
Record publishing the generated gallery to Pages as the Tier 1 follow-up,
with the constraints found while looking at it (no artifact preview, data:
URIs stripped from job summaries) and the orphan-branch interim option.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJeE1y3aj3j6L7ByKEtaZj
run: docker compose -f tests/servers/docker-compose.yml logs

- name: Upload screenshots
if: always()

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why if always()?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because those trailing steps are diagnostics and teardown, and the case you most want them in is the failing one: when a test fails, the container status, a screenshot of what the server was actually showing, and the server logs are the evidence you need — without if: always() a failed test step skips all of it, and down would leak containers. Added a comment in the workflow saying so.

The steps that are part of the test itself (start servers, run tests) deliberately don't have it, so a failure there still fails the job.


Generated by Claude Code

Comment thread tests/servers/tigervnc-auth/Dockerfile Outdated
tigervnc-tools \
x11-apps \
xauth \
netcat-openbsd \

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is netcat needed?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was only there for the HEALTHCHECK/compose readiness probe (nc -z 127.0.0.1 5900), which is what makes up --wait a reliable barrier. Not worth a package: switched to bash -c 'exec 3<>/dev/tcp/127.0.0.1/5900', same probe using bash's built-in, and netcat is gone from the images.


Generated by Claude Code

Comment thread tests/servers/tigervnc/Dockerfile Outdated
@@ -0,0 +1,25 @@
# Minimal TigerVNC (Xvnc) server, no authentication.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we layer these dockerfiles to avoid duplication?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — the three Dockerfiles are now one multi-stage tests/servers/Dockerfile: a base stage with what every server shares (X client to draw, EXPOSE, HEALTHCHECK), then a stage per server that installs only its own packages. Compose picks the stage with target:. Adding a server is one stage rather than another near-copy.


Generated by Claude Code

Comment thread tests/servers/tigervnc/entrypoint.sh Outdated
@@ -0,0 +1,25 @@
#!/bin/sh

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we not use a single entrypoint that optionally spawns with password?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — merged. tigervnc-entrypoint.sh reads VNC_PASSWORD: unset/empty gives -SecurityTypes None, set writes a password file with vncpasswd and gives -SecurityTypes VncAuth -PasswordFile .... The tigervnc-auth image is gone; both services are the same image (pinned to one build via image: in compose) and differ only by that env var.

The wait-for-X-then-draw block both entrypoints duplicated is now a shared draw-content.sh in the base image, so x11vnc-entrypoint.sh is down to three lines of actual work.


Generated by Claude Code

Comment thread tests/servers/tigervnc/Dockerfile Outdated
# third-party desktop image so the exact server version is controlled by
# apt in the base distro rather than by whatever a random upstream image
# happens to bundle.
FROM debian:bookworm-slim

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will gh actions cache these custom images or are we paying a build tax everytime. Address as todo rather than blocker.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're paying it. GitHub-hosted runners start each job with an empty Docker cache, so layer caching only helps within a run — every run rebuilds from scratch, ~35s of the ~70s total. Recorded as a Tier 1 follow-up in docs/server-compatibility-plan.md with the two options: docker/build-push-action with cache-from/to: type=gha, or build once into GHCR and have CI pull pinned digests (which folds into the digest-pinning item). Not treating it as a blocker.

The layering change above helps a little in the meantime — the TigerVNC services now share one image instead of building two, so it's two builds per run rather than three, over a shared base stage.


Generated by Claude Code

Comment thread Makefile
help:
@echo "test: run unit tests"
@echo "test-func: run functional tests"
@echo "servers-up: start the docker VNC test servers"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

servers-down?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed it — added to help.


Generated by Claude Code

claude added 2 commits August 13, 2026 22:37
Address review:

- one multi-stage tests/servers/Dockerfile: a shared base stage (X client,
  healthcheck) with a stage per server, selected by compose `target:`,
  instead of three near-identical Dockerfiles
- one TigerVNC entrypoint and image for both the no-auth and password
  services; VNC_PASSWORD unset means SecurityTypes None, set means VncAuth.
  The tigervnc-auth image is gone
- shared draw-content.sh for the wait-for-X-then-draw logic both servers had
- drop netcat: the healthcheck uses bash's /dev/tcp for the same probe
- explain in the workflow why the diagnostic steps carry `if: always()`
- Makefile help was missing servers-down

Also record the CI image build tax (empty Docker cache per run, ~35s) as a
plan follow-up with the gha-cache/GHCR options, per review.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJeE1y3aj3j6L7ByKEtaZj
Test server functional tests was failing on tigervnc and tigervnc-auth
with "capture is a single flat colour": the image HEALTHCHECK (and
therefore `docker compose up --wait`) only probed that the RFB TCP port
accepted a connection. For Xvnc that port opens the instant the process
starts, well before the backgrounded draw-content.sh has waited for the
display and mapped its xlogo window, so the compose wait can return
before there is anything on screen. x11vnc happened to be slow enough
starting up (Xvfb, then attaching x11vnc to it) that xlogo usually won
the race, masking the same bug there.

draw-content.sh now waits for the xlogo window to actually be mapped
(via xwininfo) before touching a /tmp/draw-content-ready marker, and the
Dockerfile HEALTHCHECK requires that marker in addition to the open port.
This ties container health to real screen content for all three servers
instead of relying on incidental startup-time differences.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJeE1y3aj3j6L7ByKEtaZj
@sibson
sibson merged commit d3cc4f0 into main Aug 13, 2026
11 checks passed
@sibson
sibson deleted the claude/spike-server-fleet branch August 13, 2026 22:46
sibson pushed a commit that referenced this pull request Aug 13, 2026
Now that #334 has landed, carry its review outcomes across:

* the two OS jobs were near-copies differing only by shell and script, so
  they collapse into one matrix job with fail-fast disabled, which keeps
  the "one OS failing must not take the other down" property without the
  duplication;
* say in the workflow why the trailing steps are if: always(), since that
  is the question the shape invites;
* probe the port with bash's own /dev/tcp rather than nc, the way the
  Docker servers now do;
* record why an open port *is* sufficient readiness here, given the Docker
  servers needed a content marker on top of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V2PPxkQw8HZ6YUU4hbeigc
sibson added a commit that referenced this pull request Aug 13, 2026
ep.sh landed at the repo root in #334: a debug copy of
tests/servers/tigervnc-entrypoint.sh with the trap removed and an echo in
front of Xvnc, so it prints the command rather than serving. Nothing
references it.

The `# codeql[py/weak-cryptographic-algorithm]` comment on des_encrypt()
did not suppress anything -- the alert was raised on that line anyway and
had to be dismissed in the UI -- so it was claiming to do something it
doesn't. The comment explaining why RFB mandates DES stays.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants