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.

#CheckStepsWhy manual
1Two real WG profiles available for multi-tunnel testingHave corp (0/0) and lab (10/8) configs readyEnvironment setup; can't be automated
2Real OpenVPN profile with username+password auth (OVPN 2.4+)Have .ovpn file with auth-user-pass directiveEnvironment setup
3OVPN 2.3.x binary available for version-rejection pathapt install openvpn=2.3.* or equivalentTest fixture; requires installing old binary
4Second OS user account for daemon UID-gate adversarial testuseradd vortix-adversaryTest fixture; cross-UID setup
5Multi-tunnel routing: corp owns default, lab owns 10/8vortix 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-labMulti-tunnel netns harness not built (plan 002 U1+U2)
6Multi-tunnel real-internet exit IP differs per tunnelcurl 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
73+ tunnels overflow ladder in header degrades cleanlyConnect 6 tunnels on a 60-col terminal; verify Tier 1 → Tier 2 → Tier 3 (dot-row) transitions; never wrapsTUI 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)
11Multi-tunnel disconnect via d key disconnects single tunnelConnect 2 tunnels; focus secondary in sidebar; press d; only that one disconnectsTUI keybinding; needs snapshot harness
12Shift+D with N≥2 fires "Disconnect all N?" confirm dialogConnect 3 tunnels; press Shift+D; verify dialog renders; Y disconnects allSame as #11
13Tab cycles Connection Details focus across active tunnels2 tunnels up; focus Connection Details; press Tab; focus advances; wraps at endSame as #11
14c cancels an in-flight Connecting stateConnect a slow tunnel; while Connecting (), focus Details + press c; sidebar clearsSame as #11
15Multi-tunnel iptables ruleset has per-tunnel ACCEPT + RFC1918 carveAfter 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)
16Atomicity probe: no traffic leak window during tunnel transitionWhile corp+lab up, run continuous curl 10.99.0.99 --max-time 2 loop; externally kill corp; assert no BLOCKED → ACCEPT → BLOCKED gapSame as #15
17macOS pf ruleset has per-tunnel pass-out rules + RFC1918 carvemacOS only: sudo pfctl -s rules shows per-interface pass-outs + RFC1918 carveSame as #15; macOS-only
18Every WG launch strips DNS = ... before wg-quickConnect 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
19Every OVPN launch suppresses protocol-side DNS mutationConnect 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
20OVPN 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 attemptNeeds real OVPN 2.3 binary install
21fwmark hijack warning shows on Connection Details for WG without FwMarkConnect WG without FwMark = ... as secondary; Connection Details shows ⚠ Fwmark hijack risk... lineTUI rendering
22fwmark warning DOES NOT show when secondary has FwMark = 51820Same as #21 but with FwMark set; no warning lineTUI rendering
23fwmark warning DOES NOT show when single tunnel (not secondary)Connect just the WG-without-FwMark profile alone; no warningTUI rendering
24Sidebar empty-state renders correctlyStart vortix with no profiles imported; sidebar shows "No profiles" messageTUI rendering
25Sidebar scroll with 10+ profiles worksImport 15 profiles; j/k scroll the list; selection wraps at edgesTUI rendering
26Header degrades at 50 / 60 / 80 / 100 / 120 columnsResize terminal to each width; header never wraps; tunnels strip uses the right TierTUI rendering at variable widths
27Sidebar truncates rows cleanly at 80x12Resize to 80x12; sidebar still renders without panic; rows truncated with ellipsisTUI rendering
28NO_COLOR=1 renders badges via Unicode shape onlyNO_COLOR=1 vortix; status badges ( etc.) still distinguishable by shape, not just colorTUI rendering + accessibility
29VoiceOver / Orca announces sidebar rowsmacOS VoiceOver or Linux Orca enabled; sidebar row text is announced; no purely-color signals required for comprehensionScreen-reader accessibility; no automation hook
30AwaitingUserInput hint renders on Connection DetailsOVPN with 2FA prompt — Connection Details shows ⚠ Press [Enter] to provide 2FA code/passphrase/inputTUI rendering
31Demoted 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 DetailsTUI rendering
32Security Guard shows EXPOSED with 0 tunnelsNo tunnels up; Security panel headline = EXPOSEDTUI rendering
33Security Guard shows PARTIAL with secondaries-onlyConnect tunnels that don't claim 0/0; Security panel = PARTIAL + KS-mode-aware Killswitch bulletTUI rendering
34(retired — replaced by rows 89–99 covering the v0.4.2 dual-stack Real IP / Exit IP redesign)
35Passive candidate socket is owner-only and non-privilegedRun 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
36Read-only CLI ops bypass daemon when socket absentStop daemon; run vortix status; falls back to direct scanner with exit 0Same as #35
37Cross-UID adversarial: different non-root user can't talk to daemonRun daemon as user A; as user B attempt socat - UNIX-CONNECT:/path/to/vortix.sock; daemon closes after first frame with UID-mismatch errorNeeds two-user setup; deferred (plan 002 U10)
38Wire-format compatibility: v0.3.x client → v2 daemon socket fails cleanlyOld vortix binary on PATH; vortix status --json against running v2 daemon; structured error, not silent mis-parseNeeds real v0.3.x binary install
39SIGTERM to passive candidate drains clients and cleans its socketWith 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
40Passive candidate restart observes without reconnectingStart 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
41V2 → V1 downgrade: v0.3.x reads V2 journal without erroringConnect 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
42V2 → V1 downgrade: clean shutdown + v0.3.x cold start worksPer docs/MIGRATION.md 5-step procedure; v0.3.x launches successfullySame as #41
43Network drop mid-handshake → FSM retries per budgetUse tc qdisc add dev <iface> root netem loss 100% mid-handshake; vortix retries; log entries readableFault injection deferred (Phase 2)
44OVPN auth fails with wrong password → + re-prompt availableConnect with wrong password; sidebar shows ; auth overlay can re-openReal OVPN server needed
45Profile deleted while connected → tunnel disconnects on next TickActive tunnel; delete profile file; within ~1s tunnel goes down + sidebar updatesReal-fs + state coherence; partial automation possible
46Out-of-disk during secret_file write → graceful errormount -o remount,size=1 or ramdisk to fill /tmp; attempt vortix auth save; error returned; no half-written auth fileFault injection deferred (Phase 2)
47Profile config readable only by root, run as non-root → permission errorchmod 0400 ~/.config/vortix/foo.conf; vortix up foo as non-root; error surfaced; no path leak in stderrReal-fs perms test; netns harness deferred
48Auth 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)
49Symlink 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 unchangedReal-fs + attacker setup needed
50ps aux does not leak OVPN credentials in command lineWhile OVPN running: ps auxf | grep openvpn | grep <password> returns 0 linesReal-process scan; partial automation possible
51/tmp/vortix-*/ temp WG configs are mode 0600 + unlinked on tunnel downWhile WG connected: ls -la ${TMPDIR}/vortix-*/; mode 0600; after vortix down: directory goneReal-fs assertion
52macOS Apple Silicon smoke: full multi-tunnel happy pathRun multi-tunnel scenarios on an M-series MacBookCross-platform: real consumer hardware
53macOS Intel smoke (if available)Same as #52 on an Intel MacCross-platform: real consumer hardware
54Legacy Linux iptables cleanup smokeSeed 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 remainCross-platform: requires distro-specific install
55Linux nftables host smokeRun on a host where nft is the only backendCross-platform: requires distro-specific install
56Windows: vortix does not crash (NG features stubbed)vortix --version; vortix list; no panic on profile import; multi-tunnel features may be stubbedCross-platform: Windows
5710 active tunnels: TUI render budget under 16ms/frameConnect 10 tunnels; observe TUI doesn't lag on Tab / sidebar navPerf benchmark cadence (Phase 2)
58Killswitch ruleset rewrite latency sub-100ms at N=5Time anchor-scoped pfctl / one nft inet transaction during a 5-tunnel transitionPerf benchmark cadence (Phase 2)
5950 profiles loaded: sidebar scroll + search responsiveSynthesize 50 empty .conf files; verify no perceptible TUI lagPerf benchmark cadence (Phase 2)
60(removed — auto-promote feature retired)
61Journal shows ConnectAttemptBlockedByConflict on overlay-cancelled connectTrigger conflict overlay; cancel it; journal entry appearsSame as #60
62vpn-only engages firewall immediately while VPN is upvortix 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 enforcementRegression check for the vpn-only (AlwaysOn) semantic change (commit 34f07e3)
63vpn-only keeps blocking through a VPN drop → reconnect cycleWith 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 gapCanonical Linux killswitch invariant
64Per-profile concurrent retry: two profiles failing in parallel each retry independentlyConnect 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 otherPer-profile retry HashMap correctness; can't be observed via TUI single-slot view
65D-4 auto-adopt of externally-started tunnel — authoritative iface caseStart 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 succeedsCross-process registry adoption from kernel state
66CLI vortix down A while TUI tracks A+B does not clobber B's killswitch sliceTUI 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 ruleBug 1 regression check (commit 8e7181f)
67Security Guard panel fits cleanly at 80×24 with no compaction dropsResize 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
68Security Guard panel alarm states pull the eye correctlyTrigger 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-lineTUI rendering — visual hierarchy regression check
69WireGuard on macOS reports the real utunN devicemacOS 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 portReal macOS kernel + wg-quick behavior; can't be simulated
70Multi-OpenVPN primary-election byte-comparable with kernelmacOS 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 regressionMulti-PID iface attribution that automated tests can't verify without real openvpn processes
71OpenVPN log-parse failure leaves no synthetic iface in registryForce 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
72Scanner does NOT promote Connecting → Connected pre-protocol-successConnect 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 removalRace-window observation; the bug it prevents is silent corruption, not a crash
73Externally-adopted OpenVPN on macOS multi-tunnel shows unauthoritative badgemacOS 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
74Real IP cache survives startup-with-VPN-up raceConnect 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
75Real IP persists across vortix restarts via real-ip.cacheSequence: (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
76HISTORICAL — Approach A SCRV1-via-auth-file premise failedRun 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
77Non-MFA OpenVPN regression after the static-challenge wiringSetup: 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
78Stale SCRV1 cleanup on vortix startupSetup: 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)
84Fresh resolved-native distro connects WG-with-DNS without resolvconf shimOn 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
85Default Fedora Workstation connects WG-with-DNS without openresolvFedora 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
86Multi-tunnel resolved policy has exactly one catch-allConnect 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
87DNS apply/read-back failure leaves routing up but truth degradedStop 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
88Coordinator cleanup explicitly reverts only Vortix-owned linksCapture 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
89Identity collapses to single Real IP / Exit IP rows when host has NO IPv6Host 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
90Real IPv6 populates from disk cache on launch-with-VPN-upSequence: (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
91Exit IPv6 reads ✓ on a tunnel that actually carries v6Run 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
92Exit IPv6 reads ✗ leaking when public_ipv6 == real_ipv6 while VPN upHost 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
93Exit IPv4 / Exit IPv6 both render split-route — no exit in split-only topologyHost 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
94v6 rows flip correctly across connect → disconnect cyclesHost 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
95Real IPv6 reads checking… when launched mid-connection without cacheSequence: (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
96DNS row reads ✓ Protected when configured resolver = answering recursorConnect 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)
97DNS row reads ✗ leaking when recursor is a different providerRun 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
98DNS row stays ✓ when configured = v4 anycast and recursor returns same provider's v6Connect 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
99DNS row reads Unknown (✓ no alarm) when no primary tunnel owns default routeTopology: 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
100DNS row reads ✓ ProbeFailed when the recursor probe times outSequence: (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
101IPv6-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)]
102Pre-cutover CLI/TUI control-contract baselineWith 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
103macOS PF apply, refresh, and release preserve unrelated policyStart 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
104macOS PF read-back failure degrades protection truthPlace 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
105Firewall drift invalidates protection within five secondsEngage 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
106Concurrent first nft enable converges to one verified policyOn 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
107Cross-protocol DNS primary transfer is complete and gap-safeConnect 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
108Linux resolvconf fallback is owned and idempotentOn 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
109macOS primary DNS follows the active SystemConfiguration service and restores exactlyOn 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
110macOS scoped secondary resolver does not become globalPrimary 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
111OpenVPN restart with unavailable DNS evidence stays conservativeConnect 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
112Concurrent CLI and TUI DNS reconciliation is serialized and liveKeep 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
113Default-route probe failure preserves the last known DNS topologyWith 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
114WireGuard teardown never replays profile DNSImport 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
115Legacy profile identity backfill is one-time and rollback-readableCopy 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
116Malformed or duplicate profile identity fails before mutationDuplicate 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
117Profile rename preserves all associationsSave 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
118Profile rename resumes after every durable boundaryFor 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
119External editor rename is observation-onlyWhile 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
120Profile import and delete recover after every durable boundaryKill 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
121Tampered transaction journals cannot escape the profile rootReplace 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
122Profile mutation lock contention is boundedHold 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
123CLI profile mutation cannot race tunnel lifecycleStart 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
124Delayed retry follows identity through rename and reorderSchedule 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
125Supervised multi-profile connects keep the TUI responsiveStart 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
126Scanner observations never seize lifecycle authorityStart 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
127Disconnect tombstone contains late scanner ticksDisconnect 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
128OpenVPN foreground custody leaves no process treeConnect 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
129Policy barrier failures never publish mixed protectionFor 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
130One-shot OpenVPN custody survives its initiating CLIRun 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
131Custodian failures and stale capabilities fail closedExercise 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
132OpenVPN foreground config cannot escape custodyImport 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
133WireGuard valid and unreachable peers agree on every surfaceOn 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
134WireGuard timeout cleanup preserves blocking and owns only its attemptEnable 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
135WireGuard per-peer stale health recovers without false disconnectUse 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
136macOS WireGuard utun mapping is not connectivity proofOn 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
137Scanner-only WireGuard stays observed and unmanagedBring 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
138Per-peer activity expectation never borrows healthUse 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
139Engine settings compatibility bridge reaches real WireGuard factorySet 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
140Standard-mode WireGuard receipt survives the initiating clientConnect 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
141Authoritative config directory selects every settings layerPlace 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
142Standard-mode hooks run only as the invoking ownerAs 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
143Hook timeout and cancellation leave no descendant process treeConfigure 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
144Executable protocol directives never reach privileged binariesImport 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
145Same-boot control restart scans before resuming durable intentIn 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
146Passive daemon stays mutation-free under concurrent clientsRun 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
147Normal package staging starts no Vortix serviceInstall 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
148Staged helper refuses every operational entrypointAs 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
149Package identity and service instance substitution fail closedIn 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
150Helper loss never turns a missing reply into a blind retryIn 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
151Helper lifecycle owns and reaps exactly one admitted tunnelIn 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
152Helper policy barriers remain fail-closed across crashesIn 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
153Helper cleanup removes only ledger-owned resources after proving absenceIn 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
154Root helper replay ledger survives crashes without accepting path substitutionOn clean Linux and macOS package installs, interrupt helper checkpoint writes before data fsync, before rename, and before parent-directory fsync, including each pending_effectowned 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
155Standard-mode OpenVPN clears inherited provider and loader environment before privileged launchOn 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
156Legacy migration stays inside the invoking user's profile directory and ignores runtime configsIn 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
157One-shot Standard-mode ownership survives CLI process exit without adopting foreign tunnelsOn 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
158Canonical one-shot CLI preserves contracts and durable timeout recoveryOn 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
159vpn-only recovery remains fail-closed across hostname reconnect and partial firewall failureOn 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
160Canonical one-shot control stays responsive under slow scans and large split-route policyOn 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
161OpenVPN static challenge is service-owned, one-shot, and memory-onlyOn 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
162Typed profile mutations preserve identity and serialize against lifecycleOn 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
163Read-only clients cannot delete a live WireGuard teardown capabilityStart 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
164Canonical TUI remains responsive and truthful across multi-tunnel controlOn 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
165Multi-WireGuard scan and unexpected-drop recovery are one coherent transitionConnect 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
166Background diagnostics remain bounded, redacted, and advisory during failureRun 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
167Preparatory remote adapters stay dormant without changing Standard modeOn 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
168Prepared Background UX restores the terminal and never elevates an untrusted clientOn 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
169Helper-owned nftables policy is atomic, exact, and restart-safeIn 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
170Helper-owned macOS DNS is exact, foreign-safe, and restart-auditedIn 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
171Helper-owned Linux DNS preserves exact per-generation state across crashesIn 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
172Helper-owned route cutover has one writer and exact restart truthIn 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
173Preparatory authority lock fences Standard writers without enabling Background modeOn 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
174Helper policy inventory is exact across restart and schema downgradeIn 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
175Live telemetry remains current without flooding the Event LogConnect 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
176Last-connected activity is canonical, verified, and restart-stableWith 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
177Large TUI directory imports respect bounded control backpressureFrom 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
178Profile deletion waits for the complete disconnect transactionConnect 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
179Restart-restored teardown fences clear from exact absenceInterrupt 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
180OpenVPN pushed DNS reaches the canonical final policyConnect 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
181Managed macOS encrypted DNS does not cause a false VPN refusalOn 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
182Remembered OpenVPN credentials remain owner-bound across sudo upgradeOn 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
183Definitive OpenVPN authentication rejection exits ConnectingOn 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
184Security Guard DNS verdict follows canonical policy and resolver routesConnect 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
185Long-lived Security Guard evidence stays truthful and IPv6 probes terminateKeep 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
186WireGuard interface names fail early and remain explicitOn 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
187macOS WireGuard policy rollback retains the wg-quick aliasImport 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
188TUI first frame is independent of control bootstrap latencyWith 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
189One-shot CLI lifecycle commands acknowledge bounded work immediatelyOn 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
190Ephemeral compatibility lab is complete and leaves no paid resource behindRun 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
191Cold one-shot CLI does not let startup policy overwrite its first lifecycle commandWith 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
192Late OpenVPN route conflicts stop once and return to the existing confirmation flowConnect 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
193macOS restart recovers protected DNS ownership before a new connectionConnect 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
194Multi-profile disconnect reaches terminal truth without a restartConnect 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
195Helper WireGuard sessions do not accumulate NetworkManager historyOn 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
196Live theme switching covers every built-in palettePress 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
197Saturated durable operation history never blocks startup recoveryRetain 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
198New release recovers metadata pollution left by an older releaseConnect 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
199Second-instance refusal and dormant-mode UI stay clearStart 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
200Kill-switch emergency release and effective truth survive real failure boundariesOn 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
201Dense TUI actions remain profile-scoped, legible, and actionableAt 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
202Competing VPN DNS routes fail safely with actionable guidanceKeep 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
203OpenVPN readiness timeout stops instead of silently reconnectingUse 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
204Timed-out profile cleanup cannot disrupt a later profile connectionLet 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
205Size-optimised release build stays responsive at the terminalBuild 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
206RUST_LOG still filters the same way after the env-filter removalWith 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
207Rejected OpenVPN credentials reopen the prompt on the next connectWith 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
208Password reveal shows exactly what was typedOpen 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
209Startup does not replay past profile imports as failuresOn 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

  1. Pick the next sequential #.
  2. Write the Check in one line — what you're verifying.
  3. Write Steps as the literal commands or actions to perform.
  4. 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.