left4me/docs/superpowers/plans/2026-05-05-l4d2-host-smoke-test.md
2026-05-05 23:47:06 +02:00

21 KiB

L4D2 Host Smoke Test Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Validate the implemented l4d2host library and l4d2ctl CLI on disposable host ckn@10.0.4.128 with explicit user approval before every server-touching phase.

Architecture: This is a gated smoke-test runbook, not a feature implementation. Each task executes one bounded phase on the target server, captures command evidence, stops, and asks for approval before the next phase. The host library remains unchanged unless the smoke test identifies a defect that requires a separate fix plan.

Tech Stack: SSH, sudo, Python virtualenv/pip, Typer CLI entry point, SteamCMD, fuse-overlayfs/fuse3, systemd user services, journald, /opt/l4d2 runtime paths.


Source Design

  • docs/superpowers/specs/2026-05-05-l4d2-host-smoke-test-design.md

Gating Rule

Before running any task below, ask the user for explicit approval. After running a task, report evidence and stop. Do not continue to the next task until the user approves it.

Use this approval prompt before each task, replacing N and the title with the concrete task number and task title from this plan:

Approve Task 1: read-only inspection on ckn@10.0.4.128?

If any command fails, stop immediately and report:

Failed command: the exact command that failed
Exit/status: the observed exit code, signal, or SSH failure status
Relevant stdout/stderr: the shortest excerpt that explains the failure
Category: environment issue | host-lib bug | packaging/deploy issue | unclear
Recommended next action: one concrete next step based on the observed failure

Do not perform cleanup after a failure unless the user approves cleanup.

