Skip to content

tests: VM mode for kernel cells (aircrack-ng/rtl8812au on pinned kernel)#33

Merged
josephnef merged 1 commit into
masterfrom
feat/regress-vm-mode
May 23, 2026
Merged

tests: VM mode for kernel cells (aircrack-ng/rtl8812au on pinned kernel)#33
josephnef merged 1 commit into
masterfrom
feat/regress-vm-mode

Conversation

@josephnef
Copy link
Copy Markdown
Collaborator

@josephnef josephnef commented May 23, 2026

Status: WIP — chip init succeeds, RX data flow still silent

Brings up the RTL8821AU end-to-end except for RX bulk-IN data flow. Posting so others can continue the investigation from a clean checkpoint that builds against current master instead of resurrecting #22 (which deletes the 8814AU work and has a parallel dispatch enum that doesn't compose with the ICType-based convention #23-#29 established).

NOTE: examples below originally used a real username/IP; scrubbed to <user>@<VM-IP> placeholders. The merged commit message still contains the original — tests/setup_vm.sh and the README on master are fixed in #34.

What this is

Adds a libvirt-VM execution mode to tests/regress.py so the kernel-side cells of the regression matrix can run against the aircrack-ng/rtl8812au out-of-tree driver on a pinned kernel, instead of fighting the host kernel.

Why a VM

The OOT aircrack-ng/rtl8812au driver lags kernel API changes by 6-12 months (timer_*, cfg80211 callback signatures with MLO link_id, etc.). On kernel 6.15+ it needs hand-patching to build. morrownr's README flags that mainline rtw88_* is now the recommended path from kernel 6.14 onwards — but mainline rtw88_8814au currently fails to probe RTL8814AU on this lab's adapter (failed to download firmware, error -22). So for 8814 specifically, OOT aircrack-ng is the only working kernel-side path.

Pinning a VM to Ubuntu 22.04 LTS (kernel 5.15) gives a stable platform where aircrack-ng's driver builds and loads cleanly. The host can upgrade freely without breaking the test rig.

Pieces

