Description
Implement a comprehensive test suite covering all internal packages. The strategy prioritizes determinism (no real hardware, no real network), speed (table-driven unit tests), and confidence (golden files catch config regressions).
Interfaces Required (define before writing tests)
// internal/runner/runner.go
type Runner interface {
Run(ctx context.Context, name string, args ...string) (Output, error)
}
type Output struct { Stdout, Stderr string; ExitCode int }
// internal/bakery/client.go
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
// internal/probe/prober.go
type DiskProber interface { ListDisks(ctx context.Context) ([]Disk, error) }
type NetworkProber interface { ListInterfaces(ctx context.Context) ([]Interface, error) }
// internal/install/installer.go
type Installer interface { Install(ctx context.Context, config InstallConfig) error }
Per-Package Test Plan
internal/runner
TestRunner_Execute_ReturnsStdout — captures stdout from real command
TestRunner_Execute_ReturnsStderr — captures stderr separately
TestRunner_Execute_NonZeroExit — returns structured error with exit code
TestRunner_DryRun_LogsCommand — dry-run records command without executing
TestRunner_DryRun_DoesNotMutateSystem — no side effects
TestRunner_Execute_Timeout — context cancellation kills child process
TestRunner_Execute_BinaryNotFound — wraps exec.ErrNotFound
TestRunner_CommandLog_OrderPreserved — dry-run log order
internal/probe
TestProbe_Disks_ParsesLsblkJSON — parses fixture (NVMe, SATA, USB)
TestProbe_Disks_FiltersByType — excludes loop, rom, lvm
TestProbe_Disks_EmptyOutput — no disks → empty slice, no error
TestProbe_Disks_MalformedJSON — returns parse error
TestProbe_Disks_ByIDPath — maps /dev/sda → /dev/disk/by-id/...
TestProbe_Network_ParsesIPLink — parses fixture (eth0, wlan0, lo)
TestProbe_Network_FiltersLoopback — excludes lo
TestProbe_Network_NoInterfaces — returns empty, no error
internal/bakery
TestBakery_FetchCatalog_Success — parses valid catalog JSON (httptest)
TestBakery_FetchCatalog_HTTPError — 404/500 returns typed error
TestBakery_FetchCatalog_Timeout — context deadline exceeded
TestBakery_FetchCatalog_MalformedJSON — decode error with body prefix
TestBakery_FetchCatalog_EmptyCatalog — zero sysexts is valid
TestBakery_FilterByArch_AMD64 — filters entries by architecture
TestBakery_SysextURL_Constructs — builds download URL from entry
internal/ignition (golden file tests)
TestIgnition_Assemble_MinimalConfig — hostname + disk → valid Butane
TestIgnition_Assemble_WithSSHKeys — SSH keys in passwd section
TestIgnition_Assemble_WithSysexts — sysext units and downloads
TestIgnition_Assemble_WithStaticIP — network unit rendered
TestIgnition_Assemble_FlatcarVariant — output contains variant: flatcar
TestIgnition_Compile_GoldenMinimal — golden file comparison
TestIgnition_Compile_GoldenFull — golden file comparison (all options)
TestIgnition_ExternalURL_Passthrough — URL mode skips local generation
TestIgnition_ExternalURL_MutualExclusion — URL + local → validation error
internal/validate (table-driven)
TestValidate_Hostname_Valid / _Empty / _TooLong / _InvalidChars
TestValidate_IPv4_ValidCIDR / _InvalidCIDR / _OutOfRange
TestValidate_Gateway_ValidIPv4 / _WithCIDR / _OutsideSubnet
TestValidate_SSHKey_ValidEd25519 / _ValidRSA / _Empty / _MalformedPrefix
TestValidate_DiskPath_ValidByID / _RawDevice / _NonExistent
TestValidate_ConfigConsistency_StaticIPRequiresGateway
TestValidate_ConfigConsistency_DHCPIgnoresStaticFields
internal/wizard (state machine)
TestWizard_InitialState — starts at step 0
TestWizard_Next_AdvancesStep / _BlockedByValidation
TestWizard_Back_DecrementsStep / _AtFirstStep
TestWizard_Summary_AllFields — shows all collected data
TestWizard_Confirm_TriggersInstall — invokes install
TestWizard_ExternalURL_SkipsConfigSteps
TestWizard_StateTransitions_Full — complete happy path
internal/install
TestInstall_Execute_HappyPath — correct flatcar-install args
TestInstall_Execute_DryRun — logs without execution
TestInstall_Execute_DiskByID — uses /dev/disk/by-id/ path
TestInstall_Execute_CommandFailure — wraps runner error
TestInstall_Execute_MissingBinary — clear error message
cmd/knuckle
TestCLI_DryRunFlag_SetsRunner
TestCLI_VersionFlag_Prints
TestCLI_HelpFlag_ShowsUsage
TestCLI_InvalidFlag_Exits1
Test Fixtures (testdata/)
testdata/
├── probe/
│ ├── lsblk_nvme_sata_usb.json # Mixed disk types
│ ├── lsblk_single_nvme.json # Single NVMe
│ ├── lsblk_empty.json # No blockdevices
│ ├── lsblk_with_loops.json # Loop devices to filter
│ ├── lsblk_malformed.json # Truncated JSON
│ ├── ip_link_eth_wlan.json # Multiple interfaces
│ ├── ip_link_single_eth.json # Single wired NIC
│ └── ip_link_empty.json # No interfaces
├── bakery/
│ ├── catalog_full.json # Complete sysext catalog
│ ├── catalog_empty.json # Empty array
│ └── catalog_malformed.json # Invalid JSON
├── ignition/golden/
│ ├── minimal.ign # Hostname + disk + SSH key
│ ├── full.ign # All options
│ ├── static_ip.ign # Static network
│ ├── sysexts.ign # With extensions
│ └── dhcp.ign # DHCP networking
└── install/
└── flatcar_install_help.txt # For arg verification
Critical Edge Cases
- Disk with no serial → by-id path construction fails gracefully
- SSH key with YAML special chars (
#, :) → no YAML injection in Butane
- IPv6 when only IPv4 expected → wrong address family rejected
- CIDR /0 or /32 → technically valid but flagged as suspicious
- Gateway outside subnet → config is syntactically valid but won't route
- Catalog response >10MB → enforce read limit (DoS protection)
- Rapid back/next in wizard → no state corruption
- No disks found → error screen, no crash
- Sysext name with URL-unsafe chars → URL encoding in download path
Coverage Targets
| Package |
Target |
internal/validate |
≥80% |
internal/ignition |
≥80% |
internal/probe |
≥80% |
internal/runner |
≥80% |
internal/bakery |
≥70% |
internal/install |
≥70% |
internal/wizard |
≥60% |
Implementation Priority
internal/validate — pure functions, highest immediate value
internal/runner — foundation; mock must work for all other tests
internal/probe — fixture-driven, catches parsing regressions
internal/ignition — golden files catch config regressions (machine-bricking risk)
internal/bakery — httptest pattern
internal/install — depends on runner mock
internal/wizard — integration-heavy, depends on all mocks
cmd/knuckle — thin layer, lowest risk
Build Tags & Running
go test ./... # Unit tests only (fast)
go test -tags=integration ./... # Include integration tests
go test ./internal/ignition -update # Regenerate golden files
go test -race ./... # Race detector for wizard state
Acceptance Criteria
Description
Implement a comprehensive test suite covering all internal packages. The strategy prioritizes determinism (no real hardware, no real network), speed (table-driven unit tests), and confidence (golden files catch config regressions).
Interfaces Required (define before writing tests)
Per-Package Test Plan
internal/runnerTestRunner_Execute_ReturnsStdout— captures stdout from real commandTestRunner_Execute_ReturnsStderr— captures stderr separatelyTestRunner_Execute_NonZeroExit— returns structured error with exit codeTestRunner_DryRun_LogsCommand— dry-run records command without executingTestRunner_DryRun_DoesNotMutateSystem— no side effectsTestRunner_Execute_Timeout— context cancellation kills child processTestRunner_Execute_BinaryNotFound— wraps exec.ErrNotFoundTestRunner_CommandLog_OrderPreserved— dry-run log orderinternal/probeTestProbe_Disks_ParsesLsblkJSON— parses fixture (NVMe, SATA, USB)TestProbe_Disks_FiltersByType— excludes loop, rom, lvmTestProbe_Disks_EmptyOutput— no disks → empty slice, no errorTestProbe_Disks_MalformedJSON— returns parse errorTestProbe_Disks_ByIDPath— maps /dev/sda → /dev/disk/by-id/...TestProbe_Network_ParsesIPLink— parses fixture (eth0, wlan0, lo)TestProbe_Network_FiltersLoopback— excludes loTestProbe_Network_NoInterfaces— returns empty, no errorinternal/bakeryTestBakery_FetchCatalog_Success— parses valid catalog JSON (httptest)TestBakery_FetchCatalog_HTTPError— 404/500 returns typed errorTestBakery_FetchCatalog_Timeout— context deadline exceededTestBakery_FetchCatalog_MalformedJSON— decode error with body prefixTestBakery_FetchCatalog_EmptyCatalog— zero sysexts is validTestBakery_FilterByArch_AMD64— filters entries by architectureTestBakery_SysextURL_Constructs— builds download URL from entryinternal/ignition(golden file tests)TestIgnition_Assemble_MinimalConfig— hostname + disk → valid ButaneTestIgnition_Assemble_WithSSHKeys— SSH keys in passwd sectionTestIgnition_Assemble_WithSysexts— sysext units and downloadsTestIgnition_Assemble_WithStaticIP— network unit renderedTestIgnition_Assemble_FlatcarVariant— output containsvariant: flatcarTestIgnition_Compile_GoldenMinimal— golden file comparisonTestIgnition_Compile_GoldenFull— golden file comparison (all options)TestIgnition_ExternalURL_Passthrough— URL mode skips local generationTestIgnition_ExternalURL_MutualExclusion— URL + local → validation errorinternal/validate(table-driven)TestValidate_Hostname_Valid/_Empty/_TooLong/_InvalidCharsTestValidate_IPv4_ValidCIDR/_InvalidCIDR/_OutOfRangeTestValidate_Gateway_ValidIPv4/_WithCIDR/_OutsideSubnetTestValidate_SSHKey_ValidEd25519/_ValidRSA/_Empty/_MalformedPrefixTestValidate_DiskPath_ValidByID/_RawDevice/_NonExistentTestValidate_ConfigConsistency_StaticIPRequiresGatewayTestValidate_ConfigConsistency_DHCPIgnoresStaticFieldsinternal/wizard(state machine)TestWizard_InitialState— starts at step 0TestWizard_Next_AdvancesStep/_BlockedByValidationTestWizard_Back_DecrementsStep/_AtFirstStepTestWizard_Summary_AllFields— shows all collected dataTestWizard_Confirm_TriggersInstall— invokes installTestWizard_ExternalURL_SkipsConfigStepsTestWizard_StateTransitions_Full— complete happy pathinternal/installTestInstall_Execute_HappyPath— correct flatcar-install argsTestInstall_Execute_DryRun— logs without executionTestInstall_Execute_DiskByID— uses /dev/disk/by-id/ pathTestInstall_Execute_CommandFailure— wraps runner errorTestInstall_Execute_MissingBinary— clear error messagecmd/knuckleTestCLI_DryRunFlag_SetsRunnerTestCLI_VersionFlag_PrintsTestCLI_HelpFlag_ShowsUsageTestCLI_InvalidFlag_Exits1Test Fixtures (
testdata/)Critical Edge Cases
#,:) → no YAML injection in ButaneCoverage Targets
internal/validateinternal/ignitioninternal/probeinternal/runnerinternal/bakeryinternal/installinternal/wizardImplementation Priority
internal/validate— pure functions, highest immediate valueinternal/runner— foundation; mock must work for all other testsinternal/probe— fixture-driven, catches parsing regressionsinternal/ignition— golden files catch config regressions (machine-bricking risk)internal/bakery— httptest patterninternal/install— depends on runner mockinternal/wizard— integration-heavy, depends on all mockscmd/knuckle— thin layer, lowest riskBuild Tags & Running
Acceptance Criteria
_test.gowith listed test functionsgo test ./...passes with zero failuresgo test -race ./...passes (no data races)-updateflag works//go:build integrationgo test -race ./...on every push