Files And Runtime Locations

  • Read: docs/superpowers/specs/2026-05-05-l4d2-host-smoke-test-design.md
  • Read: l4d2host/pyproject.toml
  • Read: l4d2host/**
  • Remote create: ~/l4d2host-smoke/
  • Remote create: ~/l4d2host-smoke/.venv/
  • Remote create: ~/l4d2host-smoke/specs/smoke.yaml
  • Remote create: ~/l4d2host-smoke/logs/
  • Remote create/modify: /opt/l4d2/
  • Remote create/modify: /opt/l4d2/installation/
  • Remote create/delete: /opt/l4d2/instances/smoke/
  • Remote create/delete: /opt/l4d2/runtime/smoke/
  • Remote create/modify: /home/ckn/.config/systemd/user/l4d2@.service
  • Local temporary create: /var/folders/h4/nnvk2kxs2sv7nr32kmb_4dm40000gn/T/opencode/l4d2-host-lib-smoke.tar.gz

Task 1: Read-Only Server Inspection

Files:

  • Read remote host state only.

  • Do not create, modify, mount, install, start, stop, or delete anything.

  • Step 1: Ask for approval

Ask:

Approve Task 1: read-only inspection on ckn@10.0.4.128?

Expected: user explicitly approves before commands are run.

  • Step 2: Verify SSH identity and sudo availability without changing state

Run:

ssh ckn@10.0.4.128 'set -eu; printf "user="; whoami; printf "host="; hostname; sudo -n true && printf "sudo=noninteractive\n" || printf "sudo=requires-password-or-unavailable\n"'

Expected: output includes user=ckn, a hostname, and either sudo=noninteractive or sudo=requires-password-or-unavailable.

  • Step 3: Inspect OS, package manager, Python, and runtime commands

Run:

ssh ckn@10.0.4.128 'set -u; printf "os_release=\n"; if [ -r /etc/os-release ]; then sed -n "1,12p" /etc/os-release; else uname -a; fi; printf "\npackage_managers=\n"; for c in apt-get dnf yum pacman zypper; do command -v "$c" || true; done; printf "\npython=\n"; command -v python3 || true; python3 --version 2>&1 || true; printf "\nruntime_commands=\n"; for c in steamcmd fuse-overlayfs fusermount3 systemctl journalctl loginctl; do printf "%s=" "$c"; command -v "$c" || true; done'

Expected: reports OS details, any available package manager, Python version if installed, and presence/absence of required runtime commands.

  • Step 4: Inspect systemd user state and /opt/l4d2 without changing it

Run:

ssh ckn@10.0.4.128 'set -u; printf "uid="; id -u; printf "groups="; id -nG; printf "\nlinger=\n"; loginctl show-user "$(whoami)" -p Linger 2>/dev/null || true; printf "\nsystemd_user=\n"; XDG_RUNTIME_DIR="/run/user/$(id -u)" systemctl --user is-system-running 2>&1 || true; printf "\nopt_l4d2=\n"; if [ -e /opt/l4d2 ]; then ls -ld /opt/l4d2 /opt/l4d2/* 2>/dev/null || true; else printf "/opt/l4d2 missing\n"; fi; printf "\nmounts=\n"; mount | grep /opt/l4d2 || true'

Expected: reports UID/groups, lingering state, systemd user status, /opt/l4d2 state, and existing /opt/l4d2 mounts if any.

  • Step 5: Report findings and stop

Report:

Task 1 evidence:
- SSH identity: report the observed user and hostname
- sudo availability: report whether noninteractive sudo worked
- OS/package manager: report OS family and detected package manager
- Python: report Python path and version, or absence
- runtime commands present/missing: report steamcmd, fuse-overlayfs, fusermount3, systemctl, journalctl, loginctl
- systemd user state: report lingering and systemctl --user result
- /opt/l4d2 state: report whether it exists, ownership, and any mounts

Approve Task 2: server preparation on ckn@10.0.4.128?

Expected: no server state has been changed.

Task 2: Server Preparation

Files:

  • Remote create/modify: /opt/l4d2/

  • Remote modify if required: system packages

  • Remote modify if required: user lingering for ckn

  • Step 1: Ask for approval

Ask:

Approve Task 2: server preparation on ckn@10.0.4.128?

Expected: user explicitly approves before commands are run.

  • Step 2: Install baseline packages using the detected package manager

Run exactly one of these command blocks based on Task 1 package-manager output.

For Debian/Ubuntu with apt-get:

ssh ckn@10.0.4.128 'set -eu; sudo apt-get update; sudo DEBIAN_FRONTEND=noninteractive apt-get install -y python3 python3-venv python3-pip curl ca-certificates tar gzip fuse-overlayfs fuse3'

Expected: command exits 0 and packages are installed or already current.

For Fedora/RHEL-like systems with dnf:

ssh ckn@10.0.4.128 'set -eu; sudo dnf install -y python3 python3-pip curl ca-certificates tar gzip fuse-overlayfs fuse3'

Expected: command exits 0 and packages are installed or already current.

For RHEL-like systems with yum and no dnf:

ssh ckn@10.0.4.128 'set -eu; sudo yum install -y python3 python3-pip curl ca-certificates tar gzip fuse-overlayfs fuse3'

Expected: command exits 0 and packages are installed or already current.

For Arch with pacman:

ssh ckn@10.0.4.128 'set -eu; sudo pacman -Sy --noconfirm python python-pip curl ca-certificates tar gzip fuse-overlayfs fuse3'

Expected: command exits 0 and packages are installed or already current.

  • Step 3: Install SteamCMD from Valve tarball

Run:

ssh ckn@10.0.4.128 'set -eu; sudo mkdir -p /opt/steamcmd; curl -fsSL https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz -o /tmp/steamcmd_linux.tar.gz; sudo tar -xzf /tmp/steamcmd_linux.tar.gz -C /opt/steamcmd; sudo ln -sf /opt/steamcmd/steamcmd.sh /usr/local/bin/steamcmd; steamcmd +quit'

Expected: steamcmd +quit exits 0 after bootstrapping or verifying SteamCMD.

  • Step 4: Prepare /opt/l4d2 and systemd user prerequisites

Run:

ssh ckn@10.0.4.128 'set -eu; sudo mkdir -p /opt/l4d2/installation /opt/l4d2/overlays /opt/l4d2/instances /opt/l4d2/runtime; sudo chown -R ckn:ckn /opt/l4d2; sudo loginctl enable-linger ckn; mkdir -p "$HOME/.config/systemd/user"; XDG_RUNTIME_DIR="/run/user/$(id -u)" systemctl --user daemon-reload || true'

Expected: /opt/l4d2 exists and is owned by ckn; lingering is enabled; systemd user daemon reload either succeeds or reports a diagnosable user-manager issue.

  • Step 5: Verify prepared commands and writable runtime paths

Run:

ssh ckn@10.0.4.128 'set -eu; command -v python3; python3 --version; command -v steamcmd; command -v fuse-overlayfs; command -v fusermount3; test -w /opt/l4d2; ls -ld /opt/l4d2 /opt/l4d2/installation /opt/l4d2/overlays /opt/l4d2/instances /opt/l4d2/runtime; XDG_RUNTIME_DIR="/run/user/$(id -u)" systemctl --user is-system-running 2>&1 || true'

Expected: all command -v checks print paths, /opt/l4d2 is writable, and systemd user state is visible.

  • Step 6: Report findings and stop

Report:

Task 2 evidence:
- package installation: report package manager used and install command status
- steamcmd bootstrap: report steamcmd path and bootstrap command status
- /opt/l4d2 ownership: report owner/group and writability for ckn
- systemd user/linger state: report loginctl linger value and systemctl --user result

Approve Task 3: deploy current host lib on ckn@10.0.4.128?

Expected: server is prepared for host-lib deployment.

Task 3: Deploy Current Host Lib

Files:

  • Local create: /var/folders/h4/nnvk2kxs2sv7nr32kmb_4dm40000gn/T/opencode/l4d2-host-lib-smoke.tar.gz

  • Remote create/modify: ~/l4d2host-smoke/

  • Remote create/modify: ~/l4d2host-smoke/.venv/

  • Step 1: Ask for approval

Ask:

Approve Task 3: deploy current host lib on ckn@10.0.4.128?

Expected: user explicitly approves before commands are run.

  • Step 2: Create local source archive from current host-lib component

Run from repository root:

tar --exclude='*.pyc' --exclude='__pycache__' --exclude='.pytest_cache' --exclude='*.egg-info' -C l4d2host -czf /var/folders/h4/nnvk2kxs2sv7nr32kmb_4dm40000gn/T/opencode/l4d2-host-lib-smoke.tar.gz .

Expected: command exits 0 and archive exists at /var/folders/h4/nnvk2kxs2sv7nr32kmb_4dm40000gn/T/opencode/l4d2-host-lib-smoke.tar.gz.

  • Step 3: Copy source archive to remote host and unpack it

Run:

ssh ckn@10.0.4.128 'set -eu; rm -rf "$HOME/l4d2host-smoke"; mkdir -p "$HOME/l4d2host-smoke/src" "$HOME/l4d2host-smoke/logs" "$HOME/l4d2host-smoke/specs"'

Expected: remote smoke workspace is recreated.

Run:

scp /var/folders/h4/nnvk2kxs2sv7nr32kmb_4dm40000gn/T/opencode/l4d2-host-lib-smoke.tar.gz ckn@10.0.4.128:~/l4d2host-smoke/l4d2-host-lib-smoke.tar.gz

Expected: archive copies successfully.

Run:

ssh ckn@10.0.4.128 'set -eu; tar -xzf "$HOME/l4d2host-smoke/l4d2-host-lib-smoke.tar.gz" -C "$HOME/l4d2host-smoke/src"; test -f "$HOME/l4d2host-smoke/src/pyproject.toml"; test -f "$HOME/l4d2host-smoke/src/cli.py"'

Expected: source tree unpacks and expected files exist.

  • Step 4: Install host lib into remote virtualenv

Run:

ssh ckn@10.0.4.128 'set -eu; python3 -m venv "$HOME/l4d2host-smoke/.venv"; "$HOME/l4d2host-smoke/.venv/bin/python" -m pip install --upgrade pip; "$HOME/l4d2host-smoke/.venv/bin/python" -m pip install -e "$HOME/l4d2host-smoke/src"'

Expected: pip exits 0 and installs l4d2host in editable mode.

  • Step 5: Verify CLI command surface

Run:

ssh ckn@10.0.4.128 'set -eu; "$HOME/l4d2host-smoke/.venv/bin/l4d2ctl" --help | tee "$HOME/l4d2host-smoke/logs/l4d2ctl-help.log"; grep -E "install|initialize|start|stop|delete" "$HOME/l4d2host-smoke/logs/l4d2ctl-help.log"'

Expected: help output includes install, initialize, start, stop, and delete.

  • Step 6: Report findings and stop

Report:

Task 3 evidence:
- archive creation/copy: report local archive path and remote unpack path
- venv/pip install: report virtualenv path and pip install status
- l4d2ctl command surface: report the five commands found in help output

Approve Task 4: run l4d2ctl install on ckn@10.0.4.128?

Expected: host lib is installed on the server but no L4D2 server files have been downloaded by this task.

Task 4: Run l4d2ctl install

Files:

  • Remote create/modify: /opt/l4d2/installation/

  • Remote create: ~/l4d2host-smoke/logs/install.log

  • Step 1: Ask for approval

Ask:

Approve Task 4: run l4d2ctl install on ckn@10.0.4.128?

Expected: user explicitly approves before commands are run.

  • Step 2: Run install command and capture output

Run with a long timeout when executing:

ssh ckn@10.0.4.128 'bash -lc "set -o pipefail; \"$HOME/l4d2host-smoke/.venv/bin/l4d2ctl\" install 2>&1 | tee \"$HOME/l4d2host-smoke/logs/install.log\""'

Expected: command exits 0 after SteamCMD completes Windows and Linux platform app updates for app 222860.

  • Step 3: Inspect installation output paths

Run:

ssh ckn@10.0.4.128 'set -eu; test -d /opt/l4d2/installation; find /opt/l4d2/installation -maxdepth 3 \( -name srcds_run -o -name left4dead2 \) -print; du -sh /opt/l4d2/installation; tail -n 40 "$HOME/l4d2host-smoke/logs/install.log"'

Expected: output includes /opt/l4d2/installation/srcds_run, a left4dead2 path, install directory size, and recent SteamCMD log lines.

  • Step 4: Report findings and stop

Report:

Task 4 evidence:
- l4d2ctl install exit: report exit status and SteamCMD completion status
- installed paths: report srcds_run and left4dead2 paths found under /opt/l4d2/installation
- install log excerpt: report the last relevant SteamCMD lines

Approve Task 5: run smoke instance lifecycle on ckn@10.0.4.128?

Expected: L4D2 dedicated server files exist under /opt/l4d2/installation.

Task 5: Run Instance Lifecycle Smoke Test

Files:

  • Remote create: ~/l4d2host-smoke/specs/smoke.yaml

  • Remote create/modify/delete: /opt/l4d2/instances/smoke/

  • Remote create/modify/delete: /opt/l4d2/runtime/smoke/

  • Remote create/modify: /home/ckn/.config/systemd/user/l4d2@.service

  • Remote create: ~/l4d2host-smoke/logs/lifecycle.log

  • Step 1: Ask for approval

Ask:

Approve Task 5: run smoke instance lifecycle on ckn@10.0.4.128?

Expected: user explicitly approves before commands are run.

  • Step 2: Create minimal smoke spec

Run:

ssh ckn@10.0.4.128 'set -eu; mkdir -p "$HOME/l4d2host-smoke/specs"; printf "%s\n" "port: 27015" "arguments:" "  - -insecure" "  - +map" "  - c5m1_waterfront" "config:" "  - hostname left4me-smoke" > "$HOME/l4d2host-smoke/specs/smoke.yaml"; sed -n "1,20p" "$HOME/l4d2host-smoke/specs/smoke.yaml"'

Expected: spec file shows port 27015, no overlays, three arguments, and one config line.

  • Step 3: Initialize the smoke instance

Run:

ssh ckn@10.0.4.128 'bash -lc "set -o pipefail; \"$HOME/l4d2host-smoke/.venv/bin/l4d2ctl\" initialize smoke -f \"$HOME/l4d2host-smoke/specs/smoke.yaml\" 2>&1 | tee \"$HOME/l4d2host-smoke/logs/initialize.log\""'

Expected: command exits 0.

Run:

ssh ckn@10.0.4.128 'set -eu; test -f /opt/l4d2/instances/smoke/instance.env; test -f /opt/l4d2/instances/smoke/server.cfg; test -d /opt/l4d2/runtime/smoke/upper; test -d /opt/l4d2/runtime/smoke/work; test -d /opt/l4d2/runtime/smoke/merged; sed -n "1,20p" /opt/l4d2/instances/smoke/instance.env; sed -n "1,20p" /opt/l4d2/instances/smoke/server.cfg; test -f "$HOME/.config/systemd/user/l4d2@.service"'

Expected: instance files and runtime directories exist; instance.env contains L4D2_PORT=27015 and L4D2_LOWERDIRS=/opt/l4d2/installation.

  • Step 4: Start the smoke instance

Run:

ssh ckn@10.0.4.128 'bash -lc "set -o pipefail; XDG_RUNTIME_DIR=/run/user/$(id -u) \"$HOME/l4d2host-smoke/.venv/bin/l4d2ctl\" start smoke 2>&1 | tee \"$HOME/l4d2host-smoke/logs/start.log\""'

Expected: command exits 0 or fails with actionable stdout/stderr that identifies an environment or host-lib issue.

  • Step 5: Inspect service, mount, status API, and logs API

Run:

ssh ckn@10.0.4.128 'set -u; XDG_RUNTIME_DIR="/run/user/$(id -u)" systemctl --user status l4d2@smoke.service --no-pager 2>&1 | sed -n "1,80p"; printf "\nmount_state=\n"; mount | grep "/opt/l4d2/runtime/smoke/merged" || true; printf "\nstatus_api=\n"; "$HOME/l4d2host-smoke/.venv/bin/python" -c "from l4d2host.status import get_instance_status; print(get_instance_status(\"smoke\"))"; printf "\nlogs_api=\n"; "$HOME/l4d2host-smoke/.venv/bin/python" -c "from itertools import islice; from l4d2host.logs import stream_instance_logs; [print(line) for line in islice(stream_instance_logs(\"smoke\", lines=50, follow=False), 20)]"'

Expected: service status is visible, mount state is reported, status API prints an InstanceStatus, and logs API prints up to 20 recent journal lines.

  • Step 6: Stop the smoke instance

Run:

ssh ckn@10.0.4.128 'bash -lc "set -o pipefail; XDG_RUNTIME_DIR=/run/user/$(id -u) \"$HOME/l4d2host-smoke/.venv/bin/l4d2ctl\" stop smoke 2>&1 | tee \"$HOME/l4d2host-smoke/logs/stop.log\""'

Expected: command exits 0, service stops, and overlay unmount command succeeds.

  • Step 7: Delete the smoke instance and verify repeated delete

Run:

ssh ckn@10.0.4.128 'bash -lc "set -o pipefail; XDG_RUNTIME_DIR=/run/user/$(id -u) \"$HOME/l4d2host-smoke/.venv/bin/l4d2ctl\" delete smoke 2>&1 | tee \"$HOME/l4d2host-smoke/logs/delete-1.log\""'

Expected: command exits 0 and removes /opt/l4d2/instances/smoke and /opt/l4d2/runtime/smoke.

Run:

ssh ckn@10.0.4.128 'set -u; if [ -e /opt/l4d2/instances/smoke ] || [ -e /opt/l4d2/runtime/smoke ]; then ls -ld /opt/l4d2/instances/smoke /opt/l4d2/runtime/smoke 2>/dev/null; exit 1; fi; printf "smoke instance/runtime removed\n"'

Expected: output is smoke instance/runtime removed.

Run:

ssh ckn@10.0.4.128 'bash -lc "set -o pipefail; XDG_RUNTIME_DIR=/run/user/$(id -u) \"$HOME/l4d2host-smoke/.venv/bin/l4d2ctl\" delete smoke 2>&1 | tee \"$HOME/l4d2host-smoke/logs/delete-2.log\""'

Expected: command exits 0, proving missing instance/runtime delete is a no-op success.

  • Step 8: Report findings and stop

Report:

Task 5 evidence:
- spec creation: report remote spec path and rendered YAML
- initialize: report exit status and created instance/runtime files
- start: report exit status and start log result
- service/mount status: report systemd user service state and overlay mount state
- status API: report printed InstanceStatus value
- logs API: report whether journal lines were returned
- stop: report exit status and unmount result
- delete and repeated delete: report first delete status, path removal status, and second delete status

Approve Task 6: cleanup decision on ckn@10.0.4.128?

Expected: smoke instance has been deleted, and /opt/l4d2/installation remains available for later web-app testing unless cleanup removes it.

Task 6: Cleanup Decision

Files:

  • Remote optional delete: ~/l4d2host-smoke/

  • Remote optional delete: /opt/l4d2/installation/

  • Remote optional delete: /opt/l4d2/

  • Step 1: Ask for cleanup preference

Ask:

Cleanup options for ckn@10.0.4.128:
1. Keep /opt/l4d2/installation and remove only ~/l4d2host-smoke
2. Keep everything for debugging/later web testing
3. Remove all smoke-test artifacts including /opt/l4d2

Which cleanup option should I run?

Expected: user selects one option before cleanup commands are run.

  • Step 2: Run selected cleanup command

For option 1:

ssh ckn@10.0.4.128 'set -eu; rm -rf "$HOME/l4d2host-smoke"; printf "kept /opt/l4d2, removed ~/l4d2host-smoke\n"'

Expected: remote smoke workspace is removed; /opt/l4d2/installation remains.

For option 2:

ssh ckn@10.0.4.128 'set -eu; printf "kept ~/l4d2host-smoke and /opt/l4d2 for later inspection\n"; ls -ld "$HOME/l4d2host-smoke" /opt/l4d2 /opt/l4d2/installation 2>/dev/null || true'

Expected: no cleanup is performed; paths are listed if present.

For option 3:

ssh ckn@10.0.4.128 'set -eu; rm -rf "$HOME/l4d2host-smoke"; sudo rm -rf /opt/l4d2; printf "removed ~/l4d2host-smoke and /opt/l4d2\n"'

Expected: remote smoke workspace and /opt/l4d2 are removed.

  • Step 3: Report final state

Run:

ssh ckn@10.0.4.128 'set -u; printf "workspace=\n"; ls -ld "$HOME/l4d2host-smoke" 2>/dev/null || printf "~/l4d2host-smoke missing\n"; printf "\nopt_l4d2=\n"; ls -ld /opt/l4d2 /opt/l4d2/installation 2>/dev/null || printf "/opt/l4d2 or installation missing\n"; printf "\nsmoke_mounts=\n"; mount | grep /opt/l4d2 || true; printf "\nsmoke_service=\n"; XDG_RUNTIME_DIR="/run/user/$(id -u)" systemctl --user status l4d2@smoke.service --no-pager 2>&1 | sed -n "1,30p" || true'

Expected: final state matches selected cleanup option; no smoke mounts remain.

  • Step 4: Summarize smoke-test result

Report:

Host-lib smoke-test result:
- Server inspected: state whether Task 1 completed successfully
- Server prepared: state whether Task 2 completed successfully
- Host lib deployed: state whether Task 3 completed successfully
- l4d2ctl install validated: state whether Task 4 completed successfully
- lifecycle validated: state whether Task 5 completed successfully
- cleanup option applied: state which cleanup option was applied
- host-lib defects found: list defects found, or state that none were found during the smoke test
- recommended next phase: choose web-app lifecycle job wiring or host-lib fix plan based on evidence

Expected: final report clearly states whether to proceed to web-app lifecycle job wiring or stop for a host-lib fix.


Self-Review Checklist

  • Spec coverage: all six design steps are represented as gated tasks.
  • Approval constraint: every server-touching task starts with an explicit approval step.
  • Failure policy: failure reporting and no automatic cleanup are documented.
  • Evidence: each task has exact commands and expected evidence.
  • Scope: no web-app implementation is included in this plan.