homelabby/.planning/phases/05-cable-test-integration/05-01-PLAN.md
2026-04-10 07:04:57 +00:00

246 lines
10 KiB
Markdown

---
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()"
---
<objective>
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.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
</context>
<interfaces>
<!-- Key types from existing packages that tester package must integrate with. -->
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
}
```
</interfaces>
<tasks>
<task type="tdd" tdd="true">
<name>Task 1: TesterDriver interface, TestResult types, mock discrete drivers (USB/DP/HDMI)</name>
<files>internal/tester/driver.go, internal/tester/mock_drivers.go, internal/tester/driver_test.go, internal/usb/device.go</files>
<behavior>
- 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
</behavior>
<action>
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.
</action>
<verify>
<automated>cd /home/mikkel/homelabby && go test ./internal/tester/... ./internal/usb/... -v -count=1 -run "TestMock|TestKnown"</automated>
</verify>
<done>All mock discrete driver tests pass; KnownDevices has 4 entries (1 printer + 3 testers); go build ./... passes.</done>
</task>
<task type="tdd" tdd="true">
<name>Task 2: FNB58 streaming mock driver</name>
<files>internal/tester/driver.go, internal/tester/mock_drivers.go, internal/tester/driver_test.go</files>
<behavior>
- 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)
</behavior>
<action>
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.
</action>
<verify>
<automated>cd /home/mikkel/homelabby && go test ./internal/tester/... -v -count=1 -run "TestFNB58|TestStream" -timeout 30s</automated>
</verify>
<done>FNB58 streaming tests pass; Stream() emits 3 readings and closes; Disconnect() stops stream; go build ./... passes; race detector clean (add -race flag).</done>
</task>
</tasks>
<threat_model>
## 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 |
</threat_model>
<verification>
```
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
```
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/05-cable-test-integration/05-01-SUMMARY.md`
</output>