--- phase: 05-cable-test-integration plan: "01" type: tdd wave: 1 depends_on: [] files_modified: - internal/tester/driver.go - internal/tester/mock_drivers.go - internal/tester/driver_test.go - internal/usb/device.go autonomous: true requirements: [CBL-01, CBL-02, CBL-03, CBL-04, CBL-07] must_haves: truths: - "TesterDriver interface exists with Connect/Read/Disconnect/StreamLive methods" - "TestResult struct captures all cable fields (type, version, speed, power, continuity, eMarker, resistance)" - "MockUSBDriver, MockDPDriver, MockHDMIDriver return deterministic test data without hardware" - "MockFNB58Driver emits a stream of LiveReading samples via a channel" - "KnownDevices in usb/device.go has placeholder entries for all 4 testers (RoleCableTester)" - "All mock drivers satisfy the TesterDriver interface at compile time" artifacts: - path: "internal/tester/driver.go" provides: "TesterDriver interface, TestResult, LiveReading, CableType constants" exports: [TesterDriver, TestResult, LiveReading, CableType] - path: "internal/tester/mock_drivers.go" provides: "MockUSBDriver, MockDPDriver, MockHDMIDriver, MockFNB58Driver" - path: "internal/tester/driver_test.go" provides: "Tests for all four mock drivers" - path: "internal/usb/device.go" provides: "KnownDevices entries for all four tester VID:PIDs" key_links: - from: "internal/usb/device.go KnownDevices" to: "internal/tester/driver.go CableType" via: "VID:PID keys map to CableType constants for auto-detection" - from: "MockFNB58Driver.Stream()" to: "chan LiveReading" via: "channel closed on Disconnect()" --- Create the `internal/tester` package: TesterDriver interface, TestResult/LiveReading types, four mock implementations (TreedixUSB/DP/HDMI + FNB58), and VID:PID placeholder entries in the USB device registry. Purpose: All cable test code builds and tests pass without hardware. Real driver implementations are drop-in replacements when hardware arrives 2026-04-13. Output: internal/tester package (driver.go + mock_drivers.go + driver_test.go), updated internal/usb/device.go. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/04-usb-manager-label-printing/04-01-SUMMARY.md @.planning/phases/05-cable-test-integration/05-CONTEXT.md @internal/usb/device.go @internal/printer/driver.go From internal/usb/device.go: ```go type DeviceRole int const ( RolePrinter DeviceRole = iota RoleCableTester // reserved for Phase 5 RoleUnknown ) type DeviceSpec struct { VID string PID string Name string Role DeviceRole BaudRate int } // KnownDevices — add 4 tester entries here (placeholder VID:PIDs) var KnownDevices = map[string]DeviceSpec{ "0525:a4a7": {VID: "0525", PID: "a4a7", Name: "PRT Qutie", Role: RolePrinter, BaudRate: 9600}, // TODO Phase 5: Treedix testers placeholder entries go here } ``` From internal/printer/driver.go (pattern to follow): ```go type PrinterDriver interface { Connect() error Print(bitmap []byte, width, height int) error Disconnect() error } ``` Task 1: TesterDriver interface, TestResult types, mock discrete drivers (USB/DP/HDMI) internal/tester/driver.go, internal/tester/mock_drivers.go, internal/tester/driver_test.go, internal/usb/device.go - TestResult.CableType must be one of: CableTypeUSB, CableTypeDP, CableTypeHDMI - MockUSBDriver.Read() returns TestResult{CableType: CableTypeUSB, USBVersion: "USB 3.2 Gen 2", SpeedGbps: 10.0, MaxWatts: 100, PinContinuity: true, HasEMarker: true, ResistanceOhm: 0.12} - MockDPDriver.Read() returns TestResult{CableType: CableTypeDP, DPVersion: "1.4", PinContinuity: true} - MockHDMIDriver.Read() returns TestResult{CableType: CableTypeHDMI, HDMIVersion: "2.1", PinContinuity: true} - Read() before Connect() returns ErrNotConnected - Disconnect() twice returns nil (idempotent) - Compile-time interface assertion: `var _ TesterDriver = (*MockUSBDriver)(nil)` etc. - KnownDevices in usb/device.go has entries for "dead0:0001" (TreedixUSB), "dead0:0002" (TreedixDP), "dead0:0003" (TreedixHDMI) — all Role: RoleCableTester RED: Write driver_test.go first — compile-time assertions + behavior tests for all three mocks. Run: `go test ./internal/tester/... ./internal/usb/...` — must fail (package does not exist). GREEN: Create internal/tester/driver.go: ```go package tester import "errors" var ErrNotConnected = errors.New("tester: not connected — call Connect() first") type CableType int const ( CableTypeUSB CableType = iota CableTypeDP CableTypeHDMI ) type TestResult struct { CableType CableType USBVersion string DPVersion string HDMIVersion string SpeedGbps float64 MaxWatts int PinContinuity bool HasEMarker bool ResistanceOhm float64 } type TesterDriver interface { Connect() error Read() (TestResult, error) Disconnect() error } ``` Create internal/tester/mock_drivers.go with MockUSBDriver, MockDPDriver, MockHDMIDriver structs. Each has a `connected bool` field. Connect() sets connected=true. Disconnect() sets connected=false (idempotent). Read() returns ErrNotConnected if !connected, else returns the deterministic TestResult shown in the behavior block. Update internal/usb/device.go KnownDevices map: add three Treedix entries with placeholder VID:PIDs "dead0:0001", "dead0:0002", "dead0:0003", Role: RoleCableTester, BaudRate: 115200. Add comment: `// TODO(hardware): Replace placeholder VID:PIDs after characterization 2026-04-13`. Commit RED then GREEN separately. cd /home/mikkel/homelabby && go test ./internal/tester/... ./internal/usb/... -v -count=1 -run "TestMock|TestKnown" All mock discrete driver tests pass; KnownDevices has 4 entries (1 printer + 3 testers); go build ./... passes. Task 2: FNB58 streaming mock driver internal/tester/driver.go, internal/tester/mock_drivers.go, internal/tester/driver_test.go - LiveReading struct: Voltage float64, CurrentAmps float64, PowerWatts float64, PDProtocol string, Timestamp time.Time - StreamingTesterDriver interface embeds TesterDriver and adds: Stream() <-chan LiveReading - MockFNB58Driver.Stream() returns a channel that emits 3 deterministic LiveReading samples (V=5.1, A=3.0, W=15.3, PDProtocol="PD3.0") at 100ms intervals then closes - Stream() before Connect() returns a closed channel (not nil, not blocking) - Disconnect() closes the stream channel and sets connected=false - Compile-time: `var _ StreamingTesterDriver = (*MockFNB58Driver)(nil)` - KnownDevices has "dead0:0004" for FNB58 (Role: RoleCableTester, BaudRate: 0 = HID, not serial) RED: Extend driver_test.go with FNB58-specific tests. Run tests — fail on missing types. GREEN: Add to internal/tester/driver.go: ```go type LiveReading struct { Voltage float64 CurrentAmps float64 PowerWatts float64 PDProtocol string Timestamp time.Time } type StreamingTesterDriver interface { TesterDriver Stream() <-chan LiveReading } ``` Add MockFNB58Driver to mock_drivers.go. Connect() sets connected=true, initialises internal streamCh (buffered 8). Disconnect() closes streamCh if open, sets connected=false. Stream() returns streamCh (or a pre-closed channel if !connected). In Connect(), launch a goroutine that sends 3 readings 100ms apart then closes the channel — use a context derived from a cancelFunc stored on the struct so Disconnect() can also stop it early. Add "dead0:0004" to KnownDevices: {VID:"dead0", PID:"0004", Name:"FNIRSI FNB58", Role:RoleCableTester, BaudRate:0} with `// TODO(hardware): BaudRate 0 = HID protocol — replace VID:PID after characterization`. Commit RED then GREEN. cd /home/mikkel/homelabby && go test ./internal/tester/... -v -count=1 -run "TestFNB58|TestStream" -timeout 30s FNB58 streaming tests pass; Stream() emits 3 readings and closes; Disconnect() stops stream; go build ./... passes; race detector clean (add -race flag). ## Trust Boundaries | Boundary | Description | |----------|-------------| | USB serial → tester package | Raw bytes from hardware (not applicable for mocks; real drivers must validate) | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-05-01 | Tampering | TestResult deserialization (future real drivers) | accept | Mock phase only — add input validation when real drivers are implemented | | T-05-02 | DoS | Stream() channel never closed | mitigate | MockFNB58Driver closes channel after N readings; Disconnect() closes channel; goroutine cancelled via context | ``` go build ./... # must pass go test ./internal/tester/... -race -count=1 # all pass, race-clean go test ./internal/usb/... -count=1 # existing tests still pass grep -c "dead0:" internal/usb/device.go # must print 4 ``` - internal/tester package exists and compiles - TesterDriver and StreamingTesterDriver interfaces defined - MockUSBDriver, MockDPDriver, MockHDMIDriver: deterministic Read(), correct CableType - MockFNB58Driver: Stream() emits readings, closes on Disconnect(), race-clean - KnownDevices has 4 placeholder entries (3 Treedix + 1 FNB58), all Role: RoleCableTester - All existing tests (phases 1-4) continue to pass After completion, create `.planning/phases/05-cable-test-integration/05-01-SUMMARY.md`