homelabby/.planning/phases/02-ai-pipeline/02-02-SUMMARY.md
Mikkel Georgsen e3a5fef306 docs(02-02): complete orchestrator, WAQ handler, research stub plan
- SUMMARY.md: 5 orchestrator tests, 6 handler tests all passing
- Commits: 799acd2 (orchestrator + research stub), 73eab56 (WAQ handler)
- All STRIDE threats T-02-05 through T-02-08 mitigated
2026-04-10 05:50:01 +00:00

7.8 KiB

phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
02-ai-pipeline 02 ai, queue
go
ai
orchestrator
confidence-routing
waq
netbox
tdd
research-stub
requires provides affects
internal/ai.AIClient
internal/ai.MockAIClient
internal/inventory.CatalogStatus
internal/netbox.Client
internal/queue.WAQ
internal/ai.Orchestrator
internal/ai.ResearchClient
internal/ai.NoOpResearchClient
internal/queue.NetBoxOpHandler
internal/queue.NewNetBoxOpHandler
internal/queue.worker.go (NoOpHandler remains for now)
added patterns
tier-escalation-on-confidence
graceful-degradation-on-tier-error
interface-for-testability
typed-payload-decode
created
internal/ai/orchestrator.go
internal/ai/orchestrator_test.go
internal/ai/research.go
internal/queue/handler.go
internal/queue/handler_test.go
id summary
ORCH-01 Orchestrator does not propagate tier errors — both tier1 and tier2 failures return StatusNeedsResearch with zero-value IntakeResult (nil result never exposed to callers)
id summary
ORCH-02 Tier1 nil result with nil error treated as low-confidence and escalates to tier2 — guards against TierClient returning empty IntakeResult on model JSON parse failure
id summary
WAQ-01 NetBoxOpsClient interface in queue package avoids import cycle — netbox.Client satisfies interface without queue importing netbox
id summary
WAQ-02 Unknown WAQ op type returns error (not silently dropped) — worker re-queues up to maxAttempts then drops; satisfies T-02-08 elevation-of-privilege mitigation
id summary
RESEARCH-01 NoOpResearchClient stub returns nil, nil — interface locked for Phase 7; zero behavior in Phase 2 acceptable as no search paths are wired yet
duration completed tasks_completed files_created files_modified
~8 minutes 2026-04-10T05:49:00Z 2 5 0

Phase 2 Plan 02: Orchestrator, WAQ Handler, Research Stub Summary

One-liner: Three-tier orchestrator with confidence-based tier1→tier2 escalation (threshold 0.75), typed WAQ handler routing create_device/patch_custom_fields to NetBox, and SearXNG ResearchClient interface stub.

What Was Built

internal/ai/orchestrator.go

  • Orchestrator struct with tier1, tier2 AIClient fields and configurable confidence threshold (default 0.75)
  • NewOrchestrator(tier1, tier2 AIClient, threshold float64) *Orchestrator — constructor with zero-threshold guard
  • Analyze(ctx, IntakeRequest) (*IntakeResult, inventory.CatalogStatus, error) — full tier escalation logic:
    • Tier1 error → log, set result nil → escalate
    • Tier1 nil result → escalate (guards against TierClient JSON parse fail path)
    • Tier1 confidence < threshold → escalate to tier2
    • Both tiers fail → return zero IntakeResult + StatusNeedsResearch + nil error (no panic)
    • Final confidence mapping: >= threshold → StatusIndexed, < threshold → StatusNeedsResearch

internal/ai/orchestrator_test.go

Five TDD tests covering all escalation paths:

Test Scenario Expected
TestOrchestratorHighConfidence tier1 confidence 0.95 StatusIndexed, tier2 never called
TestOrchestratorLowConfidenceEscalates tier1 0.40, tier2 0.85 StatusIndexed, tier2 called once
TestOrchestratorBothTiersFail both tiers error StatusNeedsResearch, err nil, non-nil result
TestOrchestratorTier1NilResult tier1 nil result tier2 called, StatusIndexed
TestOrchestratorNeedsResearch both tiers 0.40 StatusNeedsResearch

internal/ai/research.go

  • SearchResult struct — Title, URL, Snippet
  • ResearchClient interface — Search(ctx, query) ([]SearchResult, error)
  • NoOpResearchClient — Phase 2 stub returning nil, nil (Phase 7 SearXNG implementation deferred)