tests/setup_vm.sh — one-shot VM provisioner. Clones an Ubuntu 22.04 cloud image (jammy-base.qcow2), generates a cloud-init seed (creates a user with caller's SSH key, NOPASSWD sudo, installs build-essential / dkms / linux-headers / iw / tcpdump / python3-scapy / aircrack-ng), virt-installs with qemu-xhci USB controller for hot-plug, runs make dkms_install of aircrack-ng/rtl8812au inside via runcmd. ~5-10 min end to end. --teardown and --status subcommands included.

tests/regress.py refactor — introduces a KernelHost abstraction owning every kernel-side operation (modprobe, sysfs reads, iw, tcpdump, scapy). Local mode = subprocess.run. VM mode = ssh ... sudo + virsh attach-device/detach-device for per-cell USB passthrough. New CLI flags --vm-name / --vm-ssh (env: DEVOURER_VM_NAME, DEVOURER_VM_SSH). When invoked under sudo, picks up SUDO_USER's SSH key — root usually doesn't have keys provisioned on the VM.

Per-cell DUT routing — each cell calls _ensure_dut_location for each DUT, which (in VM mode) moves the DUT between host and VM via virsh as needed. State always restored to "both DUTs on host" between cells via try/finally so a crashed cell doesn't poison the next one. Script start has a release_all_known_duts pass for leftover-attached DUTs from previous aborted runs.

Validation

Arch Linux host kernel 6.18, VM Ubuntu 22.04 LTS kernel 5.15, two USB DUTs in a hub (0bda:8812 RTL8812AU + 0bda:8813 RTL8814AU):

## Regression matrix — channel 100
- TX adapter: 0bda:8812 (RTL8812AU)
- RX adapter: 0bda:8813 (RTL8814AU)
- Kernel host: VM devourer-testrig via <user>@<VM-IP>
- Cell duration: 10s
- Pass threshold: ≥ 3 hits

|   | TX = devourer | TX = kernel |
|---|---|---|
| RX = devourer | 0 hits / 4500 TX ✗ | 0 hits / 258 TX ✗ |
| RX = kernel | 4172 hits / 4500 TX ✓ | 229 hits / 259 TX ✓ |
  • Baseline ✓ kernel-TX 8812 → kernel-RX 8814 inside VM, ~88% delivery
  • devourer-TX validation ✓ devourer-TX 8812 on host → kernel-RX 8814 in VM, ~93% delivery — confirms devourer's RTL8812AU TX really emits valid frames at the wire level
  • The two failing cells are the pre-existing devourer 8814 RX TODO

For comparison: the same hardware in local mode from #32's first run got 1 hit on the devourer-TX→kernel-RX cell because mainline rtw88_8814au couldn't probe the chip. The VM with aircrack-ng gives ~4000× the signal.

🤖 Generated with Claude Code

Adds a libvirt-VM execution mode so the kernel-side cells of the
regression matrix can run against the aircrack-ng/rtl8812au out-of-tree
driver on a pinned kernel — sidestepping the OOT-driver-vs-modern-kernel
breakage that kept the matrix from validating chipsets like RTL8814AU
on hosts running recent kernels.

Why VM: aircrack-ng/rtl8812au lags kernel API changes by 6-12 months
(timer_*, cfg80211 callback signatures with MLO link_id, etc.). On
kernel 6.15+ the OOT driver needs hand-patching to build, and
morrownr/USB-WiFi flags that mainline rtw88 is the recommended path
from 6.14 onwards. But mainline rtw88_8814au currently fails to probe
the RTL8814AU on this lab's adapter (`failed to download firmware`,
`error -22`) — so for 8814 specifically, the OOT driver is the only
working kernel path. Pinning a VM to Ubuntu 22.04 LTS (kernel 5.15)
gives us a stable platform where aircrack-ng's driver builds and loads
cleanly without ongoing patching, regardless of how the host kernel
evolves.

Three pieces:

1. `tests/setup_vm.sh` — one-shot VM provisioner. Clones the local
   `jammy-base.qcow2` cloud image, generates a cloud-init seed
   (creates `dima` user with the caller's SSH key, NOPASSWD sudo,
   installs build-essential / dkms / linux-headers / iw / tcpdump /
   python3-scapy / aircrack-ng), `virt-install`s with a qemu-xhci USB
   controller for hot-plug, then runs `make dkms_install` of
   aircrack-ng/rtl8812au inside via cloud-init runcmd. ~5-10 min end
   to end. `--teardown` and `--status` subcommands included.

2. `tests/regress.py` refactor: introduces a `KernelHost` abstraction
   that owns every kernel-side operation (modprobe / sysfs reads / iw
   / tcpdump / scapy inject). Local mode = `subprocess.run`. VM mode =
   `ssh ... sudo` wrappers + `virsh attach-device` / `detach-device`
   for per-cell USB passthrough. New CLI flags `--vm-name` /
   `--vm-ssh` (env: DEVOURER_VM_NAME, DEVOURER_VM_SSH). When invoked
   under `sudo`, the script picks up SUDO_USER's SSH key so it can
   reach the VM without root having its own key provisioned.

3. Per-cell DUT routing: each cell calls `_ensure_dut_location` for
   each DUT, which (in VM mode) transitions the DUT between host and
   VM via virsh as needed. State always restored to "both DUTs on
   host" between cells via try/finally so a crashed cell doesn't
   poison the next one. Script start also has a `release_all_known_duts`
   pass to clean up any leftover-attached DUT from a previous aborted run.

