test(01): persist human verification items as UAT
This commit is contained in:
parent
28e2f5a879
commit
b4fc5c5f08
4 changed files with 247 additions and 5 deletions
|
|
@ -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
|
||||
|
|
|
|||
47
.planning/phases/01-foundation/01-01-SUMMARY.md
Normal file
47
.planning/phases/01-foundation/01-01-SUMMARY.md
Normal 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
|
||||
36
.planning/phases/01-foundation/01-HUMAN-UAT.md
Normal file
36
.planning/phases/01-foundation/01-HUMAN-UAT.md
Normal 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
|
||||
159
.planning/phases/01-foundation/01-VERIFICATION.md
Normal file
159
.planning/phases/01-foundation/01-VERIFICATION.md
Normal 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)_
|
||||
Loading…
Add table
Reference in a new issue