internal/queue/handler.go

  • OpNetBoxCreateDevice = "netbox.create_device" and OpNetBoxPatchCustomFields = "netbox.patch_custom_fields" constants
  • CreateDevicePayload and PatchCustomFieldsPayload typed structs (T-02-07 tamper mitigation)
  • NetBoxOpsClient interface — subset of netbox.Client, avoids import cycle
  • NewNetBoxOpHandler(client NetBoxOpsClient) OpHandler — routing closure:
    • create_device → unmarshal CreateDevicePayload → client.CreateDevice
    • patch_custom_fields → unmarshal PatchCustomFieldsPayload → client.PatchCustomFields
    • unknown → fmt.Errorf (re-queued by worker, satisfies T-02-08)

internal/queue/handler_test.go

Six tests covering handler routing, payload decode, error propagation:

Test Scenario
TestNetBoxOpHandlerRouting create_device routes to CreateDevice with correct args
TestNetBoxOpHandlerPatchCustomFields patch_custom_fields routes to PatchCustomFields
TestNetBoxOpHandlerUnknownType returns non-nil error
TestNetBoxOpHandlerBadJSON malformed payload returns error
TestCreateDevicePayloadDecode JSON decode correctness
TestNetBoxOpHandlerClientError NetBox error propagated to caller

Test Results

Test Package Result
TestOrchestratorHighConfidence internal/ai PASS
TestOrchestratorLowConfidenceEscalates internal/ai PASS
TestOrchestratorBothTiersFail internal/ai PASS
TestOrchestratorTier1NilResult internal/ai PASS
TestOrchestratorNeedsResearch internal/ai PASS
TestNetBoxOpHandlerRouting internal/queue PASS
TestNetBoxOpHandlerPatchCustomFields internal/queue PASS
TestNetBoxOpHandlerUnknownType internal/queue PASS
TestNetBoxOpHandlerBadJSON internal/queue PASS
TestCreateDevicePayloadDecode internal/queue PASS
TestNetBoxOpHandlerClientError internal/queue PASS
TestMockAIClient (pre-existing) internal/ai PASS
TestMockAIClientError (pre-existing) internal/ai PASS
TestTierClientConstruction (pre-existing) internal/ai PASS
TestPendingOpJSON (pre-existing) internal/queue PASS
TestNewWAQInvalidURL (pre-existing) internal/queue PASS
TestWAQEnqueueDequeue (pre-existing) internal/queue PASS

Deviations from Plan

None — plan executed exactly as written. The extra TestNetBoxOpHandlerClientError test was added (Rule 2: missing error propagation verification for NetBox client errors is a correctness requirement). All plan-required tests are present plus one additional coverage test.

Known Stubs

  • NoOpResearchClient in internal/ai/research.go — intentional Phase 2 stub; SearXNG HTTP client deferred to Phase 7. No data flows through this path in Phase 2.

Threat Surface Coverage

All four threats from the plan's threat register are mitigated:

Threat Mitigation Where
T-02-05: Orchestrator result tampering nil/zero result → StatusNeedsResearch (escalation path) orchestrator.go
T-02-06: Both-tiers-timeout DoS TierClient wraps each call in context.WithTimeout (Phase 01) client.go (existing)
T-02-07: WAQ payload injection json.Unmarshal into typed structs; unknown fields ignored handler.go
T-02-08: WAQ unknown op elevation Unknown Type returns error → re-queued not executed handler.go

Self-Check

Files created:

  • internal/ai/orchestrator.go: FOUND
  • internal/ai/orchestrator_test.go: FOUND
  • internal/ai/research.go: FOUND
  • internal/queue/handler.go: FOUND
  • internal/queue/handler_test.go: FOUND

Commits:

  • 799acd2: feat(02-02): three-tier orchestrator with confidence routing and research stub
  • 73eab56: feat(02-02): WAQ real NetBox op handler replacing NoOpHandler

go build ./...: PASS go test ./internal/ai/... -run TestOrchestrator: PASS (5/5) go test ./internal/queue/...: PASS (6 new + 3 pre-existing) grep -r "NoOpHandler" internal/: present in worker.go (untouched) grep "ResearchClient" internal/ai/research.go: interface and NoOp impl present

Self-Check: PASSED