Manual Test Backlog
September 5, 2026 · View on GitHub
Pre-release human-verification checks. Every row here is something that CI can't catch and a human has to confirm before shipping.
Convention: Append rows when a new feature ships with a manual check. Delete rows when an automated test starts covering the check — never annotate "now automated"; the row goes away.
| # | Check | Steps | Why manual |
|---|---|---|---|
| 1 | Two real WG profiles available for multi-tunnel testing | Have corp (0/0) and lab (10/8) configs ready | Environment setup; can't be automated |
| 2 | Real OpenVPN profile with username+password auth (OVPN 2.4+) | Have .ovpn file with auth-user-pass directive | Environment setup |
| 3 | OVPN 2.3.x binary available for version-rejection path | apt install openvpn=2.3.* or equivalent | Test fixture; requires installing old binary |
| 4 | Second OS user account for daemon UID-gate adversarial test | useradd vortix-adversary | Test fixture; cross-UID setup |
| 5 | Multi-tunnel routing: corp owns default, lab owns 10/8 | vortix up corp; vortix up lab; ip route get 1.1.1.1 shows dev wg-corp; ip route get 10.x.y.z shows dev wg-lab | Multi-tunnel netns harness not built (plan 002 U1+U2) |
| 6 | Multi-tunnel real-internet exit IP differs per tunnel | curl https://api.ipify.org via corp returns IP A; same via lab returns IP B (when lab is primary) | Requires real public IPs; netns can't fake this |
| 7 | 3+ tunnels overflow ladder in header degrades cleanly | Connect 6 tunnels on a 60-col terminal; verify Tier 1 → Tier 2 → Tier 3 (dot-row) transitions; never wraps | TUI rendering; needs ratatui snapshot harness (Phase 2) |
| 8 | (removed — auto-promote feature retired; primary changes are silent, user manages manually) | — | — |
| 9 | (removed — see #8) | — | — |
| 10 | (removed — see #8) | — | — |
| 11 | Multi-tunnel disconnect via d key disconnects single tunnel | Connect 2 tunnels; focus secondary in sidebar; press d; only that one disconnects | TUI keybinding; needs snapshot harness |
| 12 | Shift+D with N≥2 fires "Disconnect all N?" confirm dialog | Connect 3 tunnels; press Shift+D; verify dialog renders; Y disconnects all | Same as #11 |
| 13 | Tab cycles Connection Details focus across active tunnels | 2 tunnels up; focus Connection Details; press Tab; focus advances; wraps at end | Same as #11 |
| 14 | c cancels an in-flight Connecting state | Connect a slow tunnel; while Connecting (◐), focus Details + press c; sidebar clears | Same as #11 |
| 15 | Multi-tunnel iptables ruleset has per-tunnel ACCEPT + RFC1918 carve | After vortix up corp lab: sudo iptables-save shows -A OUTPUT -o wg-corp/wg-lab -j ACCEPT + no -d 10.0.0.0/8 (carved by lab's 10/8 declaration) | Multi-tunnel netns harness deferred (plan 002 U3) |
| 16 | Atomicity probe: no traffic leak window during tunnel transition | While corp+lab up, run continuous curl 10.99.0.99 --max-time 2 loop; externally kill corp; assert no BLOCKED → ACCEPT → BLOCKED gap | Same as #15 |
| 17 | macOS pf ruleset has per-tunnel pass-out rules + RFC1918 carve | macOS only: sudo pfctl -s rules shows per-interface pass-outs + RFC1918 carve | Same as #15; macOS-only |
| 18 | Every WG launch strips DNS = ... before wg-quick | Connect a WG profile containing DNS addresses and a search domain. Verify the short-lived mode-0600 config given to wg-quick has no DNS= line, while dns-policy.state records the parsed request and the platform adapter owns the effective resolver resource. | Protocol argv plus real resolver integration crosses process/kernel boundaries |
| 19 | Every OVPN launch suppresses protocol-side DNS mutation | Connect OpenVPN as primary and secondary; ps aux | grep openvpn shows pull filters for dhcp-option DNS and dhcp-option DOMAIN, while pushed options from the OpenVPN log appear in the coordinator policy. | Requires a real server PUSH_REPLY and live OpenVPN process |
| 20 | OVPN 2.3.x as secondary is rejected with "OpenVPN 2.4+ required" | With OVPN 2.3 installed: connect any primary; attempt OVPN 2.3 as secondary; vortix refuses before connect attempt | Needs real OVPN 2.3 binary install |
| 21 | fwmark hijack warning shows on Connection Details for WG without FwMark | Connect WG without FwMark = ... as secondary; Connection Details shows ⚠ Fwmark hijack risk... line | TUI rendering |
| 22 | fwmark warning DOES NOT show when secondary has FwMark = 51820 | Same as #21 but with FwMark set; no warning line | TUI rendering |
| 23 | fwmark warning DOES NOT show when single tunnel (not secondary) | Connect just the WG-without-FwMark profile alone; no warning | TUI rendering |
| 24 | Sidebar empty-state renders correctly | Start vortix with no profiles imported; sidebar shows "No profiles" message | TUI rendering |
| 25 | Sidebar scroll with 10+ profiles works | Import 15 profiles; j/k scroll the list; selection wraps at edges | TUI rendering |
| 26 | Header degrades at 50 / 60 / 80 / 100 / 120 columns | Resize terminal to each width; header never wraps; tunnels strip uses the right Tier | TUI rendering at variable widths |
| 27 | Sidebar truncates rows cleanly at 80x12 | Resize to 80x12; sidebar still renders without panic; rows truncated with ellipsis | TUI rendering |
| 28 | NO_COLOR=1 renders badges via Unicode shape only | NO_COLOR=1 vortix; status badges (● etc.) still distinguishable by shape, not just color | TUI rendering + accessibility |
| 29 | VoiceOver / Orca announces sidebar rows | macOS VoiceOver or Linux Orca enabled; sidebar row text is announced; no purely-color signals required for comprehension | Screen-reader accessibility; no automation hook |
| 30 | AwaitingUserInput hint renders on Connection Details | OVPN with 2FA prompt — Connection Details shows ⚠ Press [Enter] to provide 2FA code/passphrase/input | TUI rendering |
| 31 | Demoted ex-primary Role shows Addressable (0.0.0.0/0, suppressed) | Connect 2× 0/0 profiles; one takes over; the demoted one shows the suppressed role in Connection Details | TUI rendering |
| 32 | Security Guard shows EXPOSED with 0 tunnels | No tunnels up; Security panel headline = EXPOSED | TUI rendering |
| 33 | Security Guard shows PARTIAL with secondaries-only | Connect tunnels that don't claim 0/0; Security panel = PARTIAL + KS-mode-aware Killswitch bullet | TUI rendering |
| 34 | (retired — replaced by rows 89–99 covering the v0.4.2 dual-stack Real IP / Exit IP redesign) | — | — |
| 35 | Passive candidate socket is owner-only and non-privileged | Run vortix daemon as the logged-in user; verify the socket at ${XDG_RUNTIME_DIR}/vortix.sock (Linux) or ${TMPDIR}/vortix.sock (macOS) is owned by that user with mode 0600. Starting a second candidate must refuse the live socket. | Real filesystem ownership and service environment |
| 36 | Read-only CLI ops bypass daemon when socket absent | Stop daemon; run vortix status; falls back to direct scanner with exit 0 | Same as #35 |
| 37 | Cross-UID adversarial: different non-root user can't talk to daemon | Run daemon as user A; as user B attempt socat - UNIX-CONNECT:/path/to/vortix.sock; daemon closes after first frame with UID-mismatch error | Needs two-user setup; deferred (plan 002 U10) |
| 38 | Wire-format compatibility: v0.3.x client → v2 daemon socket fails cleanly | Old vortix binary on PATH; vortix status --json against running v2 daemon; structured error, not silent mis-parse | Needs real v0.3.x binary install |
| 39 | SIGTERM to passive candidate drains clients and cleans its socket | With snapshot and subscription clients connected, kill -TERM <daemon-pid>; clients close, the original socket unlinks, and active tunnels remain unchanged. Replacing the socket pathname before termination must leave the replacement untouched. | Real signals, descriptors, and filesystem identity |
| 40 | Passive candidate restart observes without reconnecting | Start the candidate while real WireGuard/OpenVPN tunnels are active, terminate it, and start it again. Pass: snapshots repopulate after scanning and process/firewall/DNS/route traces show no candidate-caused mutation or second protocol child. | Real process and kernel side-effect audit |
| 41 | V2 → V1 downgrade: v0.3.x reads V2 journal without erroring | Connect with current binary; revert to v0.3.x; journalctl replay tools skip unknown variants per #[non_exhaustive] | Needs real v0.3.x binary; release-time only |
| 42 | V2 → V1 downgrade: clean shutdown + v0.3.x cold start works | Per docs/MIGRATION.md 5-step procedure; v0.3.x launches successfully | Same as #41 |
| 43 | Network drop mid-handshake → FSM retries per budget | Use tc qdisc add dev <iface> root netem loss 100% mid-handshake; vortix retries; log entries readable | Fault injection deferred (Phase 2) |
| 44 | OVPN auth fails with wrong password → ✗ + re-prompt available | Connect with wrong password; sidebar shows ✗; auth overlay can re-open | Real OVPN server needed |
| 45 | Profile deleted while connected → tunnel disconnects on next Tick | Active tunnel; delete profile file; within ~1s tunnel goes down + sidebar updates | Real-fs + state coherence; partial automation possible |
| 46 | Out-of-disk during secret_file write → graceful error | mount -o remount,size=1 or ramdisk to fill /tmp; attempt vortix auth save; error returned; no half-written auth file | Fault injection deferred (Phase 2) |
| 47 | Profile config readable only by root, run as non-root → permission error | chmod 0400 ~/.config/vortix/foo.conf; vortix up foo as non-root; error surfaced; no path leak in stderr | Real-fs perms test; netns harness deferred |
| 48 | Auth file has mode 0600 and is owned by invoking user (not root via sudo) | sudo vortix up <ovpn-profile>; enter password; ls -la ~/.config/vortix/foo.auth shows mode 0600 + user (not root) | Real-fs setup needed (plan 002 U10) |
| 49 | Symlink attack on auth file is refused (O_NOFOLLOW) | Replace ~/.config/vortix/foo.auth with symlink to /etc/shadow; vortix up triggers auth re-save; write refused; /etc/shadow unchanged | Real-fs + attacker setup needed |
| 50 | ps aux does not leak OVPN credentials in command line | While OVPN running: ps auxf | grep openvpn | grep <password> returns 0 lines | Real-process scan; partial automation possible |
| 51 | /tmp/vortix-*/ temp WG configs are mode 0600 + unlinked on tunnel down | While WG connected: ls -la ${TMPDIR}/vortix-*/; mode 0600; after vortix down: directory gone | Real-fs assertion |
| 52 | macOS Apple Silicon smoke: full multi-tunnel happy path | Run multi-tunnel scenarios on an M-series MacBook | Cross-platform: real consumer hardware |
| 53 | macOS Intel smoke (if available) | Same as #52 on an Intel Mac | Cross-platform: real consumer hardware |
| 54 | Legacy Linux iptables cleanup smoke | Seed duplicate Vortix-owned IPv4/IPv6 OUTPUT jumps and chains from an older release, then run release-killswitch; all owned legacy state disappears while unrelated rules remain | Cross-platform: requires distro-specific install |
| 55 | Linux nftables host smoke | Run on a host where nft is the only backend | Cross-platform: requires distro-specific install |
| 56 | Windows: vortix does not crash (NG features stubbed) | vortix --version; vortix list; no panic on profile import; multi-tunnel features may be stubbed | Cross-platform: Windows |
| 57 | 10 active tunnels: TUI render budget under 16ms/frame | Connect 10 tunnels; observe TUI doesn't lag on Tab / sidebar nav | Perf benchmark cadence (Phase 2) |
| 58 | Killswitch ruleset rewrite latency sub-100ms at N=5 | Time anchor-scoped pfctl / one nft inet transaction during a 5-tunnel transition | Perf benchmark cadence (Phase 2) |
| 59 | 50 profiles loaded: sidebar scroll + search responsive | Synthesize 50 empty .conf files; verify no perceptible TUI lag | Perf benchmark cadence (Phase 2) |
| 60 | (removed — auto-promote feature retired) | — | — |
| 61 | Journal shows ConnectAttemptBlockedByConflict on overlay-cancelled connect | Trigger conflict overlay; cancel it; journal entry appears | Same as #60 |
| 62 | vpn-only engages firewall immediately while VPN is up | vortix up corp; vortix killswitch vpn-only; PF/nft read-back shows the exact Vortix-owned terminal block policy; before the P5d-era fix this silently produced "Armed" with no enforcement | Regression check for the vpn-only (AlwaysOn) semantic change (commit 34f07e3) |
| 63 | vpn-only keeps blocking through a VPN drop → reconnect cycle | With vpn-only engaged and corp up, externally wg-quick down corp; verify iptables -L OUTPUT -n still shows policy DROP between drop and reconnect; no ICMP escapes during the gap | Canonical Linux killswitch invariant |
| 64 | Per-profile concurrent retry: two profiles failing in parallel each retry independently | Connect two profiles with bad endpoints simultaneously; tail -f ~/.local/share/vortix/journal.jsonl | jq '.event' shows separate ConnectAttemptFailed/RetryScheduled entries per ProfileId with their own attempt counters; neither overwrites the other | Per-profile retry HashMap correctness; can't be observed via TUI single-slot view |
| 65 | D-4 auto-adopt of externally-started tunnel — authoritative iface case | Start the TUI (no profile connected); in another terminal run wg-quick up corp (or single openvpn --config ... outside vortix); within ~1 scanner tick the sidebar shows corp Connected, registry has the entry with interface_authoritative=true (no muted-dot, sidebar * if it owns kernel default route), killswitch slice includes corp's interface. WG on macOS is authoritative via /var/run/wireguard/<name>.name; Linux is always authoritative; OpenVPN on macOS is authoritative only when lsof Method A succeeds | Cross-process registry adoption from kernel state |
| 66 | CLI vortix down A while TUI tracks A+B does not clobber B's killswitch slice | TUI open with corp+lab Connected and AlwaysOn engaged; in another terminal vortix down corp; cat ~/.config/vortix/killswitch.toml still lists lab's interface + server_ips; firewall keeps lab's ACCEPT rule | Bug 1 regression check (commit 8e7181f) |
| 67 | Security Guard panel fits cleanly at 80×24 with no compaction drops | Resize the terminal to exactly 80×24; connect a primary VPN; verify the Security Guard panel shows both section words (Identity, Defense), all rows (Real IP, Exit IP, Location, DNS, Killswitch, Encryption, IPv6 — 7 content rows after the split-rows redesign), and the Updated … footer with no rows dropped or truncated mid-word. Pay particular attention to whether compact_to_fit is dropping the Location or IPv6 row at the bottom | |
| 68 | Security Guard panel alarm states pull the eye correctly | Trigger DNS leak (e.g. set system DNS to a non-VPN server while connected); confirm only the DNS row uses bright ✗ + bold and gains a sub-line; other rows stay muted. Repeat for Auto + dropped VPN — only the Killswitch row goes loud with press r to reconnect sub-line | TUI rendering — visual hierarchy regression check |
| 69 | WireGuard on macOS reports the real utunN device | macOS only: vortix up <wg-profile>; Connection Details VPN IP line shows @ utunN (not @ wg-corp or basename). Confirm byte-for-byte equality with route -n get 8.8.8.8 interface output. Plan 2026-06-01-001 U2 contract — protocol layer routes through Interface::resolve_wireguard_interface port | Real macOS kernel + wg-quick behavior; can't be simulated |
| 70 | Multi-OpenVPN primary-election byte-comparable with kernel | macOS only: connect ovpn-cert (full-tunnel); connect a SECOND ovpn profile via Shift+B (takeover overlay → Both). Verify sidebar * and header CONNECTED-name reflect whichever tunnel's routes win route -n get 8.8.8.8. Repeat in reverse order (split first, then primary). Both registry entries must store byte-for-byte real utunN from the log scrape, NOT colliding with the lowest-numbered utun. Plan 2026-06-01-001 scenarios #3 + #12 regression | Multi-PID iface attribution that automated tests can't verify without real openvpn processes |
| 71 | OpenVPN log-parse failure leaves no synthetic iface in registry | Force a failure of parse_kernel_interface (e.g. tail an .ovpn log mid-handshake before the daemon writes the device-open line); vortix surfaces TunnelError::DaemonExited mentioning "kernel interface"; no entry in the registry has interface = "openvpn-<name>" after the failure. Plan 2026-06-01-001 U1 — no synthetic fallback. | Real OpenVPN log timing; flaky to automate |
| 72 | Scanner does NOT promote Connecting → Connected pre-protocol-success | Connect a slow OpenVPN profile (insert artificial --connect-retry-max 99 --connect-retry 1 and a wrong remote that times out slowly). While scanner sees the openvpn process running, registry must stay in Connecting state (badge ◐, not ●) until the connect-timeout fires (handle_connection_timeout) — never jumps directly to Connected via scanner. Plan 2026-06-01-001 U4 regression — scanner-promotion removal | Race-window observation; the bug it prevents is silent corruption, not a crash |
| 73 | Externally-adopted OpenVPN on macOS multi-tunnel shows unauthoritative badge | macOS only: connect ovpn-cert through vortix. In another terminal, manually openvpn --config ovpn-auth.ovpn --daemon .... Scanner adopts the external tunnel within ~1 tick. Sidebar shows it with the muted/dim ● (not the bright SUCCESS green), Connection Details VPN IP line shows @ <iface> (external), NO asterisk (recompute_primary excludes it), vortix status --json data.primary does NOT name this profile. Plan 2026-06-01-001 R4 + U6. | Multi-OpenVPN per-PID iface ambiguity that automated tests can't simulate |
| 74 | Real IP cache survives startup-with-VPN-up race | Connect a profile via vortix; quit vortix while it's connected (or kill its process); externally verify the kernel tunnel is still up (route -n get 8.8.8.8 shows utunN). Re-launch vortix. The Security Guard Real IP row must show detecting… (NOT the VPN's exit IP — that would be the pre-fix bug). Now disconnect the tunnel through vortix; within ~1 telemetry tick the Real IP row populates with your actual ISP IP. Reconnect — Real IP stays frozen at the cached value, Exit IP shows the VPN's exit IP. Plan: real-IP gate requires scanner to have ticked AND kernel session count == 0 AND no Connected registry entries. | Race window observation that automated tests cover but only real launch-time timing on real kernel exercises end-to-end |
| 75 | Real IP persists across vortix restarts via real-ip.cache | Sequence: (a) launch vortix with no VPN up — wait for Real IP row to populate from telemetry (verify ~/.config/vortix/real-ip.cache exists with mode 600 and contains your ISP IP). (b) Connect a profile; verify Real IP row stays at your ISP IP (NOT the VPN's exit IP). (c) Quit vortix while connected. (d) Re-launch vortix while the kernel tunnel is still up — Real IP row must populate IMMEDIATELY from the cache (no detecting… placeholder) and show your ISP IP, not the VPN's exit IP. This is the load-from-disk path. (e) Move networks (different wifi) while vortix is closed, re-launch; cache is stale until you disconnect once, then auto-refreshes. | End-to-end cache persistence; covers the launch-with-VPN-up case that telemetry alone cannot solve |
| 76 | HISTORICAL — Approach A SCRV1-via-auth-file premise failed | Run on 2026-06-02 against OpenVPN 2.7.0 + ovpn-totp.ovpn (server 209.38.218.39:1196). Result: openvpn emits CHALLENGE: Enter TOTP code to stdout at us=289606 with username = '[UNDEF]' still in the parameter dump — proving the --auth-user-pass file is NOT read before the static-challenge prompt fires. The SCRV1 envelope on line 2 was never consulted; openvpn blocks on stdin waiting for the OTP and never daemonizes. Conclusion: Approach A cannot close #191 on modern openvpn. See docs/plans/2026-06-02-001-feat-openvpn-static-challenge-plan.md "Approach A is broken on OpenVPN 2.7" section. Row retained as the diagnostic artifact for the Approach B brainstorm. | The U0 spike outcome that retired Approach A |
| 77 | Non-MFA OpenVPN regression after the static-challenge wiring | Setup: a pre-existing OpenVPN profile WITHOUT static-challenge directive (e.g. ovpn-auth). Action: connect via TUI and via vortix up. Pass: behavior is identical to v0.3.1 — two-field auth overlay only, no OTP prompt, auth file contains user\npass\n after connect, connect timing and log output unchanged. | Regression guard for the most-trafficked OpenVPN path — the static-challenge code path is gated on the parser flag, so non-MFA profiles must be byte-for-byte untouched |
| 78 | Stale SCRV1 cleanup on vortix startup | Setup: any OpenVPN profile with saved credentials, vortix not running. Action: hand-corrupt the auth file with printf 'user\nSCRV1:cA==:MTIzNDU2\n' | sudo tee ~/.config/vortix/auth/<profile>.auth. Start vortix (TUI or any CLI subcommand). Pass: the file no longer exists (stat ~/.config/vortix/auth/<profile>.auth returns no such file), a warn-level tracing event mentioning the profile name appears in stderr when RUST_LOG=vortix::utils=warn. Next connect attempt re-prompts for credentials cleanly. | U6 safety net — applies whether or not Approach B is later implemented |
| 79 | (reserved — Approach B happy-path rows belong to the next plan) | — | — |
| 80 | (reserved — Approach B happy-path rows belong to the next plan) | — | — |
| 81 | (reserved — Approach B happy-path rows belong to the next plan) | — | — |
| 82 | (reserved — Approach B happy-path rows belong to the next plan) | — | — |
| 83 | (reserved — Approach B happy-path rows belong to the next plan) | — | — |
| 84 | Fresh resolved-native distro connects WG-with-DNS without resolvconf shim | On a clean Omarchy / Arch + systemd-resolved host with NO systemd-resolvconf or openresolv installed (pacman -Qi systemd-resolvconf errors), import a WG profile containing DNS = 1.1.1.1 (use scripts/test-profiles/wg-full.conf). Connect via TUI or vortix up wg-full. Pass: dep-check does NOT raise Missing dependencies: resolvconf (systemd); tunnel comes Connected; resolvectl status wg-full shows DNS Servers: 1.1.1.1 and Default Route: yes. Issue #190 acceptance signal. See "Linux test environments for rows 84–88" below for a 5-min Colima/Lima setup on macOS. | Headline #190 verification; needs a fresh systemd-resolved host without the shim |
| 85 | Default Fedora Workstation connects WG-with-DNS without openresolv | Fedora 39+ default install (resolved is the default since F33), openresolv NOT installed (dnf list installed openresolv empty). Same WG profile + connect as #84. Pass: same observations as #84. | Confirms the path works across the two major resolved-shipping distros |
| 86 | Multi-tunnel resolved policy has exactly one catch-all | Connect wg-full (DNS = 1.1.1.1) as primary and wg-split (DNS = 1.0.0.1) as secondary. Pass: only wg-full has ~.. A secondary without a search domain has no resolver registration; add corp.example to verify the secondary is registered only as ~corp.example. In both cases its CIDR route still works. | R3/R14 acceptance needs real resolved routing and resolver selection |
| 87 | DNS apply/read-back failure leaves routing up but truth degraded | Stop or block resolvectl after the tunnel interface appears and before policy apply. Pass: the tunnel remains Connected and routes packets, but logs/effective state report DNS Degraded; no surface claims verified DNS protection. Restore resolved and reconcile again; the same desired generation converges. | Failure-window timing and real DBus behavior cannot be simulated by unit tests |
| 88 | Coordinator cleanup explicitly reverts only Vortix-owned links | Capture resolvectl status with a Vortix primary plus an unrelated host link. Disconnect the primary. Pass: the Vortix link is reverted/absent, the unrelated link is byte-identical, and a second disconnect/reconcile is a no-op. | Ownership and idempotency require a real resolved instance |
| 89 | Identity collapses to single Real IP / Exit IP rows when host has NO IPv6 | Host has NO IPv6 (curl -6 -s --max-time 5 https://ifconfig.co/ip times out). Disconnect any VPN; open TUI. Identity section shows Real IP / Exit IP labels (NOT Real IPv4 / Real IPv6). No Real IPv6 or Exit IPv6 rows render. #227 acceptance: no v6 connectivity → labels stay v4-only-friendly. | TUI rendering — needs v6-less host |
| 90 | Real IPv6 populates from disk cache on launch-with-VPN-up | Sequence: (a) launch vortix with no VPN; wait for telemetry to write ~/.config/vortix/real-ipv6.cache. (b) vortix up wg-full (or any tunnel that doesn't carry v6). (c) Quit vortix. (d) Relaunch vortix WHILE the VPN is still up. Pass: Identity section renders explicit Real IPv4 / Real IPv6 / Exit IPv4 / Exit IPv6 rows immediately; Real IPv6 shows the cached value (not checking…). Validates the parallel-to-v4 disk-cache fix. | Real-fs cache survival across vortix restarts |
| 91 | Exit IPv6 reads ✓ on a tunnel that actually carries v6 | Run scripts/vpn-lab.sh up and use wg11 (dual-stack client + server and IPv6 masquerade). Host has IPv6. Record REAL_V6 pre-connect via curl -6 ifconfig.co/ip. Connect wg11. Re-run curl -6 ifconfig.co/ip. Pass: returns a DIFFERENT v6 (server's DO-assigned v6); TUI Exit IPv6 row reads the new address with green ✓ (no alarm sub-line). #227 acceptance — confirms ground-truth IP comparison, not AllowedIPs introspection. | TUI rendering — needs the ephemeral dual-stack lab |
| 92 | Exit IPv6 reads ✗ leaking when public_ipv6 == real_ipv6 while VPN up | Host has IPv6. Record REAL_V6 pre-connect. Connect any v4-only WG/OpenVPN profile (wg-full, wg-split, ovpn-cert, etc.). Re-run curl -6 ifconfig.co/ip — still returns REAL_V6 because tunnel doesn't carry v6. TUI Exit IPv6 row reads REAL_V6 with red ✗ + sub-line v6 exposed — matches real IPv6. Banner demotes to PARTIAL. This is the reporter's literal scenario from #227. | TUI rendering — real v6 leak; any v4-only tunnel works |
| 93 | Exit IPv4 / Exit IPv6 both render split-route — no exit in split-only topology | Host has IPv6. Connect wg-split (AllowedIPs 10.8.0.0/24 only — no default route, no ::/0). Pass: both Exit IPv4 and Exit IPv6 rows read split-route — no exit with ─ sigil. No ✗ alarm on v6 (split-only ≠ leak — user didn't ask for protection). Verdict banner reads PARTIAL. | TUI rendering — split-only consistency |
| 94 | v6 rows flip correctly across connect → disconnect cycles | Host has IPv6. Connect wg-v6: Exit IPv6 ✓. vortix down wg-v6 → telemetry re-probes; Exit IPv6 should now match Real IPv6 (both = your real v6, ✓ since no expectation of protection). vortix up wg-v6 again → Exit IPv6 returns to the masked value within ~1 telemetry tick. No stale-state hangover. | TUI rendering — state transitions over time |
| 95 | Real IPv6 reads checking… when launched mid-connection without cache | Sequence: (a) delete ~/.config/vortix/real-ipv6.cache if present. (b) Connect a v4-only WG profile via vortix up wg-full (outside the TUI, via CLI). (c) Launch vortix TUI fresh. Pass: Real IPv6 row reads checking… with ─ sigil (we can't safely cache while VPN is up unless no tunnel carries v6 per AllowedIPs introspection). After disconnect, the row populates and the cache file appears. | TUI rendering — pending-state UX |
| 96 | DNS row reads ✓ Protected when configured resolver = answering recursor | Connect any tunnel that pushes a public-resolver DNS (wg-full pushes DNS = 1.1.1.1). TUI DNS row reads 1.1.1.1 · Cloudflare ✓ (no sub-line). Verify with dig +short txt o-o.myaddr.l.google.com on the host — the returned IP should be a Cloudflare anycast (1.1.1.0/24, 1.0.0.0/24, 2606:4700::/32, or 2400:cb00::/32). Vortix's recursor-IP probe runs the same query and classifies it as same-provider. | Recursor-IP probe baseline (recursor matches configured DNS provider) |
| 97 | DNS row reads ✗ leaking when recursor is a different provider | Run scripts/vpn-lab.sh up and use wg14 — the profile requests DNS = 9.9.9.9 (Quad9), while the lab's source-scoped nft rule redirects its DNS traffic to OpenDNS. Connect, wait one telemetry tick. Pass: DNS row reads 9.9.9.9 · Quad9 ✗ + a sub-line identifying the different answering recursor. Verdict banner demotes to PARTIAL. | Recursor-IP probe positive — intentional negative profile on the ephemeral lab |
| 98 | DNS row stays ✓ when configured = v4 anycast and recursor returns same provider's v6 | Connect wg-v6 (full-tunnel dual-stack, pushes DNS = 1.1.1.1, 2606:4700:4700::1111). On many setups Google's auth server sees Cloudflare's recursor via its v6 backbone — the TXT response carries a Cloudflare v6 anycast IP (e.g. 2400:cb00:71:1024::6816:7bd5). Pass: DNS row stays ✓; the v4-configured + v6-recursor cross-family match works because the provider table covers both Cloudflare's v4 anycast (1.1.1.0/24) and v6 anycast (2606:4700::/32, 2400:cb00::/32, 2803:f800::/32). Regression guard against the false-positive we hit during testing. | Provider-table v6 coverage regression |
| 99 | DNS row reads Unknown (✓ no alarm) when no primary tunnel owns default route | Topology: no tunnel up, OR only split-only tunnels (no primary). Pass: DNS row sigil is ✓ green, no sub-line. The leak probe never runs because there's no expectation of DNS protection without a primary tunnel (mirrors v6's split-only handling). Disconnecting all tunnels should also clear any stale Leaking state within one tick. | UX safety — no false alarms in EXPOSED / split-only |
| 100 | DNS row reads ✓ ProbeFailed when the recursor probe times out | Sequence: (a) Connect any primary tunnel. (b) Block outbound UDP/53 to break the probe (sudo pfctl -e -f /etc/pf-test.conf with a block out proto udp to any port 53 rule on macOS, or iptables -A OUTPUT -p udp --dport 53 -j DROP on Linux). (c) Wait for next telemetry tick. Pass: DNS row sigil is ─ (NotApplicable, gray); no alarm sub-line. The 3s UDP timeout returns ProbeFailed, which renders informationally — no false alarm when the probe can't reach Google's authoritative server. | Probe failure-mode safety |
| 101 | IPv6-disabled host refuses a v6-Address WG profile pre-flight (#242) | Linux only. (a) sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1 net.ipv6.conf.default.disable_ipv6=1. (b) Run scripts/vpn-lab.sh up, import wg11, and connect via TUI Enter AND vortix up wg11. Pass: both surfaces refuse before WireGuard starts — Missing: host IPv6 (kernel disabled) + the sysctl/edit-profile hint; no RTNETLINK stderr in the event log. (c) Re-enable IPv6; the same profile now connects normally. | Cross-platform: needs a Linux host with sysctl access; gate is #[cfg(linux)] |
| 102 | Pre-cutover CLI/TUI control-contract baseline | With real WireGuard and OpenVPN profiles, exercise up, down, reconnect, status, kill-switch, profile actions, audit, timeout, and one interactive credential flow in human/JSON/quiet/watch modes. Repeat with one tunnel, two tunnels, split-only/no-primary, and a conflicting default route. Pass: CLI and TUI agree on outcome, primary, route role, vocabulary, and semantic exit code; included split destinations route through the tunnel and excluded destinations do not. | Feature wiring gap: real kernel, protocol processes, terminal, and credentials cannot be exercised by fixture tests |
| 103 | macOS PF apply, refresh, and release preserve unrelated policy | Start with PF enabled and add a harmless unrelated PF anchor/rule. Engage vpn-only with two utun tunnels (one IPv6 endpoint), refresh after disconnecting one tunnel, then release. sudo pfctl -a com.apple/vortix.killswitch -sr changes atomically and becomes empty on release; the unrelated anchor/rule remains and PF stays enabled. | Cross-platform: real macOS PF kernel |
| 104 | macOS PF read-back failure degrades protection truth | Place a test pfctl wrapper ahead of PATH that succeeds loading the Vortix anchor but fails -a com.apple/vortix.killswitch -sr; engage vpn-only. Pass: the persisted effective state is Degraded, no CLI/TUI/JSON surface reports Blocking/Protected, and the prior anchor remains fail-closed. | Fault injection: real privileged PF subprocess |
| 105 | Firewall drift invalidates protection within five seconds | Engage vpn-only, then remove one Vortix interface allow rule out-of-band with nft or PF on the native OS. Pass: current Standard-mode surfaces cease reporting Blocking/Protected on their next policy synchronization; after continuous reconciliation lands in U6, repeat and require automatic invalidation within five seconds without a desired-generation change. | Cross-platform drift signal/freshness; U6 completes continuous observation |
| 106 | Concurrent first nft enable converges to one verified policy | On an nft-only Linux host with no vortix_killswitch table, launch two vortix killswitch vpn-only commands simultaneously while the active tunnel set changes between them. Pass: at least the final command succeeds, the owned table exactly matches its active-tunnel policy, and neither command reports protected without matching read-back. | Real-process race timing and privileged nft state are not deterministic in unit tests |
| 107 | Cross-protocol DNS primary transfer is complete and gap-safe | Connect WG primary + OVPN secondary, then transfer the default route to OVPN without disconnecting WG. Pass: the new full policy is applied, only OVPN owns catch-all DNS, WG DNS is suppressed/scoped, both tunnel routes still work, and only prior-generation Vortix resources are released. Repeat in reverse order. | Real route-election and resolver-cache timing on Linux/macOS |
| 108 | Linux resolvconf fallback is owned and idempotent | On Linux without systemd-resolved but with openresolv, connect a primary with DNS. Pass: resolvconf -l contains only a vortix.<iface> record; repeated reconcile is unchanged; disconnect removes that record and preserves every foreign record. | Requires a non-resolved distro/VM and privileged resolver mutation |
| 109 | macOS primary DNS follows the active SystemConfiguration service and restores exactly | On macOS, record the active primary service and its complete Setup:/Network/Service/<uuid>/DNS dictionary, then connect a primary tunnel that requests different DNS. Pass: the active service uses the tunnel DNS, the Vortix owner/backup keys are present, ordinary public names resolve through it, and no /etc/resolver/default is created. Transfer primary and disconnect twice; only the matching generation restores the byte-equivalent original dictionary and removes the owner/backup. Change the service DNS externally before disconnect and confirm Vortix reports degraded without overwriting the external value. | Real macOS SystemConfiguration mutation, service changes, cache flush, and crash-safe restoration |
| 110 | macOS scoped secondary resolver does not become global | Primary has public DNS; secondary requests corp.example. Pass: the active primary service points to primary DNS, /etc/resolver/corp.example points to secondary, public names use primary, corp names use secondary, and the secondary CIDR route remains installed after either resolver update. | Requires two reachable real resolvers and macOS resolver cache observation |
| 111 | OpenVPN restart with unavailable DNS evidence stays conservative | Connect an OpenVPN profile whose DNS comes only from PUSH_REPLY, terminate Vortix without disconnecting, then remove/truncate its runtime log and restart while the daemon remains active. Repeat with a different PID/interface. Pass: the persisted request is retained only as degraded diagnostic evidence, no resolver mutation or duplicate resource is attempted for either observation-only session, and neither case reports fresh Applied DNS. | Crash timing, log rotation, and live cross-process identity are not deterministic in unit tests; trusted identity-qualified adoption is deferred to the executor ledger |
| 112 | Concurrent CLI and TUI DNS reconciliation is serialized and live | Keep the TUI open with one managed tunnel, then run a CLI up/down that changes DNS while forcing the resolver command to pause briefly. Pass: the TUI remains responsive, converges to the latest complete policy after the CLI releases dns-policy.lock, and no partial/interleaved resolver state is visible. | Real-process lock contention, privileged resolver mutation, and terminal responsiveness |
| 113 | Default-route probe failure preserves the last known DNS topology | With a primary tunnel active, make only the route-probe command fail or time out for several scanner ticks, then restore it. Pass: the TUI signals stale route observation, retains the prior primary and DNS policy without mutation during failure, and converges after the next successful probe. | Fault injection: platform route subprocess/backoff and real TUI signaling |
| 114 | WireGuard teardown never replays profile DNS | Import a WireGuard profile from outside /etc/wireguard with DNS = ..., then exercise normal disconnect, reconnect, forced disconnect, and a disconnect after restarting Vortix while the interface remains up. Trace wg-quick/resolver commands. Pass: every down uses a DNS-free managed config (or an interface-only adopted handle), the source profile is unchanged, resolver state changes only through the Vortix DNS policy adapter, and lifecycle temp files disappear after successful teardown. | Real wg-quick config lookup, hooks, restart adoption, and resolver subprocess tracing |
| 115 | Legacy profile identity backfill is one-time and rollback-readable | Copy a real pre-sidecar WireGuard and OpenVPN profile (including saved OpenVPN auth), plus a config-less .meta.toml left by a v0.4.2 delete. Start Vortix, record .vortix-profile-inventory-v1.toml, the active sidecars, and .vortix-legacy-sidecars-v1/, then restart twice. Pass: one opaque ID is assigned to each active profile, IDs and associations remain byte-stable, configs/auth are unchanged, the stale sidecar is preserved only in the archive, and an older binary can still read the original configs. | Real-fs migration and rollback compatibility |
| 116 | Malformed or duplicate profile identity fails before mutation | Duplicate a sidecar ID, then separately corrupt an ID and add/remove a config after the migration inventory is saved. Attempt up from CLI and TUI. Pass: startup refuses with an actionable profile-identity error and no VPN process, route, firewall, or DNS mutation occurs. | Real process/platform mutation boundary |
| 117 | Profile rename preserves all associations | Save OpenVPN credentials, metadata and boot intent for a disconnected profile, rename it in the TUI, restart, then connect and disconnect it. Pass: display name and config path change, the sidecar ID and ID-keyed auth path do not, metadata/boot/desired associations still target the same ID, and no old-name files remain. | Real-fs multi-file transaction and auth integration |
| 118 | Profile rename resumes after every durable boundary | For each rename intent stage, terminate Vortix after the config move, sidecar move/update, legacy-auth compatibility migration, metadata update, and inventory update; restart after each fault. Pass: recovery completes one readable profile with one ID, never exposes both names, and never loses credentials or boot intent. | Fault injection: process kill between fsync/rename boundaries |
| 119 | External editor rename is observation-only | While a profile is disconnected and while it is active, save it through an editor that uses temp-file rename, then permanently move/delete it. Pass: transient replacement retains the existing ID, an active tunnel remains attributed to that ID, and stable disappearance surfaces ProfileMissing without generating a sidecar or new ID. | Real editor filesystem events and debounce timing |
| 120 | Profile import and delete recover after every durable boundary | Kill Vortix after each config, sidecar, auth/metadata and inventory boundary during separate imports and deletes, then restart. Pass: an import converges to one complete profile and a delete converges to no profile or association; no orphan config, sidecar, credential or inventory entry remains. | Fault injection: process kill and real filesystem durability |
| 121 | Tampered transaction journals cannot escape the profile root | Replace each persisted inventory/rename/import/delete basename and stage in turn with absolute paths, .., symlinks and unknown values, then restart. Pass: Vortix reports invalid transaction data before mutation and every outside sentinel remains byte-identical. | Adversarial filesystem race and journal tampering |
| 122 | Profile mutation lock contention is bounded | Hold the profile lock from one process while attempting import, rename, touch and delete from another CLI and the TUI. Pass: each second caller returns a typed busy/timeout error within the documented bound, the TUI remains responsive, and retry after lock release succeeds. | Real cross-process flock contention and UI responsiveness |
| 123 | CLI profile mutation cannot race tunnel lifecycle | Start vortix rename and vortix delete --yes concurrently with CLI/TUI connect attempts for the same profile. Pass: the shared lifecycle authority serializes each mutation, an active or in-flight profile is refused at the final state check, and sidecar/inventory remain readable after every accepted mutation. | Real-process lifecycle/profile-lock contention and kernel-state observation |
| 124 | Delayed retry follows identity through rename and reorder | Schedule an automatic retry with a long delay, then rename the target and change profile sort order before the retry fires. Pass: Vortix reconnects the renamed target, never the row that inherited its old index, and the activity log uses the current display name. | Real timer delivery plus TUI catalog mutation |
| 125 | Supervised multi-profile connects keep the TUI responsive | Start two slow real connects for distinct profiles while continuously navigating the TUI and requesting status from another client. Pass: both protocol launches overlap, snapshots remain responsive, and each profile converges independently. | Real subprocess latency, terminal responsiveness, and cross-client scheduling |
| 126 | Scanner observations never seize lifecycle authority | Start an external WireGuard/OpenVPN session matching a catalog profile while another profile is connecting. Pass: the external session is visible but dim/non-primary, never auto-retries, never changes the in-flight protocol interface, and Vortix does not mutate its DNS/routes. | Real kernel/process attribution and scanner races |
| 127 | Disconnect tombstone contains late scanner ticks | Disconnect a managed tunnel while delaying kernel interface removal so two positive scanner ticks arrive after teardown dispatch. Pass: it is never re-adopted; after absence is observed, a genuinely external restart becomes visible as read-only. | Real teardown/scanner timing |
| 128 | OpenVPN foreground custody leaves no process tree | Connect a real OpenVPN profile in one-shot Standard mode, then test normal disconnect, startup failure, forced timeout, and abrupt Vortix termination. Pass: connect returns only after the custody receipt, normal/forced cleanup reaps the child and descendants, and no openvpn or custodian process remains after containment. | Real process groups, signals, privilege boundary, and zombie inspection |
| 129 | Policy barrier failures never publish mixed protection | For each blocking, tunnel, route, DNS, observation, and publication boundary, inject one real backend failure during connect, disconnect, drop, and primary transfer. Pass: blocking precedes exposure, owned prior-generation resources compensate in reverse order, and no snapshot reports Protected for a mixed generation. | Privileged cross-backend fault injection and packet leak observation |
| 130 | One-shot OpenVPN custody survives its initiating CLI | Run vortix up <openvpn-profile> from a short-lived shell, close that shell after success, and inspect the private custodian runtime directory and process tree from another terminal. Restart Vortix while the tunnel remains active and confirm the scanner reports the OpenVPN child PID, which differs from its guardian PID. Pass: the foreground OpenVPN process remains live under exactly one tunnel-scoped custodian, restart authenticates the child as a non-leader member of that custodian's process group, vortix status finds it by stable profile receipt, and vortix down authenticates the exact token, reaps the complete process group, and removes its socket, receipt, PID, log, and managed config. | Real process ancestry, cross-process IPC, privilege boundary, and filesystem permissions |
| 131 | Custodian failures and stale capabilities fail closed | Exercise EOF and SIGTERM after READY but before COMMIT, SIGKILL of a committed custodian owning a TERM-resistant leader plus descendant, a dropped status response, a nonexistent OpenVPN executable, natural child exit, and an old receipt retained across a reconnect attempt on both Linux and macOS. Pass: the guardian removes the complete process group after custodian death; handoff failures leave no group; a response write failure does not stop a healthy tunnel; forced stop confirms exact receipt, socket, and group absence; natural exit frees the profile; and an old/wrong token cannot query or stop the newer child. Do not treat this as a machine-power-loss guarantee. | Fault injection: signals, process groups, PID reuse window, and authenticated Unix socket IPC |
| 132 | OpenVPN foreground config cannot escape custody | Import profiles containing active/mixed-case daemon, recursive config/include, plugins, every supported script hook (up, down, route/address/auth/TLS hooks), local DNS directives, and commented forbidden text. Pass: every executable or recursive directive is rejected before runtime side effects or spawn, comments remain harmless, accepted launches use a private 0600 managed config with stable profile/ownership markers, DNS directives are absent from that copy, and the source profile is byte-identical. | Real OpenVPN option parsing, managed-file permissions, and recovery identity inspection |
| 133 | WireGuard valid and unreachable peers agree on every surface | On real Linux and macOS, connect one valid peer and one unreachable peer while watching CLI JSON, the TUI at 80x24, journal, and a second subscriber. Pass: all show Handshaking until the same current-generation timestamp appears; unreachable times out and never shows Connected/Protected. | Real peer, terminal, and cross-client evidence |
| 134 | WireGuard timeout cleanup preserves blocking and owns only its attempt | Enable vpn-only, force handshake timeout, cancellation, client crash, and helper reply loss immediately after interface creation. Pass: the attempt interface and policy are removed after observed absence, unrelated tunnels remain, blocking never opens, and retry creates no duplicate. | Real firewall/kernel fault injection |
| 135 | WireGuard per-peer stale health recovers without false disconnect | Use a multi-peer profile with disjoint AllowedIPs. Leave one peer idle without keepalive, then generate routed traffic on another, age its handshake past three minutes, and restore it. Pass: only the expected peer/routes gain the ! stale signal, the idle peer stays informational, and fresh evidence clears the signal. | Real multi-peer timing and TUI density |
| 136 | macOS WireGuard utun mapping is not connectivity proof | On supported macOS, connect valid and unreachable profiles and inspect /var/run/wireguard/*.name plus the TUI. Pass: creating utunN alone remains Handshaking and never becomes primary; only fresh typed peer evidence connects; the first missing handshake explains that Vortix is retrying once; and an exhausted retry removes the owned utun/config, returns the profile to Disconnected, and does not touch another tunnel. Restart Vortix with the valid owned tunnel still active and confirm its authenticated recovered handshake restores Connected. | Cross-platform: real macOS WireGuard |
| 137 | Scanner-only WireGuard stays observed and unmanaged | Bring up a WireGuard interface outside vortix, including one with an old or fresh handshake and non-zero cumulative counters, then open the TUI and run human, brief, watch, and JSON status. Pass: every surface says Handshaking, JSON has no primary, vortix does not automatically time out or tear down the external interface, and removing it clears the observation. | Real kernel ownership boundary and cross-process observation |
| 138 | Per-peer activity expectation never borrows health | Use two peers with disjoint routes: generate a recent byte delta or an actual vortix probe for one, leave the other idle, then enable keepalive on the never-seen peer. Pass: idle is informational, the expected never-seen peer is degraded with its own routes, and a healthy peer never makes the aggregate healthy. | Real counter timing, keepalive, and route attribution |
| 139 | Engine settings compatibility bridge reaches real WireGuard factory | Set legacy config.toml WireGuard timeout/targets, verify them, then override each through [engine] in settings.toml and VORTIX_ENGINE__*. Pass: old partial files retain their values, explicit new settings/env win, and timeout/probe behavior changes identically in CLI, TUI, and daemon paths. | Layered filesystem/env configuration and real factory behavior |
| 140 | Standard-mode WireGuard receipt survives the initiating client | Connect a real WireGuard profile from a one-shot CLI, let that process exit, then run human, brief, quiet, watch, and JSON status from another shell before and after aging/recovering the peer handshake. Pass: the stable profile/interface/generation/peer receipt restores Connected, every surface agrees on Healthy → Degraded → Healthy (quiet is non-zero only while degraded), and removing the interface clears the receipt only after kernel absence. Bring up the same profile externally without a valid receipt and verify every surface remains read-only Handshaking. | Cross-process atomic receipt, real kernel absence, typed health/journal/subscriber parity |
| 141 | Authoritative config directory selects every settings layer | Place conflicting settings.toml files in the default directory and two temporary directories. Run once with --config-dir, once with VORTIX_CONFIG_DIR, and once under sudo-user resolution while retaining an old partial config.toml. Pass: only ${resolved_config_dir}/settings.toml is read, environment engine overrides still win, and legacy fields remain compatibility defaults without default-path drift. | Real CLI parsing, environment layering, sudo-user home resolution, old-file compatibility |
| 142 | Standard-mode hooks run only as the invoking owner | As a non-root user configure a connected hook that records id -u, then connect once normally and once through sudo vortix; also invoke Vortix as direct root without sudo provenance. Pass: the first two records contain the same non-root UID, inherited supplementary root groups are absent, and direct root refuses the hook without changing tunnel success. | Real sudo credentials, filesystem ownership, and process credentials |
| 143 | Hook timeout and cancellation leave no descendant process tree | Configure a hook executable that starts a TERM-resistant descendant and waits. Exercise timeout, Vortix cancellation, and normal parent exit while observing its process group. Pass: lifecycle state completes independently, bounded diagnostics report one attempt, and no hook descendant remains. | Real process groups, signals, and zombie inspection on Linux/macOS |
| 144 | Executable protocol directives never reach privileged binaries | Import WireGuard profiles containing each of PreUp, PostUp, PreDown, and PostDown, plus OpenVPN profiles containing script/plugin directives. Attempt connect and teardown while tracing protocol argv. Pass: validation gives lifecycle-hook migration guidance before any wg-quick/OpenVPN/helper execution and no profile command runs as root. | Privileged protocol tracing and real profile migration |
| 145 | Same-boot control restart scans before resuming durable intent | In Background mode connect two real tunnels, terminate the control service after dispatch and before a reply, then restart it without rebooting. Pass: CLI/TUI mutations report starting/not-ready until the first complete scan, no duplicate protocol child is spawned, ambiguous sessions remain read-only, teardown tombstones are honored, and prior desired generations resume only after observation. | Real service manager, kernel sessions, and crash timing |
| 146 | Passive daemon stays mutation-free under concurrent clients | Run the passive candidate beside active WireGuard and OpenVPN sessions, attach two TUI/status subscribers plus repeated CLI snapshots, and attempt a raw execute frame after a valid handshake. Pass: reads stay live and generation-consistent, execute is rejected as a missing capability, and audit traces show no authority lock, control-state write, retry, protocol, firewall, DNS, or route mutation. | Real concurrent clients plus kernel/process side-effect audit |
| 147 | Normal package staging starts no Vortix service | Install each supported Linux/macOS package into a clean VM without opting into setup. Pass: daemon/helper artifacts and disabled templates may exist, but no Vortix process, socket, lease, enrollment marker, firewall/DNS/route mutation, or boot job is active. | Real package manager and service-manager state |
| 148 | Staged helper refuses every operational entrypoint | As root and non-root invoke the installed helper with --serve, execute, install, malformed arguments, and a valid-looking framed request. Pass: every invocation exits 78 without creating a socket, ledger, child, policy, or service-manager change; --version alone succeeds and says staged/unenrolled. | Installed root-owned artifact plus process/filesystem audit |
| 149 | Package identity and service instance substitution fail closed | In disposable Linux/macOS VMs replace each daemon/helper/bootstrap artifact, manifest, service definition, PID, start token, manager nonce, containment, owner UID, and socket path one at a time. Pass: preflight or handshake rejects before authority/lease creation; supported packages recover transactionally and unsupported channels give Standard-mode secure-package guidance. | Root-owned package layouts, systemd/launchd identity, signature verification |
| 150 | Helper loss never turns a missing reply into a blind retry | In an enrolled disposable VM, interrupt the helper before request delivery, after durable admission, after observation/effect, and before its reply. Pass: pre-delivery loss reports unavailable; every post-delivery loss reports ambiguous/reconciling and performs a fresh authenticated handshake plus exact resource observation; a pending WireGuard interface is claimed or cleared only from exact present/absent evidence, while pending OpenVPN requires the tunnel, process group, and matching containment identity together; scanner/PID evidence alone never repeats an operation or mints ownership. | Real IPC fault injection, root ledger durability, process containment, service-manager restart |
| 151 | Helper lifecycle owns and reaps exactly one admitted tunnel | In enrolled Linux and macOS test VMs, duplicate WireGuard and OpenVPN framed starts before and after their replies, attempt a stop for a foreign profile/generation, return mismatched child identity from a fault-injected executor, then stop each admitted tunnel. Pass: WireGuard leaves no setup process and owns only its exact interface; OpenVPN owns one foreground process group; foreign/mismatched resources never become owned; mismatched children are contained; foreign stop never reaches the platform executor; success follows exact interface absence and any required reap. | Real process containment, duplicate IPC delivery, and adversarial helper-executor fault injection |
| 152 | Helper policy barriers remain fail-closed across crashes | In an enrolled network namespace or macOS VM, interrupt the helper before and after each blocking, route, DNS, firewall, observation, and obsolete-release ledger boundary, including one mixed firewall/DNS/route release. Pass: no later phase runs from an unobserved predecessor; confirmation-persistence failure poisons the session; current blocking remains intact; every family verifies only its own retained and obsolete resources; a still-live obsolete-only route prevents release; and restart scans exact generation-owned resources before reconciliation. | Real firewall/DNS/route effects, durable ledger fault injection, and helper restart |
| 153 | Helper cleanup removes only ledger-owned resources after proving absence | In enrolled Linux and macOS test VMs, request cleanup for exact owned tunnel/process-group resources, mixed owned/foreign resources, a forged runtime-secret tag, and duplicate delivery before/after reply; fault-inject a lost reply or later-action failure after the first resource is already removed, then retry. Pass: any foreign or untracked member rejects the whole request before platform execution; duplicates execute once; retry accepts an already-absent resource only when its fixed helper runtime is absent or clean, refuses unsafe leftovers, and completes the remaining actions; mismatched/present read-back remains ambiguous and retains ownership; exact absence removes only the requested ownership records. | Real kernel/process/resource ledger correlation, adversarial cleanup requests, duplicate IPC delivery and partial-batch retry |
| 154 | Root helper replay ledger survives crashes without accepting path substitution | On clean Linux and macOS package installs, interrupt helper checkpoint writes before data fsync, before rename, and before parent-directory fsync, including each pending_effect → owned and pending_release → absent transition; then try symlink, hard-link, mode, owner, oversized, malformed, and future-schema substitutions at the fixed ledger path. Pass: restart sees either the complete prior or complete new replay-plus-ownership checkpoint, never resets replay state or loses the exact reconciliation set, contains any OpenVPN child whose identity could not be recorded, and refuses every unsafe or invalid existing ledger before privileged execution or replacement. | Real root ownership, package-created directories, process containment, filesystem crash/fault injection, APFS/ext4 durability |
| 155 | Standard-mode OpenVPN clears inherited provider and loader environment before privileged launch | On supported Linux and macOS, set OPENSSL_CONF, OPENSSL_MODULES, OPENSSL_ENGINES, and the applicable LD_*/DYLD_* loader variables; attempt Standard-mode connects with active engine, providers, or pkcs11-providers, and with setenv opt or quoted executable directives. Pass: every malicious profile is rejected before root spawn; a safe authenticated profile still connects, installs its expected routes, and its privileged child has none of those inherited variables. | Real sudo boundary, OpenVPN/loader behavior, and cross-platform process-environment inspection |
| 156 | Legacy migration stays inside the invoking user's profile directory and ignores runtime configs | In a disposable macOS/Linux account with a v0.4.2 config-less sidecar, run the upgrade through sudo vortix while a second process repeatedly swaps .vortix-legacy-sidecars-v1 with a symlink to an outside sentinel directory; repeat with a power cut after archive-directory creation and after source unlink. Then keep an OpenVPN tunnel active so its hidden .vortix-*.ovpn managed config remains beside the saved profile inventory and restart Vortix. Pass: no outside entry is created; restart resumes with the same inventory IDs and bytes; the final 0700 archive plus its contents are readable/removable by SUDO_USER; and the hidden runtime config neither receives profile identity nor triggers an inventory-change refusal, while a new visible .conf/.ovpn still fails closed. | Real sudo ownership, descriptor-relative filesystem race, active OpenVPN runtime material, APFS/ext4 durability |
| 157 | One-shot Standard-mode ownership survives CLI process exit without adopting foreign tunnels | On supported Linux and macOS, connect one WireGuard and one OpenVPN profile with sudo vortix up, let that process exit, then run a fresh read-only command and sudo vortix down for each. Pass: neither intentionally persistent tunnel is warned about as a possible orphan; the exact generation is removed; root-private ownership records disappear only after kernel/process absence; changing the profile config, boot scope, owner, interface, peer, receipt mode/owner, or substituting a symlink makes teardown fail closed; an externally created lookalike remains untouched and is still reported as a possible orphan. | Real root-owned runtime filesystem, kernel WireGuard evidence, OpenVPN custodian IPC, reboot boundary |
| 158 | Canonical one-shot CLI preserves contracts and durable timeout recovery | On Linux and macOS, exercise real WireGuard and OpenVPN up, down, reconnect, killswitch, and release-killswitch in human/JSON/quiet modes, including two simultaneous tunnels, split routes, an unreachable peer, and a configured owner-run lifecycle hook. Pass: only the canonical control service mutates protocol/policy state; outputs and exit categories retain their documented shape; a timeout prints an operation ID that vortix status --operation <ID> can query from a fresh process; the hook runs once as the invoking non-root owner; and a concurrently opened TUI or second CLI fails fast on the writer lock without interleaving effects. | Real kernel/protocol processes, cross-process lock, sudo credentials, and hook process lifetime |
| 159 | vpn-only recovery remains fail-closed across hostname reconnect and partial firewall failure | On Linux and macOS, connect a hostname-endpoint profile, enable vpn-only, disconnect, block ordinary DNS egress, and reconnect from a fresh CLI process; inspect the private managed WireGuard/OpenVPN config before launch, then fault-inject the firewall backend after it may have installed the pre-tunnel rules. With an unrelated external VPN active, run sudo vortix release-killswitch. Pass: reconnect accepts only an owner-authenticated hostname/port mapping bound to the exact profile digest, renders its cached IP and original port/family into the managed config, and opens no DNS exception; editing the profile or cache fails closed; partial pre-block failure either restores the exact prior Vortix policy or reports a persisted degraded fail-closed state; and release removes Vortix-owned blocking despite the external session without modifying that session. | Real firewall atomicity/read-back, DNS blocking, managed protocol config, root authentication, and external-session isolation |
| 160 | Canonical one-shot control stays responsive under slow scans and large split-route policy | On Linux and macOS, add scanner subprocess delay while connecting and disconnecting a real tunnel, then connect a profile with many split routes including overlapping claims that share an exact probe target. Pass: control/deadline progress continues at 50 ms while at most one full scan is in flight, a new scan starts within 250 ms of the prior start (or immediately after an overlong scan), terminal CLI shutdown remains bounded, only semantic presence/interface changes publish, equivalent probes run once per fresh route barrier, route work stops no later than one 1-second query after the operation deadline, and more than 256 distinct probes fail before any route query. | Real subprocess timing, policy-routing kernel truth, and deadline behavior |
| 161 | OpenVPN static challenge is service-owned, one-shot, and memory-only | On Linux and macOS, use a real static-challenge profile with saved username/password and run up, targeted reconnect, and reconnect-all. Answer one OTP normally; then try a second answer, a second CLI client, Ctrl-C, no input through the operation deadline, and process termination while the prompt is open. Replace the saved-auth file in turn with a symlink, wrong-owner file, loose-mode file, and oversized file. Pass: the admitted operation publishes exactly one client-bound challenge before OpenVPN spawns, only its originating client can answer once, management-socket auth succeeds without any .scrv1.auth file, every unsafe saved-auth artifact is rejected before privileged spawn, terminal/cancel/expiry leaves no secret file/socket/process or reconnect intent, reconnect prompts again, reboot does not auto-connect the interactive profile, and logs/snapshots/events/control-state contain no credential material. | Real terminal cancellation, OpenVPN management protocol, root-owned filesystem adversaries, process/file cleanup, and reboot boundary |
| 162 | Typed profile mutations preserve identity and serialize against lifecycle | On Linux and macOS, import one WireGuard and one OpenVPN profile in human/JSON/quiet modes, bulk-import a directory containing valid and invalid files, then race up against rename/delete from a second terminal and repeat while each tunnel is active. Interrupt the process at every FsProfileStore insert/rename/delete intent boundary and retry; also delay a committed mutation until just after its operation deadline. Pass: output and partial-success exit contracts are unchanged; raw configs and credentials never appear in control state/events; each successful rename keeps the same ProfileId plus auth/boot associations; active or racing mutations reject without orphaning desired state; delete removes only the stable-ID associations; restart recovers each transaction and the refreshed catalog admits exactly the committed profiles; a late commit remains expired but reports completed_after_deadline and explicitly warns that it must not be retried. | Real concurrent CLI processes, filesystem crash/deadline boundaries, auth/boot artifacts, and output capture |
| 163 | Read-only clients cannot delete a live WireGuard teardown capability | Start an unreachable WireGuard up with a short handshake timeout, then repeatedly run status and open/close the TUI from other terminals while the attempt is handshaking. Repeat a successful up followed by observers and a fresh-process down. Pass: every concurrent process has a distinct scratch session and lease; observers never unlink the active managed config; timeout cleanup removes the exact interface and returns the handshake failure promptly; successful cross-process teardown still removes only the owned interface. | Real multi-process advisory locks, wg-quick lifecycle, crash cleanup, and macOS/Linux filesystem semantics |
| 164 | Canonical TUI remains responsive and truthful across multi-tunnel control | On Linux and macOS, keep the TUI open at 80x24 while a second terminal issues CLI lifecycle and kill-switch commands for two real tunnels. In the TUI, confirm a default-route takeover, choose the exclusive-switch action, reconnect the last disconnected profile, submit and retry an OpenVPN challenge with Save credentials disabled, and rapidly cycle the kill switch while fault-injecting a slow control-state write. Pass: CLI and TUI converge on the same generation, primary, routes, health, and effective kill-switch state; the existing confirmation overlays appear before conflicting mutation; exclusive switch disconnects every non-target before connecting the target; reconnect-last targets the stable profile ID; challenge secrets never reach disk/log/snapshot; input and rendering remain responsive while durability is pending; and no legacy App scanner, retry, protocol, DNS, or firewall writer runs. | Real terminal event loop, durable-storage delay, protocol challenge, kernel route/firewall truth, and cross-process observation |
| 165 | Multi-WireGuard scan and unexpected-drop recovery are one coherent transition | Connect two managed WireGuard profiles, enable block-on-drop, trace wg invocations and canonical snapshot generations, then remove one interface externally and restart Vortix during the reconnect backoff. Pass: each scan invokes exactly one bounded wg show all dump; each interface's details stay attributed to its exact profile; all changed scan facts appear in one snapshot generation; blocking is verified before reconnect; and same-boot restart resumes the durable bounded recovery without an unblocked attempt or duplicate child. | Real kernel interfaces, subprocess tracing, firewall read-back, crash timing |
| 166 | Background diagnostics remain bounded, redacted, and advisory during failure | Run the passive daemon as one user, follow diagnostics from two clients, flood control/helper/queue events, disconnect the socket, replace the owner-readable fallback from another same-UID process, and fault-inject disk-full plus crashes before rename and directory fsync. Include profiles, endpoints, IP/DNS values, credentials, command arguments, paths, stderr, and helper payloads in the originating failures. Pass: live clients authenticate and resync after a visible sequence gap; no sensitive value appears; the ring stays within 512 records/1 MiB; fallback stays within 256 records/512 KiB at mode 0600 with no archive; interrupted writes retain the prior generation; disk failure emits only a typed degraded code without changing protection; foreign users cannot read it; and every fallback is visibly stale/unauthenticated and cannot claim authority, protection, enrollment, helper identity, or cleanup permission. | Real peer credentials, filesystem ownership/durability, broadcast lag, disk-full behavior, and privacy inspection |
| 167 | Preparatory remote adapters stay dormant without changing Standard mode | On Linux and macOS, start the passive candidate, keep a Standard-mode TUI and CLI lifecycle command active, then send raw control_open, command, profile-stage, challenge, and subscribe requests after a compatible handshake. Pass: every remote mutation/session request is rejected as a missing capability before any control authority, profile staging, process, firewall, DNS, route, or local fallback starts; the Standard-mode TUI/CLI retain their existing local control behavior and outputs; diagnostics/passive reads remain available; and no secret-bearing request appears in logs, diagnostics, fallback files, or replay state. | Real socket framing, process/kernel side-effect audit, and secret inspection |
| 168 | Prepared Background UX restores the terminal and never elevates an untrusted client | On packaged Linux and macOS builds at 80x24, keyboard through Setup, Status, Recover, Diagnostics, and Disable; cancel before confirmation, deny elevation, interrupt the trusted bootstrap with a signal, inject bootstrap/service failure, and retry. Include OpenVPN boot profiles using quoted/setenv opt credential directives plus external PEM and PKCS#12 material. Pass: one non-color header signal remains visible with no new panel; focus/scroll are deterministic; raw/alternate-screen modes restore on every path; system sudo names only the absolute verified package bootstrap with no shell/password collection; this preparatory release refuses activation nonzero and starts no privileged/service process; fallback diagnostics remain advisory; and every interactive or externally sourced boot credential fails closed before intent persistence. | Real terminal, package trust/ownership, sudo signal handling, service manager, and credential prompts |
| 169 | Helper-owned nftables policy is atomic, exact, and restart-safe | In an enrolled Linux VM where nftables is the selected helper backend, preserve an unrelated table, then exercise fresh vpn-only, two-tunnel replacement, helper crash before and after the durable effect marker, release, and deliberate drift of the Vortix table between prepare and effect. Pass: each apply is one fixed-table nft transaction; exact read-back matches ordered interface/CIDR/endpoint allowances and digest; drift fails before mutation; restart audits only the recorded nft backend and resumes without touching unrelated tables; release proves exact prior ownership and final absence; no iptables policy is claimed. | Real nft kernel transactions, helper crash boundaries, package binary ownership, and cross-process drift |
| 170 | Helper-owned macOS DNS is exact, foreign-safe, and restart-audited | In an enrolled macOS VM, preserve the primary service's complete DNS dictionary and foreign /private/etc/resolver entries, then apply catch-all and scoped generations, transfer primary, and restart the helper before/after the backup, service replacement, ownership marker, scoped resolver write, and release steps. Pass: catch-all follows OpenVPN's SystemConfiguration semantics with exact backup/read-back/restore; scoped files remain generation-owned and atomically installed through the pinned directory; every foreign value stays byte-identical; exact current/prior/absent recovery is accepted; partial, linked, oversized, wrong-owner, externally changed, or unexpected inventory fails closed without claiming DNS protection. | Real root ownership, SystemConfiguration/resolver selection, cache behavior, filesystem adversaries, and helper crash boundaries |
| 171 | Helper-owned Linux DNS preserves exact per-generation state across crashes | In enrolled disposable Linux VMs, run once with systemd-resolved and once with only resolvconf; preserve foreign per-link/record state, then apply catch-all and scoped generations, transfer primary, and crash the helper before and after each durable physical-DNS checkpoint and each backend command. Pass: the backend selected before the first effect never changes while prior ownership remains; resolved restores exact servers/domains/default-route per helper-derived generation interface; resolvconf refuses scoped or multi-link policy it cannot represent and restores exact prior record versus absence; every partial command sequence either verifies full rollback or remains visibly ambiguous/fail-closed; restart accepts only exact prior/target members and never changes unrelated resolver state. | Real resolved/resolvconf behavior, helper crash boundaries, package binary ownership, and kernel/service read-back |
| 172 | Helper-owned route cutover has one writer and exact restart truth | In enrolled disposable Linux and macOS VMs, connect full-tunnel and split-tunnel WireGuard/OpenVPN profiles, including multiple tunnels with distinct transport endpoints, explicit/OpenVPN net_gateway and remote_host gateways, metrics, multi-remote failover, and IPv4/IPv6 redirects; crash and restart the helper before and after each durable route checkpoint, then remove or redirect one owned route. Pass: WireGuard runs with Table = off; only the helper mutates its fixed Linux policy table/rules or macOS PROTO2 scoped routes; transport endpoint escapes are installed before route activation and removed last; net_gateway comes from the pre-tunnel main/default route; remote_host equals the actual remote selected by the latest completed OpenVPN session and is one of the authenticated endpoint allowances; exact gateway/interface/metric/endpoint ownership survives restart; foreign or drifted routes are untouched and reported ambiguous/fail-closed; unsupported IPv6 remote_host, DHCP gateway, bypass-dhcp, bypass-dns, and block-local semantics are rejected before any route effect. | Real Linux policy rules, macOS scoped routes, helper crash boundaries, OpenVPN gateway behavior and failover, and multi-tunnel endpoint exceptions |
| 173 | Preparatory authority lock fences Standard writers without enabling Background mode | On clean supported Linux and macOS package installs, stage the trusted bootstrap for one non-root owner, then race TUI startup, profile import, lifecycle commands, and a second bootstrap reserve while one writer holds the lock. Reboot and repeat before any enrollment commit; also replace the lock with a symlink, wrong owner, loose mode, or hard link using a privileged fault injector. Pass: the bootstrap installs the fixed lock in a root-controlled directory at owner-only read mode, every current client and bootstrap transition contends on the same inode, unsafe metadata fails closed without falling back to the legacy config lock, package absence retains Standard behavior, no daemon/helper mutation authority activates, and an unrelated local user cannot acquire the enrolled owner's lock. | Real package ownership, cross-process flock semantics, reboot, multi-user permissions, and filesystem adversaries |
| 174 | Helper policy inventory is exact across restart and schema downgrade | In an enrolled Linux and macOS VM with owned firewall, route, and DNS generations, restart the helper after a settled generation, after the durable pre-effect checkpoint, and while obsolete resources are pending release. Negotiate schema 6 from the enrolled daemon, then repeat with a schema-5 client. Pass: schema 6 reports only root-ledger resources from the authenticated authority epoch with exact current, predecessor, pending/owned state, and intended/effective digests; schema 5 omits policy_inventory from the wire shape; neither handshake mutates policy or promotes observation into ownership; a foreign-epoch, contradictory, oversized, or duplicate inventory is rejected before daemon reconciliation. | Real helper restart, authenticated IPC, root-ledger recovery, and N/N-1 wire compatibility |
| 175 | Live telemetry remains current without flooding the Event Log | Connect a busy WireGuard or OpenVPN tunnel, open the TUI Event Log at info level, and leave traffic running for at least two configured telemetry intervals. Pass: transfer/health, packet-loss, and jitter rows continue updating; routine packet-loss/jitter samples emit no per-sample lines, while each network-quality category transition emits one line; one public-IP probe runs promptly when the active egress interface, primary, routes, or connection presence changes; refresh nudges never overlap identity probes; IP, ISP, and location update as one sample; geolocation is cached against the exact public IP rather than queried every interval; an HTTP 429 is not retried and opens a provider cooldown while the location-capable fallback supplies metadata; an IP-only fallback for the same exit preserves known ISP/location; identical successful provider results add only one info line; a changed result, total probe failure, and subsequent recovery each produce a new line; leak warnings remain visible. | Real network timing, provider responses, live scanner statistics, and TUI log rendering |
| 176 | Last-connected activity is canonical, verified, and restart-stable | With profiles sorted by last used, connect a profile from the TUI while a second TUI or CLI client observes it, then restart Vortix; also attempt a refused and a failed connection. Pass: the profile panel updates only after verified connection success, all clients see the same stable-ID timestamp, selection remains on the same profile if sorting changes its row, restart restores the time, and refused/failed/adopted presence does not advance it. | Real cross-client publication, persistence, process restart, and TUI sorting |
| 177 | Large TUI directory imports respect bounded control backpressure | From the TUI, import a directory containing more than eight valid mixed WireGuard/OpenVPN profiles while continuing to navigate and render; include one invalid profile, inject slow durable filesystem writes, and repeat while an unrelated control command occupies capacity. Pass: the terminal remains responsive, exactly one batch mutation is in flight, every next valid profile receives a fresh deadline after its predecessor settles, valid profiles are eventually admitted exactly once in deterministic filename order, temporary queue saturation produces no profile-failure message, the invalid profile is reported once, and ordinary interactive commands remain bounded rather than opening an unbounded queue. | Real terminal scheduling, durable filesystem effects, mixed protocol parsing, and control-queue contention |
| 178 | Profile deletion waits for the complete disconnect transaction | Connect a real WireGuard and OpenVPN profile, request disconnect, and open Delete immediately after the protocol interface/process disappears but before final policy observation completes; repeat with a delayed and a failed final policy barrier. Pass: the profile remains visibly Disconnecting, Delete is refused locally without submitting a mutation, and it becomes deletable only after the disconnect operation reaches a terminal state; no another operation already owns this profile error is shown for a profile presented as disconnected. | Real protocol teardown timing, policy convergence, canonical snapshot publication, and TUI input ordering |
| 179 | Restart-restored teardown fences clear from exact absence | Interrupt Vortix after a real OpenVPN or WireGuard disconnect fence is persisted, restart while the tunnel is already absent, and then connect/disconnect another profile. Repeat with a persisted failed-teardown fence. Pass: the first complete scanner observation clears the exact restored fence, later operations reach terminal state promptly, and a live in-process teardown fence cannot clear before its worker completes. | Real process interruption, persisted tombstones, scanner absence, and worker timing |
| 180 | OpenVPN pushed DNS reaches the canonical final policy | Connect a full-tunnel OpenVPN profile whose DNS exists only in its completed PUSH_REPLY; confirm the OpenVPN child still has Vortix's DNS pull filters, inspect dns-policy.state, and resolve a public hostname. Pass: the state contains the pushed resolver assignment for the current tunnel, hostname traffic succeeds through the VPN, disconnect restores the prior resolver, and no protocol-side DNS mutation occurs. | Real OpenVPN push plus macOS/Linux DNS |
| 181 | Managed macOS encrypted DNS does not cause a false VPN refusal | On an MDM-managed Mac with a forced com.apple.dnsSettings.managed HTTPS/TLS resolver, connect full- and split-tunnel OpenVPN profiles that push DNS. Pass: managed DNS presence alone does not reject or disconnect the tunnel; Vortix still applies and verifies its owned DNS projection, ordinary uncached hostname resolution and HTTPS work through the intended route, disconnect restores the prior primary DNS configuration, and a genuine DNS write/read-back failure still rolls back with a neutral actionable toast. | Real macOS configuration profile, NetworkExtension resolver, route cutover, rollback, and TUI notification timing |
| 182 | Remembered OpenVPN credentials remain owner-bound across sudo upgrade | On macOS and Linux, use the previous release through sudo to remember credentials for one stable-ID profile, verify its .auth file is root-owned mode 0600, then start the new Standard-mode build as root for the compatibility handoff and subsequently run it normally as the configuration owner. Repeat with an owner-written legacy name-keyed file, then with symlink, hard-link, mode-0644, malformed, oversized, and wrong-owner fixtures. Pass: only the exact root-owned stable-ID record is transferred to the authenticated owner; owner-written and unambiguous legacy credentials reconnect; unsafe fixtures remain untouched and re-prompt; rename preserves the credential association; Auth Manager edit/clear targets the stable profile; OTP/challenge answers never appear on disk; an injected remember failure lets the current connection continue with a clear “not saved” warning; the final file and directory are owned by the configuration owner with modes 0600/0700. | Real root-to-user ownership transfer, sudo environment, macOS/Linux filesystem metadata, OpenVPN authentication, and TUI UX |
| 183 | Definitive OpenVPN authentication rejection exits Connecting | On macOS and Linux, connect a certificate-only profile whose server-side client authorization is denied, then a username/password profile with an incorrect password. Pass: each operation promptly reaches terminal authentication_failed, desired intent returns to disconnected without an automatic retry, no OpenVPN child or ownership fence remains, and the TUI shows one friendly authentication/authorization message. Correct the server/password and confirm the next attempt succeeds. | Real OpenVPN server rejection, process custody cleanup, durable operation transition, and TUI notification |
| 184 | Security Guard DNS verdict follows canonical policy and resolver routes | Connect full- and split-tunnel OpenVPN/WireGuard profiles through a private forwarding resolver and through a public resolver on macOS and Linux. Confirm the displayed address is the active primary profile's configured or authenticated OpenVPN-pushed DNS, not a stale pre-VPN resolver. For each displayed resolver, inspect the kernel route (route -n get <ip> on macOS; ip route get <ip> on Linux). Then create a more-specific physical-interface route for one resolver and reconnect. On macOS, also manually change the primary network service DNS while disconnected and while stale Vortix ownership metadata exists. Pass: ✓ appears only when exact current-generation resolver readback succeeds and every resolver route uses the owning VPN interface; the physical-route case fails safe and never claims protection; no-DNS and split-only profiles warn without claiming a leak; private-forwarder recursor egress is never compared with its configured address; and disconnect restores the exact pre-connection DNS baseline. | Real platform resolver mutation/readback, exact kernel routing, private recursive forwarding, external macOS DNS changes, recovery, and TUI rendering |
| 185 | Long-lived Security Guard evidence stays truthful and IPv6 probes terminate | Keep full- and split-tunnel OpenVPN/WireGuard profiles connected for at least one minute on macOS and Linux, including one IPv4-only VPN while the host has native IPv6. Pass: generation-bound tunnel/route/DNS/firewall audits renew verification without replaying any policy mutation; genuine readback drift degrades the verdict; repeated samples of a VPN exit IPv4 never produce an unchanged-IP warning unless that address matches the saved pre-VPN IPv4; and Exit IPv6 changes from checking… to the detected address or not detected after the bounded probe completes. | Real long-lived platform readback, mutation observation, IPv4-only VPN, and TUI timing |
| 186 | WireGuard interface names fail early and remain explicit | On macOS and Linux, import a working WireGuard profile whose basename exceeds 15 bytes, such as 07-wireguard-split-ip.conf. Pass: single-file TUI and batch/CLI import reject it with the 1–15 character contract and tell the user to rename the source file before retrying; the stored config, scanner identity, and wg-quick interface all use that one name. Also place an invalid legacy profile directly in the managed directory and try to connect it. Pass: no subprocess starts, the UI never remains in Handshaking, and one actionable terminal error explains how to re-import it. | Real wg-quick validation, import UX, legacy-profile guard, scanner identity, and teardown |
| 187 | macOS WireGuard policy rollback retains the wg-quick alias | Import a short-named split-tunnel profile such as wg07.conf, connect it on macOS, and force a final policy failure immediately after the first successful handshake. Pass: Vortix never classifies its newly persisted in-process tunnel as external while waiting for the next scanner sample; an unrelated/empty DNS policy does not fail; rollback runs wg-quick down with wg07.conf rather than the kernel-assigned utunN; /var/run/wireguard/wg07.name, the utunN socket, and owned routes disappear; the operation becomes terminal instead of remaining Disconnecting. Restart after the handshake but before rollback and repeat to cover durable ownership recovery. | Real scanner timing, Standard ownership, macOS alias mapping, policy failure, restart-safe teardown |
| 188 | TUI first frame is independent of control bootstrap latency | With 20+ mixed profiles, hostname endpoints, recovered durable state, and a deliberately slow initial scanner or endpoint lookup, launch the Standard TUI on macOS and Linux. Pass: the dashboard draws and navigation remains responsive immediately; lifecycle actions explain that the VPN service is still starting; the canonical snapshot attaches when bootstrap finishes; and bootstrap failure restores the terminal and reports the original error. | Real terminal latency, background startup, and control attachment |
| 189 | One-shot CLI lifecycle commands acknowledge bounded work immediately | On macOS and Linux, run human, --json, and --quiet forms of vortix up and vortix down against a real profile whose handshake or policy verification takes at least ten seconds. Pass: human mode immediately names the transition, target, verification work, deadline, and Ctrl-C behavior before waiting; success or failure remains the final line; JSON and quiet output gain no progress text; allowing the first invocation to finish does not require a retry. | Real terminal output timing, slow protocol/policy verification, signal cancellation, and structured-output cleanliness |
| 190 | Ephemeral compatibility lab is complete and leaves no paid resource behind | Run scripts/vpn-lab.sh self-test, then scripts/vpn-lab.sh up with a DigitalOcean account and SSH key. Pass: exactly one smallest-size droplet is created; all 15 named OpenVPN/WireGuard profiles plus credentials and README are downloaded into a private directory; status reports its ID and elapsed billable lifetime; representative full, split, dual-stack, static-challenge, multi-remote, multi-peer, DNS-negative, and FwMark profiles behave as described; a forced provisioning failure deletes the tagged droplet automatically; and scripts/vpn-lab.sh down destroys the successful lab while retaining local profiles. | Real cloud-init, billing lifecycle, public IPv4/IPv6, protocol daemons, generated secrets, and SSH/SCP |
| 191 | Cold one-shot CLI does not let startup policy overwrite its first lifecycle command | With no TUI or daemon running and a real profile disconnected, run sudo vortix up <profile> --timeout 60 once. Pass: the first invocation starts the protocol process and converges instead of timing out; sudo vortix down <profile> also converges. Repeat from a fresh process after interrupting a prior policy operation. | Real process startup and policy-result ordering |
| 192 | Late OpenVPN route conflicts stop once and return to the existing confirmation flow | Connect an OpenVPN profile that owns the default route, then connect a second profile whose conflicting default or overlapping route exists only in its server PUSH_REPLY. Pass: the candidate is compensated and returns to Disconnected, the first tunnel remains Connected, the existing default-route/route-overlap dialog names both profiles, cancelling causes no relaunch, and explicitly confirming admits exactly one retry with the exact conflict acknowledgement. | Real OpenVPN pushed routes, compensation, TUI confirmation, and kernel route ownership |
| 193 | macOS restart recovers protected DNS ownership before a new connection | Connect a DNS-owning profile on macOS, terminate Vortix after the protected owner/backup markers are written but before normal release, and restart with the user-writable DNS state missing or untrusted. Pass: startup validates the protected marker pair and actual resolver, restores the saved external DNS baseline, removes both markers, and then permits the next connection. If recovery is deliberately made inconsistent, the operation fails closed and the TUI explains the specific unfinished DNS state while the Event Log retains the exact platform error instead of reporting only a generic DNS failure. | Real macOS dynamic-store ownership, crash recovery, discarded user-writable authority, rollback, and TUI failure UX |
| 194 | Multi-profile disconnect reaches terminal truth without a restart | Connect two or more owned OpenVPN/WireGuard profiles, then disconnect one while leaving the others active. Repeat while a deliberately slow scanner sweep begins immediately before teardown. Pass: the selected profile leaves Disconnecting as soon as protocol-owned absence is proven, the remaining profiles stay connected, a late pre-teardown scan cannot restore stale presence, and closing/reopening the TUI is never required. | Real protocol teardown receipts, scanner timing, multi-tunnel policy, and TUI projection |
| 195 | Helper WireGuard sessions do not accumulate NetworkManager history | On a NetworkManager desktop, repeatedly connect and disconnect the same Vortix WireGuard profile, then repeat with a second profile and inspect both nmcli connection show and the desktop network-history UI. Pass: Vortix's generation-specific interfaces are runtime-unmanaged, no new stale external connection remains after each disconnect, and a pre-existing saved WireGuard connection with a similar name is unchanged. Repeat without NetworkManager running; the tunnel must still connect normally. | Real NetworkManager device discovery, desktop history, and saved-profile isolation |
| 196 | Live theme switching covers every built-in palette | Press the global palette key (p) through Synthwave, Terminal, Catppuccin Mocha, Dracula, Nord, Gruvbox Dark, and Tokyo Night; inspect the full TUI plus every overlay at 80×24. Test Terminal against both light- and dark-background terminal palettes and confirm Bulk Actions (b) contains profile-wide actions only. Repeat while Config Viewer has previously been opened, restart Vortix, and cycle back to Synthwave. Pass: every next frame changes immediately; one concise toast names the selected theme; status, selection, border, chart, toast, and overlay colors remain readable and semantically distinct; config.toml retains its comments and unrelated settings and records only the selected top-level theme; the selection survives restart; Terminal inherits terminal defaults; and a full cycle returns to Synthwave without stale colors. | Real terminal color rendering, atomic user-owned config persistence under sudo, cached overlay styling, and 80×24 TUI contrast |
| 197 | Saturated durable operation history never blocks startup recovery | Retain a real control-state.json after enough lifecycle/profile operations to reach 512 records, stop Vortix cleanly, and launch the TUI again. Pass: startup transparently evicts the oldest completed record, persists the internal recovery operation at the same 512-record bound, and opens normally; active operations remain queryable and retention.compacted_operations increases. | Real owner-controlled state file, fsync/rename durability, and startup reconciliation |
| 198 | New release recovers metadata pollution left by an older release | Connect an OpenVPN profile with the current build so a hidden .vortix-*.ovpn runtime copy exists, then run Vortix 0.4.2 or 0.4.3 once against the same profile directory before returning to the current build. Pass: the current build authenticates and archives only the exact migration:v1 sidecar the old release generated for its managed runtime copy, leaves the runtime config and real profile identity intact, and starts normally. A hand-edited or merely lookalike hidden sidecar must still stop startup as unexplained. | Real cross-version downgrade/upgrade sequence, root-owned runtime material, byte-bound legacy identity verification, and crash-safe archive recovery |
| 199 | Second-instance refusal and dormant-mode UI stay clear | Start the TUI, then launch a second vortix process against the same config directory and open Bulk Actions (b) in the first session at 80×24. Pass: the second process exits immediately with one plain sentence explaining that Vortix is already running, without an OS errno/backtrace/source location; the first session is unaffected; the header does not say Standard; Bulk Actions and the Event Log advertise no Background-mode controls; the temporary Starting… signal still appears while control startup is incomplete. | Real process lock contention, terminal output, and 80×24 TUI rendering |
| 200 | Kill-switch emergency release and effective truth survive real failure boundaries | On macOS and nft-capable Linux, enable vpn-only with two active tunnels, then test an unexpected block-on-drop tunnel loss, a forced policy-persistence failure, a firewall command that hangs, a malformed/future kill-switch state file, a corrupted control journal, and a normal startup failure. Run sudo vortix release-killswitch after each failure. Pass: protection is never reported from desired/runtime state alone; block-on-drop installs and verifies its pre-block before recovery; every firewall command terminates within its bound; macOS rejects preceding quick-pass or restricted anchor traversal; Linux uses one atomic nft inet transaction and does not engage split-family iptables; emergency release refuses to race a live authority, remains reachable when normal startup is broken, removes only Vortix-owned state, proves absence, and durably fences any temporarily unreadable control history to off. | Real PF/nft kernels, process deadlines, persistence faults, crash recovery, and host firewall coexistence |
| 201 | Dense TUI actions remain profile-scoped, legible, and actionable | At 80×24 with two active profiles, focus an inactive row and press d (nothing else disconnects), then focus active/transitional rows and confirm c/d plus the footer agree on Connect, Cancel, Disconnect, and Force Kill. With the inactive row focused, press uppercase D, confirm Disconnect All, and verify only the two active rows turn orange while already-disconnected rows remain neutral; every tunnel disconnects with one All VPN connections disconnected success toast rather than a stale connection cancelled message. Open b and confirm it contains only Import Profiles and Disconnect All. Trigger route-takeover/overlap and confirm the affected profile, destructive default, choices, IPv6 prefix summary, and +N more count remain visible. Paste a path longer than 58 columns into Import and confirm its cursor stays visible. Trigger an error, open and cancel another overlay with one Esc, and confirm routine info does not replace the error while it is visible, a newer actionable warning or terminal result does, and the error disappears automatically after about eight seconds without requiring Esc. | Real terminal input, timing, and visual hierarchy |
| 202 | Competing VPN DNS routes fail safely with actionable guidance | Keep NetBird, Tailscale, or another VPN connected and give it a more-specific route to the DNS resolver requested by a Vortix profile, then connect that profile on macOS and Linux. Pass: Vortix restores the previous network settings; the TUI explains that another VPN or network service is routing the profile's DNS traffic and asks the user to disconnect it; the Event Log names the resolver, observed and expected interfaces, and the appropriate route -n get or ip route get diagnostic command; after stopping the competing VPN, the same profile can connect. | Real competing tunnels, kernel route precedence, DNS rollback, and TUI/Event Log copy |
| 203 | OpenVPN readiness timeout stops instead of silently reconnecting | Use an OpenVPN endpoint that accepts the process start but never reaches Initialization Sequence Completed, then connect from the TUI and CLI. Pass: the attempt ends at the configured connect timeout, desired state returns to disconnected, a bounded cleanup-only recovery removes any owned OpenVPN process and route reservation without reconnecting, the header/sidebar stop showing Connecting, and an immediate retry is admitted after cleanup. | Real OpenVPN process custody, deadline race, teardown proof, durable intent rollback, and TUI projection |
| 204 | Timed-out profile cleanup cannot disrupt a later profile connection | Let profile A time out after its owned OpenVPN process starts, then immediately connect a disjoint profile B while A's cleanup is still reconciling. Pass: A briefly shows Disconnecting only while its exact old generation is being removed and then becomes Disconnected; B connects and remains Connected; overall status stays connected to B; neither process survives its requested teardown; restarting during either cleanup resumes bounded disconnect-only recovery without relaunching A or B. | Real stale-generation process ownership, current-operation dispatch, multi-profile projection, timeout rollback, and restart recovery |
| 205 | Size-optimised release build stays responsive at the terminal | Build cargo build --release (the profile is opt-level = "z", not 3) and drive a full TUI session on a real terminal at 80x24: scroll a 15-profile sidebar with j/k held down, flip a panel and watch its 25 ms animation frames, resize the window repeatedly while connected, and connect plus disconnect two tunnels. Pass: held-key scrolling tracks without visible lag or dropped frames, the flip animation runs smoothly to completion rather than snapping, a resize repaints within one tick, and nothing feels slower than the same session driven from a cargo build (dev) binary. Fail: any perceptible input lag, a stuttering or skipped flip animation, or a repaint that trails the resize. | Real terminal input timing and animation smoothness under a size-optimised codegen profile |
| 206 | RUST_LOG still filters the same way after the env-filter removal | With a release build, run vortix list under each of: unset RUST_LOG; RUST_LOG= (set but empty); RUST_LOG=off; RUST_LOG=trace; RUST_LOG=vortix::process=info; RUST_LOG=vortix=warn,mio=trace; and a deliberately malformed value. Pass: unset, empty, off and malformed all print zero log lines to stderr and leave the alternate screen clean; trace prints events from every target; the target-scoped forms print only their target's events at or above the named level. Fail: any log line painted over the TUI when the filter should be silent. | Real process environment and stderr interleaving with the alternate screen |
| 207 | Rejected OpenVPN credentials reopen the prompt on the next connect | With a real password-auth profile (lab profile 03), connect and enter a deliberately WRONG password with "save" enabled. Let it fail. Connect again. Then repeat with the correct password, disconnect, and connect a third time. Pass: the first attempt fails with the auth error and a toast saying the saved credentials were removed; the SECOND connect reopens the credential prompt rather than silently failing; after a correct password the third connect reuses the stored pair with no prompt. Also check A (manage auth) shows no stored credentials after the rejection. Fail: the second connect fails without prompting, or a correct password is not remembered. | Real openvpn AUTH_FAILED timing against a live server; the store is only reachable through the privileged control session |
| 208 | Password reveal shows exactly what was typed | Open a connect prompt for an OpenVPN password profile, type a password, and press Ctrl+R. Press it again. Then type r as an ordinary character. Repeat in the Edit Auth Credentials overlay (A), and on a static-challenge profile (04) where the OTP field is present. Pass: Ctrl+R swaps the dots for the literal characters and back, the OTP field reveals with it, the cursor stays where it was, a bare r inserts an r rather than toggling, and the footer hint fits without wrapping at 80 cols. Fail: the toggle eats a keystroke, the cursor jumps, or a revealed value persists into the next prompt. | Real terminal keystroke handling; reveal state must never be persisted |
| 209 | Startup does not replay past profile imports as failures | On a config directory with a long import history (grep -c profile_mutation ~/.config/vortix/control/control-state.json well above zero), launch the TUI. Then import one profile and let it finish, and import one that fails (a malformed file). Pass: startup shows no profile-update toast at all and the Event Log has no Profile update failed lines; the successful import still reports success once; the failed import still reports its failure once. Fail: any count of "profile updates failed" on launch, or a real import that stops being reported. | Needs real durable history written by earlier processes; a fresh config directory cannot reproduce it |
Linux test environments for rows 84–88
The systemd-resolved DNS-integration path is Linux-only — #[cfg(target_os = "linux")] compiles it out on macOS / Windows hosts. To exercise rows 84–88 from a macOS development box, spin up a VM with a real Linux kernel + systemd-resolved. Lima (used under the hood by Colima) gives you that in ~1 minute.
# Already have Colima? You almost certainly have limactl too.
which limactl # if missing: brew install lima
# Fedora 41 ships systemd-resolved on by default — perfect for row 85.
# Arch (closer to the Omarchy reporter's setup, row 84) is also available
# via `template://archlinux`. Pick one.
limactl start --name=vortix-resolved template://fedora
limactl shell vortix-resolved
Inside the VM:
# Sanity-check resolved is the resolver and resolvectl is on PATH.
sudo systemctl is-active systemd-resolved # → active
resolvectl --version # → systemd 25x (…)
# For row 84's Arch-flavoured setup, also confirm the shim is absent:
# dnf list installed openresolv systemd-resolvconf 2>/dev/null # must be empty
# Build vortix from the feature branch.
sudo dnf install -y wireguard-tools cargo git iptables-services
git clone https://github.com/Harry-kp/vortix.git
cd vortix
git checkout feat/systemd-resolved-dns # or main, post-merge
cargo install --path crates/vortix
# Copy a WG profile in. From your Mac:
# limactl copy scripts/test-profiles/wg-full.conf vortix-resolved:/tmp/wg-full.conf
# limactl copy scripts/test-profiles/wg-split.conf vortix-resolved:/tmp/wg-split.conf
mkdir -p ~/.config/vortix/profiles
cp /tmp/wg-full.conf ~/.config/vortix/profiles/
cp /tmp/wg-split.conf ~/.config/vortix/profiles/
# Run the scenario. Note that the cargo-installed binary lives under the
# invoking user's home; sudo's secure_path won't see it without a full path.
sudo "$(which vortix)" up wg-full
resolvectl status wg-full # row 84/85 pass signal
For row 87's fail-open scenario, the cleanest reproduction is the chmod -x trick — it lets wg-quick up succeed (resolved is still running, just resolvectl can't be executed by vortix), so you observe the post-wg-quick up failure path specifically.
For row 88's cleanup verification, capture resolvectl status wg-full BEFORE vortix down, run the down, capture again. Diff should show the link's DNS / Default Route entries gone.
Cleanup when done:
limactl stop vortix-resolved && limactl delete vortix-resolved
The VM is disposable — rerun the whole sequence on a fresh VM if anything gets into a weird state.
How to add a row
- Pick the next sequential
#. - Write the Check in one line — what you're verifying.
- Write Steps as the literal commands or actions to perform.
- Write Why manual as a short tag — match an existing tag when possible so reviewers can scan related rows together (
TUI rendering,Cross-platform: ...,Real-fs ...,Feature wiring gap,Perf benchmark cadence,Fault injection deferred, etc.).
How to remove a row
When an automated test now covers the check, delete the row. Don't annotate "now covered" — the row is the source of truth for "needs human attention." Mention the deletion in the test's commit message so the link survives in git history.