mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
7996848b64
A paying operator asked the Assistant to reboot five Proxmox VMs matching a name pattern (GitHub #1782, support mail 2026-08-26 and 2026-08-29). The model resolved the VMs and then ended with a report that invented a prerequisite: a QEMU guest agent on 6.3.2, a "discovery binding" on stable 6.4.0. It never planned the action. Three defects made the governed path fail whenever the model did try it, and nothing refused the prose ending when it did not: - pulse_control handed the session-scoped id (vm:<node>:<vmid>) to the action lifecycle, whose registry keys on canonical unified ids, so a Proxmox guest plan could never resolve. - pulse_control gated the action on the legacy per-executor action list, which never carried the canonical "reboot" capability Proxmox guests advertise, so "reboot" was refused as not permitted before planning. - A reference absent from the session context was refused with "resource discovery is required" even when the unified inventory resolved it. pulse_control now binds its target to the canonical unified resource (session alias first, then a unique inventory match, refusing ambiguity with candidate ids and naming the pulse_query recovery on a miss), passes the canonical id to the planner, and answers "not available" only from the resource's current advertised capabilities. The FSM ordering block and the shared operating instructions state that a recoverable block is not a limitation to report, and the instructions require the governed action tool for advertised capabilities. The agentic loop adds a bounded advertised-action gate: when the operator asked for a lifecycle action, pulse_control was offered but never submitted, and a session-resolved resource advertises the action, a tool-free final answer is refused once with the exact per-target calls. Covered by tools and loop unit tests (the #1782 transcript against a scripted provider fails on the previous code with the two exact errors above), a prompt-contract test, and the live eval scenario ProxmoxBulkLifecycleActionScenario.
236 lines
6.4 KiB
Go
236 lines
6.4 KiB
Go
package eval
|
|
|
|
import (
|
|
"flag"
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
var runLiveEval = flag.Bool("live", false, "Run live eval against Pulse API (requires running Pulse)")
|
|
|
|
func TestMain(m *testing.M) {
|
|
flag.Parse()
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
// TestQuickSmokeTest runs a minimal smoke test against the live API
|
|
// Run with: go test -v ./internal/ai/eval -run TestQuickSmokeTest -live
|
|
func TestQuickSmokeTest(t *testing.T) {
|
|
if !*runLiveEval {
|
|
t.Skip("Skipping live eval test. Use -live flag to run against live Pulse API")
|
|
}
|
|
|
|
runner := NewRunner(DefaultConfig())
|
|
scenario := QuickSmokeTest()
|
|
|
|
result := runner.RunScenario(scenario)
|
|
runner.PrintSummary(result)
|
|
|
|
if !result.Passed {
|
|
t.Fatalf("Scenario '%s' failed", scenario.Name)
|
|
}
|
|
}
|
|
|
|
// TestReadOnlyInfrastructure runs the full read-only infrastructure scenario
|
|
// Run with: go test -v ./internal/ai/eval -run TestReadOnlyInfrastructure -live
|
|
func TestReadOnlyInfrastructure(t *testing.T) {
|
|
if !*runLiveEval {
|
|
t.Skip("Skipping live eval test. Use -live flag to run against live Pulse API")
|
|
}
|
|
|
|
runner := NewRunner(DefaultConfig())
|
|
scenario := ReadOnlyInfrastructureScenario()
|
|
|
|
result := runner.RunScenario(scenario)
|
|
runner.PrintSummary(result)
|
|
|
|
if !result.Passed {
|
|
t.Fatalf("Scenario '%s' failed", scenario.Name)
|
|
}
|
|
}
|
|
|
|
// TestRoutingValidation runs the routing validation scenario
|
|
// Run with: go test -v ./internal/ai/eval -run TestRoutingValidation -live
|
|
func TestRoutingValidation(t *testing.T) {
|
|
if !*runLiveEval {
|
|
t.Skip("Skipping live eval test. Use -live flag to run against live Pulse API")
|
|
}
|
|
|
|
runner := NewRunner(DefaultConfig())
|
|
scenario := RoutingValidationScenario()
|
|
|
|
result := runner.RunScenario(scenario)
|
|
runner.PrintSummary(result)
|
|
|
|
if !result.Passed {
|
|
t.Fatalf("Scenario '%s' failed", scenario.Name)
|
|
}
|
|
}
|
|
|
|
// TestLogTailing runs the log tailing scenario
|
|
// Run with: go test -v ./internal/ai/eval -run TestLogTailing -live
|
|
func TestLogTailing(t *testing.T) {
|
|
if !*runLiveEval {
|
|
t.Skip("Skipping live eval test. Use -live flag to run against live Pulse API")
|
|
}
|
|
|
|
runner := NewRunner(DefaultConfig())
|
|
scenario := LogTailingScenario()
|
|
|
|
result := runner.RunScenario(scenario)
|
|
runner.PrintSummary(result)
|
|
|
|
if !result.Passed {
|
|
t.Fatalf("Scenario '%s' failed", scenario.Name)
|
|
}
|
|
}
|
|
|
|
// TestDiscovery runs the infrastructure discovery scenario
|
|
// Run with: go test -v ./internal/ai/eval -run TestDiscovery -live
|
|
func TestDiscovery(t *testing.T) {
|
|
if !*runLiveEval {
|
|
t.Skip("Skipping live eval test. Use -live flag to run against live Pulse API")
|
|
}
|
|
|
|
runner := NewRunner(DefaultConfig())
|
|
scenario := DiscoveryScenario()
|
|
|
|
result := runner.RunScenario(scenario)
|
|
runner.PrintSummary(result)
|
|
|
|
if !result.Passed {
|
|
t.Fatalf("Scenario '%s' failed", scenario.Name)
|
|
}
|
|
}
|
|
|
|
// TestAllScenarios runs all defined scenarios
|
|
// Run with: go test -v ./internal/ai/eval -run TestAllScenarios -live
|
|
func TestAllScenarios(t *testing.T) {
|
|
if !*runLiveEval {
|
|
t.Skip("Skipping live eval test. Use -live flag to run against live Pulse API")
|
|
}
|
|
|
|
runner := NewRunner(DefaultConfig())
|
|
|
|
scenarios := []Scenario{
|
|
QuickSmokeTest(),
|
|
ReadOnlyInfrastructureScenario(),
|
|
RoutingValidationScenario(),
|
|
LogTailingScenario(),
|
|
DiscoveryScenario(),
|
|
ProxmoxBulkLifecycleActionScenario(),
|
|
}
|
|
|
|
allPassed := true
|
|
for _, scenario := range scenarios {
|
|
t.Run(scenario.Name, func(t *testing.T) {
|
|
result := runner.RunScenario(scenario)
|
|
runner.PrintSummary(result)
|
|
|
|
if !result.Passed {
|
|
allPassed = false
|
|
t.Errorf("Scenario '%s' failed", scenario.Name)
|
|
}
|
|
})
|
|
}
|
|
|
|
if !allPassed {
|
|
t.Fatal("One or more scenarios failed")
|
|
}
|
|
}
|
|
|
|
// TestPatrolBasic runs the basic patrol eval scenario
|
|
// Run with: go test -v ./internal/ai/eval -run TestPatrolBasic -live
|
|
func TestPatrolBasic(t *testing.T) {
|
|
if !*runLiveEval {
|
|
t.Skip("Skipping live eval test. Use -live flag to run against live Pulse API")
|
|
}
|
|
|
|
runner := NewRunner(DefaultConfig())
|
|
result := runner.RunPatrolScenario(PatrolBasicScenario())
|
|
runner.PrintPatrolSummary(result)
|
|
|
|
if !result.Success {
|
|
t.Fatalf("Patrol scenario '%s' failed", "Patrol Basic Run")
|
|
}
|
|
}
|
|
|
|
// TestPatrolInvestigation runs the patrol investigation quality scenario
|
|
// Run with: go test -v ./internal/ai/eval -run TestPatrolInvestigation -live
|
|
func TestPatrolInvestigation(t *testing.T) {
|
|
if !*runLiveEval {
|
|
t.Skip("Skipping live eval test. Use -live flag to run against live Pulse API")
|
|
}
|
|
|
|
runner := NewRunner(DefaultConfig())
|
|
result := runner.RunPatrolScenario(PatrolInvestigationScenario())
|
|
runner.PrintPatrolSummary(result)
|
|
|
|
if !result.Success {
|
|
t.Fatalf("Patrol scenario '%s' failed", "Patrol Investigation Quality")
|
|
}
|
|
}
|
|
|
|
// TestPatrolFindingQuality runs the patrol finding quality scenario
|
|
// Run with: go test -v ./internal/ai/eval -run TestPatrolFindingQuality -live
|
|
func TestPatrolFindingQuality(t *testing.T) {
|
|
if !*runLiveEval {
|
|
t.Skip("Skipping live eval test. Use -live flag to run against live Pulse API")
|
|
}
|
|
|
|
runner := NewRunner(DefaultConfig())
|
|
result := runner.RunPatrolScenario(PatrolFindingQualityScenario())
|
|
runner.PrintPatrolSummary(result)
|
|
|
|
if !result.Success {
|
|
t.Fatalf("Patrol scenario '%s' failed", "Patrol Finding Quality")
|
|
}
|
|
}
|
|
|
|
// TestAllPatrolScenarios runs all patrol eval scenarios
|
|
// Run with: go test -v ./internal/ai/eval -run TestAllPatrolScenarios -live
|
|
func TestAllPatrolScenarios(t *testing.T) {
|
|
if !*runLiveEval {
|
|
t.Skip("Skipping live eval test. Use -live flag to run against live Pulse API")
|
|
}
|
|
|
|
runner := NewRunner(DefaultConfig())
|
|
|
|
allPassed := true
|
|
for _, scenario := range AllPatrolScenarios() {
|
|
t.Run(scenario.Name, func(t *testing.T) {
|
|
result := runner.RunPatrolScenario(scenario)
|
|
runner.PrintPatrolSummary(result)
|
|
|
|
if !result.Success {
|
|
allPassed = false
|
|
t.Errorf("Patrol scenario '%s' failed", scenario.Name)
|
|
}
|
|
})
|
|
}
|
|
|
|
if !allPassed {
|
|
t.Fatal("One or more patrol scenarios failed")
|
|
}
|
|
}
|
|
|
|
// TestProxmoxBulkLifecycleAction runs the issue #1782 regression: a bulk VM
|
|
// reboot request must end in governed pulse_control plans, not a report.
|
|
// Run with: go test -v ./internal/ai/eval -run TestProxmoxBulkLifecycleAction -live
|
|
// Set EVAL_LIFECYCLE_PATTERN and EVAL_LIFECYCLE_TARGET_COUNT for the estate.
|
|
func TestProxmoxBulkLifecycleAction(t *testing.T) {
|
|
if !*runLiveEval {
|
|
t.Skip("Skipping live eval test. Use -live flag to run against live Pulse API")
|
|
}
|
|
|
|
runner := NewRunner(DefaultConfig())
|
|
scenario := ProxmoxBulkLifecycleActionScenario()
|
|
|
|
result := runner.RunScenario(scenario)
|
|
runner.PrintSummary(result)
|
|
|
|
if !result.Passed {
|
|
t.Fatalf("Scenario '%s' failed", scenario.Name)
|
|
}
|
|
}
|