test(01): persist human verification items as UAT

This commit is contained in:
Mikkel Georgsen 2026-04-10 05:27:29 +00:00
parent 28e2f5a879
commit b4fc5c5f08
4 changed files with 247 additions and 5 deletions

View file

@ -35,11 +35,11 @@ Decimal phases appear between their surrounding integers in numeric order.
**Plans**: 5 plans
Plans:
- [ ] 01-01-PLAN.md — Go scaffold: chi server, go:embed stub SPA, viper config, health endpoint
- [ ] 01-02-PLAN.md — NetBox client: go-netbox v4 wrapper, custom field read/write types, integration tests
- [ ] 01-03-PLAN.md — NetBox provisioning: 8 custom fields, location hierarchy, plugin check, provision CLI
- [ ] 01-04-PLAN.md — HW-ID allocation, quality gate state machine, AI tag sync to NetBox
- [ ] 01-05-PLAN.md — Write-ahead queue: DragonFlyDB WAQ + retry worker goroutine
- [x] 01-01-PLAN.md — Go scaffold: chi server, go:embed stub SPA, viper config, health endpoint
- [x] 01-02-PLAN.md — NetBox client: go-netbox v4 wrapper, custom field read/write types, integration tests
- [x] 01-03-PLAN.md — NetBox provisioning: 8 custom fields, location hierarchy, plugin check, provision CLI
- [x] 01-04-PLAN.md — HW-ID allocation, quality gate state machine, AI tag sync to NetBox
- [x] 01-05-PLAN.md — Write-ahead queue: DragonFlyDB WAQ + retry worker goroutine
### Phase 2: AI Pipeline
**Goal**: Users can submit 1-3 photos and receive a structured NetBox-ready record with AI-extracted specs, suggested category/tags, and a quality gate status reflecting confidence

View file

@ -0,0 +1,47 @@
---
plan: 01-01
phase: 01-foundation
status: complete
started: 2026-04-10
completed: 2026-04-10
---
# Plan 01-01 Summary: Go Scaffold
## Outcome
All tasks completed successfully. Go binary scaffold with chi HTTP server, viper config, health endpoint, and embedded stub React SPA.
## Tasks
| # | Task | Status | Commits |
|---|------|--------|---------|
| 1 | Go module init, chi server, go:embed SPA scaffold | ✓ | 77e5a78 |
| 2 | Viper config loader and SPA fallback fix | ✓ | 6595e34 |
## Key Files
### Created
- `cmd/hwlab/main.go` — Entry point, starts chi server
- `internal/api/router.go` — Chi router with health endpoint and SPA fallback
- `internal/api/handlers/health.go` — Health check handler
- `internal/config/config.go` — Viper config loader (.env + JSON + env vars)
- `config.json` — Default config file
- `web/dist/index.html` — Stub React SPA placeholder
- `assets.go` — go:embed directives for web/dist
- `Makefile` — Build targets
- `go.mod`, `go.sum` — Go module with chi v5
### Tests
- `internal/api/handlers/health_test.go` — Health endpoint test
- `internal/config/config_test.go` — Config loader tests
## Test Results
```
ok git.georgsen.dk/hwlab/internal/api/handlers 0.003s
ok git.georgsen.dk/hwlab/internal/config 0.003s
```
## Deviations
None — executed as planned.
## Self-Check: PASSED

View file

@ -0,0 +1,36 @@
---
status: partial
phase: 01-foundation
source: [01-VERIFICATION.md]
started: 2026-04-10
updated: 2026-04-10
---
## Current Test
[awaiting human testing]
## Tests
### 1. NetBox provisioning (NB-02, NB-04)
expected: Get real 40-char NetBox token from http://10.5.0.130:8000/, set in .env, run `go run scripts/provision-netbox.go` — custom fields + location hierarchy provisioned
result: [pending]
### 2. netbox-inventory plugin installation (NB-03)
expected: SSH to LXC 130, verify `pip show netbox-inventory` works. Install if missing.
result: [pending]
### 3. NetBox integration tests (NB-01 round-trip)
expected: With real token + HWLAB_TEST_DEVICE_ID env var, run `go test ./internal/netbox/... -v` — round-trip custom field writes verified
result: [pending]
## Summary
total: 3
passed: 0
issues: 0
pending: 3
skipped: 0
blocked: 0
## Gaps

View file

@ -0,0 +1,159 @@
---
phase: 01-foundation
verified: 2026-04-10T08:00:00Z
status: human_needed
score: 5/5 must-haves verified
overrides_applied: 0
human_verification:
- test: "Run `go run scripts/provision-netbox.go` with a real 40-character NetBox API token set as HWLAB_NETBOX_TOKEN"
expected: "Script completes with log output showing 8 custom fields created (or already existing) and Site/Location/Rack hierarchy created. Verify via `curl -H 'Authorization: Token <token>' http://10.5.0.130:8000/api/extras/custom-fields/` that all 8 HWLab fields exist."
why_human: "Requires a real NetBox API token. Token in .env is a placeholder (22 chars, not 40). Cannot verify live provisioning programmatically."
- test: "SSH to LXC 130, run `pip show netbox-inventory` to verify the netbox-inventory plugin is installed (NB-03)"
expected: "Output shows netbox-inventory package with a version number. If not installed, run `pip install netbox-inventory` and restart NetBox."
why_human: "Plugin installation on LXC 130 is a manual infrastructure step that cannot be verified from the Go codebase. NB-03 has no automated test equivalent."
- test: "Obtain a real NetBox token, set HWLAB_NETBOX_TOKEN, and run `go test ./internal/netbox/... ./internal/queue/... -v` to exercise integration tests"
expected: "TestPingLive, TestListDevicesLive, TestWAQEnqueueDequeue all PASS (not SKIP). TestPatchCustomFieldsRoundTrip PASS when HWLAB_TEST_DEVICE_ID is also set."
why_human: "Integration tests are correctly guarded by token length check (skip if not 40 chars). DragonFlyDB integration (WAQ) was confirmed live; NetBox integration requires real token."
---
# Phase 1: Foundation Verification Report
**Phase Goal:** The Go binary connects to NetBox with all custom fields provisioned and a write-ahead queue buffering operations during downtime
**Verified:** 2026-04-10T08:00:00Z
**Status:** human_needed
**Re-verification:** No — initial verification
## Goal Achievement
### Observable Truths (ROADMAP Success Criteria)
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | Running Go binary serves a health endpoint and embeds a stub React SPA | VERIFIED | `go build ./...` exits 0; health.go returns `{"status":"ok","version":"0.1.0"}`; assets.go has `//go:embed web/dist`; router.go passes fs.FS to NewRouter; web/dist/index.html contains "HWLab" |
| 2 | All HWLab custom fields are readable and writable via NetBox API with round-trip test coverage | VERIFIED (unit) | `internal/netbox/custom_fields.go`: ParseCustomFields, BuildCustomFieldsPatch, BuildFullCustomFieldsPatch, PatchCustomFields all implemented; unit tests pass (5 tests); integration round-trip skips gracefully on placeholder token — requires human to confirm against live NetBox |
| 3 | A new item can be created in NetBox with a sequential HW-XXXXX ID auto-assigned | VERIFIED | `internal/netbox/hwid.go`: AllocateNextHWID implemented with optimistic-lock retry, getHighestHWIDNumber, hwIDExists; unit tests pass (10 cases via TestFormatHWID + TestParseHWID) |
| 4 | catalog_status transitions from draft through complete are enforced by the backend quality gate | VERIFIED | `internal/inventory/quality_gate.go`: validTransitions map, CatalogStatus.CanTransitionTo, Transition(); `internal/inventory/catalog_updater.go`: UpdateCatalogStatus calls Transition() then PatchCustomFields — enforcement is wired; 12+4 unit tests pass |
| 5 | A write-ahead queue in DragonFlyDB buffers failed NetBox operations and retries them on reconnect | VERIFIED | `internal/queue/waq.go`: Enqueue/Dequeue/Len via RPUSH/BLPOP; worker.go: RunWorker with backoff and max-attempts drop; main.go: non-fatal WAQ init wired with `go waq.RunWorker(ctx, ...)`; DragonFlyDB integration test PASSED live (TestWAQEnqueueDequeue 0.02s) |
**Score: 5/5 truths verified**
### Deferred Items
None.
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `cmd/hwlab/main.go` | Binary entry point | VERIFIED | Wires config, WAQ, server; graceful shutdown via signal.NotifyContext |
| `internal/api/router.go` | Chi router | VERIFIED | Accepts fs.FS param; /api routes; SPA fallback handler |
| `internal/api/handlers/health.go` | GET /api/health | VERIFIED | Returns `{"status":"ok","version":"0.1.0"}` |
| `internal/config/config.go` | Viper config | VERIFIED | HWLAB_ env prefix; BindEnv for all fields; JSON + .env loading |
| `web/dist/index.html` | Stub SPA | VERIFIED | Exists; contains "HWLab" |
| `assets.go` | go:embed directive | VERIFIED | `//go:embed web/dist`; StaticFiles passed into NewRouter |
| `go.mod` | Module + deps | VERIFIED | Module: git.georgsen.dk/hwlab; chi v5.2.5; netbox v4.3.0; go-redis v9.18.0; viper v1.21.0 |
| `internal/netbox/client.go` | NetBox client wrapper | VERIFIED | NewClient, Ping, ListDevices, GetDevice; strips /api suffix from URL |
| `internal/netbox/custom_fields.go` | Custom field helpers | VERIFIED | ParseCustomFields, BuildCustomFieldsPatch, BuildFullCustomFieldsPatch, PatchCustomFields — all real API calls |
| `internal/netbox/types.go` | Domain types | VERIFIED | Device, CustomFields structs |
| `internal/netbox/provision.go` | Provisioning | VERIFIED | ProvisionCustomFields, ProvisionLocationHierarchy, createCustomField, ensureSite/Location/Rack — no stubs |
| `scripts/provision-netbox.go` | Provision CLI | VERIFIED | `//go:build ignore`; calls Provision() + CheckNetBoxInventoryPlugin() |
| `internal/netbox/hwid.go` | HW-ID allocation | VERIFIED | AllocateNextHWID with 3-attempt retry, getHighestHWIDNumber, hwIDExists |
| `internal/inventory/quality_gate.go` | State machine | VERIFIED | validTransitions map; CatalogStatus.CanTransitionTo; Transition; ParseCatalogStatus |
| `internal/inventory/types.go` | HardwareRecord | VERIFIED | Composes netbox.CustomFields + CatalogStatus |
| `internal/inventory/catalog_updater.go` | Quality gate persistence | VERIFIED | UpdateCatalogStatus calls Transition() then PatchCustomFields |
| `internal/netbox/tags.go` | AI tag sync | VERIFIED | normalizeTags, tagNameToSlug, SyncTags, ensureTag — uses real ExtrasTagsCreate API |
| `internal/queue/waq.go` | WAQ core | VERIFIED | RPUSH/BLPOP FIFO; parseRedisURL with slash-in-password workaround; Enqueue/Dequeue/Len/Close |
| `internal/queue/worker.go` | WAQ worker | VERIFIED | RunWorker with context cancellation, backoff, max-attempts drop; NoOpHandler placeholder |
### Key Link Verification
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| cmd/hwlab/main.go | internal/api/router.go | NewRouter(staticFS) | WIRED | Line 45: `router := api.NewRouter(staticFS)` |
| assets.go | web/dist | `//go:embed web/dist` | WIRED | StaticFiles var; passed as fs.FS to NewRouter |
| internal/netbox/client.go | NetBox API | nb.NewAPIClientFor | WIRED | Line 32; URL /api suffix stripped correctly |
| internal/netbox/custom_fields.go | internal/netbox/client.go | PatchCustomFields method on Client | WIRED | Method on *Client; SetCustomFields + DcimDevicesPartialUpdate |
| scripts/provision-netbox.go | internal/netbox/provision.go | Provision(client) | WIRED | `client.Provision(ctx)` called directly |
| internal/inventory/catalog_updater.go | internal/netbox/client.go | PatchCustomFields("catalog_status") | WIRED | Line 34: `u.client.PatchCustomFields(ctx, deviceID, patch)` |
| internal/queue/waq.go | DragonFlyDB:6379 | go-redis ParseURL + NewClient | WIRED | parseRedisURL regex fallback handles slash-in-password; RPush/BLPop confirmed live |
| cmd/hwlab/main.go | internal/queue/worker.go | `go waq.RunWorker(ctx)` | WIRED | Line 40: goroutine started after successful WAQ init |
### Data-Flow Trace (Level 4)
Not applicable for this phase — no dynamic data rendering components. All artifacts are backend services (HTTP server, queue, NetBox client). The health endpoint returns static data by design.
### Behavioral Spot-Checks
| Behavior | Result | Status |
|----------|--------|--------|
| `go build ./...` | Exit 0, no errors | PASS |
| `go test ./...` | 5 packages: api/handlers OK, config OK, inventory OK, netbox OK, queue OK | PASS |
| `go vet ./...` | (confirmed via SUMMARY 01-03 and 01-05) | PASS |
| WAQ live integration (TestWAQEnqueueDequeue) | PASS 0.02s — DragonFlyDB at 10.5.0.10:6379 reachable | PASS |
| NetBox integration tests | SKIP — placeholder token (correct guard behavior) | SKIP (expected) |
### Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|-------------|-------------|--------|----------|
| INF-01 | 01-01 | Go binary serves React SPA via go:embed | SATISFIED | assets.go + router.go + web/dist/index.html; `go build` green |
| INF-02 | 01-01 | Config via JSON file + environment variables | SATISFIED | config.go: viper with HWLAB_ prefix, BindEnv, config.json defaults |
| INF-03 | 01-04 | HW-XXXXX sequential ID auto-assigned at intake | SATISFIED | hwid.go: AllocateNextHWID with optimistic-lock retry; unit tests pass |
| NB-01 | 01-02 | NetBox REST API CRUD on devices | SATISFIED | client.go: NewClient, Ping, ListDevices, GetDevice; PatchCustomFields in custom_fields.go |
| NB-02 | 01-02, 01-03 | 8 custom fields provisioned (hw_id, catalog_status, photo_urls, etc.) | SATISFIED (code) / NEEDS HUMAN (live) | provision.go: all 8 field specs defined; createCustomField real API; idempotent check-before-create. Actual provisioning requires real token. |
| NB-03 | 01-03 | netbox-inventory plugin installed | NEEDS HUMAN | CheckNetBoxInventoryPlugin() can verify via API; actual plugin installation on LXC 130 is a manual step |
| NB-04 | 01-03 | Location hierarchy (Site → Location → Rack) | SATISFIED (code) / NEEDS HUMAN (live) | ProvisionLocationHierarchy: ensureSite/ensureLocation/ensureRack all implemented with real go-netbox v4 API calls |
| NB-05 | 01-05 | Write-ahead queue in DragonFlyDB | SATISFIED | waq.go + worker.go; live integration test PASSED; main.go non-fatal wiring verified |
| NB-06 | 01-04 | Catalog quality gate enforced | SATISFIED | quality_gate.go: validTransitions; Transition() enforced; catalog_updater.go wires to PatchCustomFields |
| NB-07 | 01-04 | AI tags synced to NetBox tag system | SATISFIED | tags.go: SyncTags, ensureTag with real ExtrasTagsCreate; normalizeTags deduplication tested |
### Anti-Patterns Found
No blockers detected.
| File | Pattern | Severity | Assessment |
|------|---------|----------|------------|
| internal/queue/worker.go: NoOpHandler | Placeholder handler logs and drains queue | Info | Intentional Phase 1 placeholder; Phase 2 replaces with real NetBox retry handler. Documented in SUMMARY. Not a blocker. |
| go.mod: `// indirect` on netbox and redis | Cosmetic — packages are directly imported | Info | `go mod tidy` would fix. Packages compile and link correctly; no functional impact. |
### Human Verification Required
#### 1. NetBox Custom Fields and Location Hierarchy (NB-02, NB-04)
**Test:** Obtain a real 40-character NetBox API token from http://10.5.0.130:8000/ (Admin → API Tokens → Add). Set `HWLAB_NETBOX_TOKEN=<token>` in `.env`. Run `go run scripts/provision-netbox.go`.
**Expected:** Log output shows custom fields created/skipped (8 total) and location hierarchy created ("Homelab" site, "Lab Bench" location, "Primary Rack" rack). Verify with: `curl -s -H "Authorization: Token <token>" "http://10.5.0.130:8000/api/extras/custom-fields/" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['count'])"` should print 8 or more.
**Why human:** Requires real NetBox token. Placeholder in .env is `homelab-netbox-api-token-2024` (22 chars, not 40). All code is verified — provisioning script and functions are fully implemented. This is operator action to run the already-built tool.
#### 2. netbox-inventory Plugin (NB-03)
**Test:** SSH to LXC 130 (`ssh root@10.5.0.130` or equivalent). Run `pip show netbox-inventory`.
**Expected:** Output shows `Name: netbox-inventory` with a version string. If not installed: `pip install netbox-inventory`, then restart NetBox (`systemctl restart netbox` or `supervisorctl restart netbox:*`).
**Why human:** Plugin installation is an SSH/admin operation on the NetBox LXC container. `CheckNetBoxInventoryPlugin()` in provision.go can verify presence via the API, but initial installation cannot be automated from the Go codebase.
#### 3. NetBox Integration Tests (NB-01 round-trip)
**Test:** With real token set in HWLAB_NETBOX_TOKEN and an existing device ID in HWLAB_TEST_DEVICE_ID: `go test ./internal/netbox/... -v`.
**Expected:** TestPingLive PASS, TestListDevicesLive PASS, TestPatchCustomFieldsRoundTrip PASS. All 3 currently skip cleanly with placeholder token — correct behavior.
**Why human:** Requires real NetBox token. Skip guards are correctly implemented: `if len(token) != 40 { t.Skip(...) }`.
### Gaps Summary
No automated gaps. All code artifacts exist, are substantive (not stubs), and are correctly wired. The `go build ./...` and `go test ./...` suite passes cleanly with 0 failures.
Three items require human operator action before the phase goal is fully realized in the live environment:
1. Running the provisioning script with a real NetBox token to materialize the 8 custom fields and location hierarchy in NetBox
2. Verifying or installing the netbox-inventory plugin on LXC 130
3. Running integration tests to confirm live NetBox connectivity end-to-end
These are operational prerequisites, not code gaps. The code is complete and correct.
---
_Verified: 2026-04-10T08:00:00Z_
_Verifier: Claude (gsd-verifier)_