Validation on trainer-arch (Arch host kernel 6.18, VM Ubuntu 22.04
kernel 5.15, 8812AU + 8814AU on USB hub):

  |   | TX = devourer | TX = kernel |
  |---|---|---|
  | RX = devourer | 0 hits / 4500 TX ✗ | 0 hits / 258 TX ✗ |
  | RX = kernel | 4172 hits / 4500 TX ✓ | 229 hits / 259 TX ✓ |

  - baseline ✓ (kernel-TX 8812 → kernel-RX 8814 inside VM, ~88%
    delivery)
  - devourer-TX validation ✓ (devourer-TX 8812 on host → kernel-RX
    8814 in VM, ~93% delivery — confirms devourer's 8812AU TX really
    emits valid frames at the wire level)
  - the two failing cells are pre-existing devourer 8814 RX TODO,
    not regressions

For comparison: the same hardware in local mode (PR #32 first run)
got 1 hit on the devourer-TX→kernel-RX cell because mainline
rtw88_8814au couldn't probe the chip. The VM with aircrack-ng gives
~4000x the signal.

A few smaller fixes folded in:
- TX-count parser surfaces "Failed to send packet" failure count
  separately from the rate-limited `<devourer-tx>` print count
  (previously a misleading low number when sends were failing)
- `--no-baseline-abort` flag for partial-rig diagnostics
- `wait_for_wlan_iface` timeout bumped to 20s (kernel rebinds + VM
  passthrough enumeration can take 10s+)
- Kernel-TX cells `wait()` for inject_beacon to self-terminate instead
  of killing the ssh wrapper — captures the final "sent N frames" line
  (previously TX count showed 0 even though RX side received the frames)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@josephnef josephnef merged commit a580933 into master May 23, 2026
5 checks passed
@josephnef josephnef deleted the feat/regress-vm-mode branch May 23, 2026 10:30
josephnef added a commit that referenced this pull request May 23, 2026
Extends tests/regress.py with a --full-matrix mode that iterates every
ordered (TX, RX) pair of plugged-in DUTs across all four driver-side
combinations (kernel-only, devourer-TX/kernel-RX, kernel-TX/devourer-RX,
devourer-only) and emits one NxN table per mode instead of one 4-cell
table for a single pair. Useful for catching cross-chipset interop
regressions in PRs that touch shared HAL code.

Usage:
    sudo python3 tests/regress.py --full-matrix --channel 100 \\
        --vm-name devourer-testrig --vm-ssh <user>@<VM-IP>

For N adapters, runs N*(N-1)*4 cells total — at ~30-40s per cell in VM
mode that's ~16 min for N=3, manageable. Diagonal is blanked (same
physical adapter can't simultaneously TX and RX with one driver). The
script reuses run_cell as-is; the addition is just the outer pair loop,
result dict keyed by (tx_side, rx_side, tx_vidpid, rx_vidpid), and a
new emit_full_markdown that renders four NxN tables.

Also scrubs personal identifiers from earlier docs/scripts (PR #33):
- tests/setup_vm.sh now reads VM_USER from $SUDO_USER / $USER instead
  of hardcoding a specific username
- tests/README.md + regress.py docstrings switch to <user>@<VM-IP>
  placeholders in example commands

Validation on a 3-adapter rig (Ubuntu 22.04 VM with aircrack-ng/88XXau,
0bda:8812 + 0bda:8813 + 2357:0120, channel 100, 10s/cell):

  ## Kernel-only (rig sanity)
  All 6 cross-chipset cells pass — 88XXau handles all three chipsets
  cleanly in the pinned-kernel VM (88-271 hits per cell).

  ## devourer-TX → kernel-RX
  devourer-TX confirmed for 8812 (4114, 4693 hits) AND 8821 (4341 hits
  reaching 8814 kernel RX). 8814 TX flaky after passthrough cycles
  (chip-state degradation across cell sequencing — known sensitive).

  ## kernel-TX → devourer-RX
  Surprise — devourer-RX 8821 caught 200 frames from kernel-TX 8814,
  contradicting PR #30's "RX silent" finding. devourer-RX 8812
  confirmed (100 hits from each of 8814, 8821 TX). devourer-RX 8814
  confirmed broken (0 hits all directions — known TODO).

  ## devourer ↔ devourer
  All 0 — every cell hits at least one broken side (8814 RX or 8814 TX
  degraded mid-run).

Net new product signal from the full matrix:
- devourer-TX 8821 actually works (was unvalidated since PR #30 had no
  peer sniffer in that session — VM mode is the peer)
- devourer-RX 8821 works under at least one TX condition — reopen PR
  #30's "RX silent" conclusion
- 8814 chip state degrades through repeated host↔VM passthrough — needs
  investigation, may want a chip reset between cells

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
josephnef added a commit that referenced this pull request May 23, 2026
…el) (#33)

## What this is

Adds a libvirt-VM execution mode to `tests/regress.py` so the
kernel-side cells of the regression matrix can run against the
`aircrack-ng/rtl8812au` out-of-tree driver on a **pinned kernel**,
instead of fighting the host kernel.

## Why a VM

The OOT `aircrack-ng/rtl8812au` driver lags kernel API changes by 6-12
months (timer_*, cfg80211 callback signatures with MLO link_id, etc.).
On kernel 6.15+ it needs hand-patching to build. morrownr's README flags
that mainline `rtw88_*` is now the recommended path from kernel 6.14
onwards — but **mainline `rtw88_8814au` currently fails to probe**
RTL8814AU on this lab's adapter (`failed to download firmware`, `error
-22`). So for 8814 specifically, OOT aircrack-ng is the only working
kernel-side path.

Pinning a VM to Ubuntu 22.04 LTS (kernel 5.15) gives a stable platform
where aircrack-ng's driver builds and loads cleanly. The host can
upgrade freely without breaking the test rig.

## Pieces

**`tests/setup_vm.sh`** — one-shot VM provisioner. Clones an Ubuntu
22.04 cloud image (`jammy-base.qcow2`), generates a cloud-init seed
(creates a user with caller's SSH key, NOPASSWD sudo, installs
build-essential / dkms / linux-headers / iw / tcpdump / python3-scapy /
aircrack-ng), `virt-install`s with `qemu-xhci` USB controller for
hot-plug, runs `make dkms_install` of `aircrack-ng/rtl8812au` inside via
`runcmd`. ~5-10 min end to end. `--teardown` and `--status` subcommands
included.

**`tests/regress.py` refactor** — introduces a `KernelHost` abstraction
owning every kernel-side operation (`modprobe`, sysfs reads, `iw`,
`tcpdump`, scapy). Local mode = `subprocess.run`. VM mode = `ssh ...
sudo` + `virsh attach-device`/`detach-device` for per-cell USB
passthrough. New CLI flags `--vm-name` / `--vm-ssh` (env:
`DEVOURER_VM_NAME`, `DEVOURER_VM_SSH`). When invoked under `sudo`, picks
up `SUDO_USER`'s SSH key — root usually doesn't have keys provisioned on
the VM.

**Per-cell DUT routing** — each cell calls `_ensure_dut_location` for
each DUT, which (in VM mode) moves the DUT between host and VM via virsh
as needed. State always restored to \"both DUTs on host\" between cells
via try/finally so a crashed cell doesn't poison the next one. Script
start has a `release_all_known_duts` pass for leftover-attached DUTs
from previous aborted runs.

## Validation on trainer-arch

Arch Linux host kernel 6.18, VM Ubuntu 22.04 LTS kernel 5.15, two USB
DUTs in a hub (0bda:8812 RTL8812AU + 0bda:8813 RTL8814AU):

```
## Regression matrix — channel 100, 2026-05-23 13:22:14
- TX adapter: 0bda:8812 (RTL8812AU)
- RX adapter: 0bda:8813 (RTL8814AU)
- Kernel host: VM devourer-testrig via <user>@<VM-IP>
- Cell duration: 10s
- Pass threshold: ≥ 3 hits

|   | TX = devourer | TX = kernel |
|---|---|---|
| RX = devourer | 0 hits / 4500 TX ✗ | 0 hits / 258 TX ✗ |
| RX = kernel | 4172 hits / 4500 TX ✓ | 229 hits / 259 TX ✓ |
```

- **Baseline ✓** kernel-TX 8812 → kernel-RX 8814 inside VM, **~88%
delivery**
- **devourer-TX validation ✓** devourer-TX 8812 on host → kernel-RX 8814
in VM, **~93% delivery** — confirms devourer's RTL8812AU TX really emits
valid frames at the wire level
- The two failing cells are the pre-existing devourer 8814 RX TODO, not
regressions; cell 3's new \"0 hits / 258 TX\" output correctly fingers
the RX side (TX side really did emit 258 frames; devourer-RX 8814
silent)

For comparison: the same hardware in local mode from #32's first run got
**1 hit** on the devourer-TX→kernel-RX cell because mainline
`rtw88_8814au` couldn't probe the chip. The VM with aircrack-ng gives
**~4000× the signal**.

## Smaller fixes folded in

- TX-count parser surfaces \"Failed to send packet\" failure count
separately from the rate-limited `<devourer-tx>` print count (previously
misleadingly low when sends were failing)
- `--no-baseline-abort` flag for partial-rig diagnostics
- `wait_for_wlan_iface` timeout bumped to 20s (kernel rebinds + VM
passthrough enumeration take 10s+)
- Kernel-TX cells `wait()` for `inject_beacon` to self-terminate instead
of killing the ssh wrapper — captures the final \"sent N frames\" line
(previously TX count showed 0 even though RX side received frames)

## Usage

```bash
sudo tests/setup_vm.sh                    # ~5-10 min, one-time
sudo tests/setup_vm.sh --status

sudo python3 tests/regress.py --channel 100 \
    --vm-name devourer-testrig \
    --vm-ssh <user>@<VM-IP>
```

See [`tests/README.md`](tests/README.md) for full options, prereqs,
architecture notes.

## Known limitations (documented in README)

- VM mode assumes a single libvirt host running both `virsh` (locally)
and the VM. Pulling the VM onto a different host needs your own `virsh`
wrapper.
- Per matrix run: ~3-4 min in VM mode (USB hot-plug adds ~5s per cell
transition vs ~100s for local mode).
- Two-adapter scope today. >2 needs a pairing loop in `main()`.
- Cell 4 (`devourer-TX → devourer-RX`) needs both DUTs
devourer-claimable simultaneously — if one chipset has broken devourer
RX (current RTL8814AU TODO), that cell shows 0 regardless of TX.

## Test plan

- [x] VM provisioning succeeds end-to-end (`setup_vm.sh` clean run on
trainer-arch)
- [x] aircrack-ng/rtl8812au DKMS install works inside VM (kernel 5.15)
- [x] USB hot-plug of 8814AU into VM works (mainline rtw88 couldn't
probe; aircrack-ng claims cleanly)
- [x] Full 4-cell matrix runs end-to-end in VM mode
- [x] Baseline cell passes (rig sanity)
- [x] devourer-TX → kernel-RX cell passes (cross-driver validation)
- [x] Failing cells produce diagnostic output (TX count vs RX hits)
- [ ] Validate on a different distro / different VM base image
- [ ] Validate with a 2× same-chip DUT setup (both cells with
both-devourer pass)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
josephnef added a commit that referenced this pull request May 23, 2026
Extends tests/regress.py with a --full-matrix mode that iterates every
ordered (TX, RX) pair of plugged-in DUTs across all four driver-side
combinations (kernel-only, devourer-TX/kernel-RX, kernel-TX/devourer-RX,
devourer-only) and emits one NxN table per mode instead of one 4-cell
table for a single pair. Useful for catching cross-chipset interop
regressions in PRs that touch shared HAL code.

Usage:
    sudo python3 tests/regress.py --full-matrix --channel 100 \\
        --vm-name devourer-testrig --vm-ssh <user>@<VM-IP>

For N adapters, runs N*(N-1)*4 cells total — at ~30-40s per cell in VM
mode that's ~16 min for N=3, manageable. Diagonal is blanked (same
physical adapter can't simultaneously TX and RX with one driver). The
script reuses run_cell as-is; the addition is just the outer pair loop,
result dict keyed by (tx_side, rx_side, tx_vidpid, rx_vidpid), and a
new emit_full_markdown that renders four NxN tables.

Also scrubs personal identifiers from earlier docs/scripts (PR #33):
- tests/setup_vm.sh now reads VM_USER from $SUDO_USER / $USER instead
  of hardcoding a specific username
- tests/README.md + regress.py docstrings switch to <user>@<VM-IP>
  placeholders in example commands

Validation on a 3-adapter rig (Ubuntu 22.04 VM with aircrack-ng/88XXau,
0bda:8812 + 0bda:8813 + 2357:0120, channel 100, 10s/cell):

  ## Kernel-only (rig sanity)
  All 6 cross-chipset cells pass — 88XXau handles all three chipsets
  cleanly in the pinned-kernel VM (88-271 hits per cell).

  ## devourer-TX → kernel-RX
  devourer-TX confirmed for 8812 (4114, 4693 hits) AND 8821 (4341 hits
  reaching 8814 kernel RX). 8814 TX flaky after passthrough cycles
  (chip-state degradation across cell sequencing — known sensitive).

  ## kernel-TX → devourer-RX
  Surprise — devourer-RX 8821 caught 200 frames from kernel-TX 8814,
  contradicting PR #30's "RX silent" finding. devourer-RX 8812
  confirmed (100 hits from each of 8814, 8821 TX). devourer-RX 8814
  confirmed broken (0 hits all directions — known TODO).

  ## devourer ↔ devourer
  All 0 — every cell hits at least one broken side (8814 RX or 8814 TX
  degraded mid-run).

Net new product signal from the full matrix:
- devourer-TX 8821 actually works (was unvalidated since PR #30 had no
  peer sniffer in that session — VM mode is the peer)
- devourer-RX 8821 works under at least one TX condition — reopen PR
  #30's "RX silent" conclusion
- 8814 chip state degrades through repeated host↔VM passthrough — needs
  investigation, may want a chip reset between cells

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
josephnef added a commit that referenced this pull request May 23, 2026
## What this is

Extends `tests/regress.py` with `--full-matrix`: iterates every ordered
(TX, RX) pair of plugged-in DUTs across all 4 driver-side combinations
(kernel-only / dvr-TX×kernel-RX / kernel-TX×dvr-RX / devourer-only) and
emits one NxN table per mode. Useful for catching cross-chipset interop
regressions in PRs that touch shared HAL code.

```bash
sudo python3 tests/regress.py --full-matrix --channel 100 \
    --vm-name devourer-testrig --vm-ssh <user>@<VM-IP>
```

For N adapters → N×(N-1)×4 cells. ~16 min for N=3 in VM mode. Diagonal
blanked (same physical adapter can't simultaneously TX and RX with one
driver).

Also scrubs placeholder usernames from earlier docs/scripts
(`tests/setup_vm.sh` now reads `VM_USER` from `$SUDO_USER`/`$USER`
instead of hardcoding; READMEs + docstrings use `<user>@<VM-IP>`).

## Validation on a 3-adapter rig

Ubuntu 22.04 VM with aircrack-ng/88XXau, DUTs `0bda:8812 + 0bda:8813 +
2357:0120`, channel 100, 10s/cell:

### Kernel-only (rig sanity / cross-chipset kernel interop)

| TX \ RX | RTL8814AU | RTL8812AU | RTL8821AU |
|---|---|---|---|
| RTL8814AU | — | 99 hits ✓ | 271 hits ✓ |
| RTL8812AU | 243 hits ✓ | — | 270 hits ✓ |
| RTL8821AU | 260 hits ✓ | 88 hits ✓ | — |

**6/6 cells pass.** `88XXau` in the pinned-kernel VM gives full
cross-chipset kernel-side interop. Rig is sound.

### devourer-TX → kernel-RX (does devourer emit valid frames?)

| TX \ RX | RTL8814AU | RTL8812AU | RTL8821AU |
|---|---|---|---|
| RTL8814AU | — | 0 ✗ (2272 fail) | 0 ✗ (2322 fail) |
| RTL8812AU | **4114 ✓** | — | **4693 ✓** |
| RTL8821AU | **4341 ✓** | 0 ✗ | — |

- **devourer-TX 8812 validated** end-to-end at the wire — 4000+ hits to
each kernel RX peer
- **devourer-TX 8821 validated end-to-end** — 4341 hits reaching
kernel-RX 8814. New result: PR #30 had no peer sniffer to confirm; the
VM gives us one. The TX path that #30 wired works.
- 8814 TX flaky after the chip cycles through host↔VM passthroughs
(chip-state degradation across cell sequencing — single-cell tests in PR
#33 worked, this is a multi-cell wear issue)

### kernel-TX → devourer-RX (does devourer RX a known-good frame?)

| TX \ RX | RTL8814AU | RTL8812AU | RTL8821AU |
|---|---|---|---|
| RTL8814AU | — | **100 ✓** | **200 ✓** |
| RTL8812AU | 0 ✗ | — | 0 ✗ |
| RTL8821AU | 0 ✗ | **100 ✓** | — |

- **Surprise: devourer-RX 8821 caught 200 frames** from kernel-TX 8814 —
contradicts PR #30's "RX silent" finding. Reopen-worthy: 8821 RX may
actually be working under certain conditions and #30's session-local
conclusion was wrong.
- devourer-RX 8812 confirmed working (100 hits from each of 8814 and
8821 TX)
- devourer-RX 8814 confirmed broken — pre-existing TODO

### devourer ↔ devourer (end-to-end devourer)

All 0. Every cell hits at least one broken side (8814 RX broken or 8814
TX degraded mid-run).

## Net new product signal

1. **devourer-TX 8821 is working** — closes the gap PR #30 documented as
"wired but unvalidated"
2. **devourer-RX 8821 may actually be working** — at least one cell got
200 hits. PR #30's negative conclusion needs reopening.
3. **Chip state degrades through repeated passthrough** — devourer-TX
8814 is reliable in single-cell tests but flakes after several virsh
attach/detach cycles. Suggests we should consider a chip-reset /
power-cycle between cells in the orchestrator, or accept this as a known
limitation of the VM rig.

## What this PR doesn't touch

- The `--full-matrix` extension doesn't change any existing code path;
the single-pair mode (`--tx-pid`/`--rx-pid`) still works exactly as
before. Pure addition.
- Doesn't address the 8814 chip-state degradation (that's separate work
— possibly a `usbreset` between cells)
- Doesn't open the PR #30 reopen (that's a follow-up: rerun the 8821 RX
investigation with this rig)

## Test plan

- [x] `--full-matrix` runs end-to-end against 3 adapters in VM mode
- [x] All 4 NxN tables render correctly
- [x] Cell counts and per-pair timing match expected (~30-40s per cell)
- [x] Kernel-only baseline (6 cells) all pass
- [x] At least one cell in each non-trivial mode passes (validates
harness logic)
- [ ] Run on a different rig with different chipset combinations
- [ ] Validate on 4+ adapters (script supports it; not exercised here)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant