Files
pulse/internal/ai/chat/session_additional_test.go
T
2026-05-08 15:33:14 +01:00

1126 lines
37 KiB
Go

package chat
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
func TestSessionStore_KnowledgeAndToolSets(t *testing.T) {
store, err := NewSessionStore(t.TempDir())
if err != nil {
t.Fatalf("failed to create session store: %v", err)
}
session, err := store.Create()
if err != nil {
t.Fatalf("failed to create session: %v", err)
}
ka1 := store.GetKnowledgeAccumulator(session.ID)
ka2 := store.NewKnowledgeAccumulatorForRun(session.ID)
if ka1 == ka2 {
t.Fatalf("expected new knowledge accumulator for run")
}
toolSet := map[string]bool{"pulse_query": true}
store.SetToolSet(session.ID, toolSet)
got := store.GetToolSet(session.ID)
if got == nil || !got["pulse_query"] {
t.Fatalf("expected tool set entry")
}
got["pulse_query"] = false
if store.GetToolSet(session.ID)["pulse_query"] != true {
t.Fatalf("expected tool set to be copied")
}
updated := store.AddToolSet(session.ID, map[string]bool{"pulse_metrics": true})
if !updated["pulse_metrics"] {
t.Fatalf("expected tool set to include additions")
}
}
func TestSessionStore_ModelHandoffContextLifecycle(t *testing.T) {
store, err := NewSessionStore(t.TempDir())
if err != nil {
t.Fatalf("failed to create session store: %v", err)
}
session, err := store.Create()
if err != nil {
t.Fatalf("failed to create session: %v", err)
}
initial, err := store.GetModelHandoffContext(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffContext failed: %v", err)
}
if initial != "" {
t.Fatalf("initial handoff context = %q, want empty", initial)
}
initialFindingID, err := store.GetModelHandoffFindingID(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffFindingID failed: %v", err)
}
if initialFindingID != "" {
t.Fatalf("initial handoff finding ID = %q, want empty", initialFindingID)
}
if err := store.SetModelHandoffFindingID(session.ID, " finding-123 "); err != nil {
t.Fatalf("SetModelHandoffFindingID failed: %v", err)
}
gotFindingID, err := store.GetModelHandoffFindingID(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffFindingID failed: %v", err)
}
if gotFindingID != "finding-123" {
t.Fatalf("handoff finding ID = %q, want finding-123", gotFindingID)
}
handoffContext := " [Finding Context]\nID: finding-123\nConclusion: CPU saturated after backup. "
if err := store.SetModelHandoffContext(session.ID, handoffContext); err != nil {
t.Fatalf("SetModelHandoffContext failed: %v", err)
}
got, err := store.GetModelHandoffContext(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffContext failed: %v", err)
}
if got != strings.TrimSpace(handoffContext) {
t.Fatalf("handoff context = %q, want trimmed %q", got, strings.TrimSpace(handoffContext))
}
handoffResources := []HandoffResource{
{ID: " vm-100 ", Name: " web-server ", Type: " vm ", Node: " pve-1 "},
{ID: "vm-100", Name: "web-server", Type: "vm", Node: "pve-1"},
}
if err := store.SetModelHandoffResources(session.ID, handoffResources); err != nil {
t.Fatalf("SetModelHandoffResources failed: %v", err)
}
gotResources, err := store.GetModelHandoffResources(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffResources failed: %v", err)
}
if len(gotResources) != 1 {
t.Fatalf("handoff resources = %#v, want one normalized resource", gotResources)
}
if gotResources[0] != (HandoffResource{ID: "vm-100", Name: "web-server", Type: "vm", Node: "pve-1"}) {
t.Fatalf("handoff resource = %#v, want normalized VM", gotResources[0])
}
handoffActions := []HandoffAction{
{
FindingID: " finding-123 ",
RecordID: " record-123 ",
ApprovalID: " approval-123 ",
FixID: " fix-123 ",
Description: " Restart the workload service ",
RiskLevel: " medium ",
Destructive: true,
TargetHost: " pve-1 ",
TargetResourceID: " vm-100 ",
TargetResourceName: " web-server ",
TargetResourceType: " vm ",
TargetNode: " pve-1 ",
},
{
FindingID: "finding-123",
RecordID: "record-123",
ApprovalID: "approval-123",
FixID: "fix-123",
Description: "Restart the workload service",
RiskLevel: "medium",
Destructive: true,
TargetHost: "pve-1",
TargetResourceID: "vm-100",
TargetResourceName: "web-server",
TargetResourceType: "vm",
TargetNode: "pve-1",
},
}
if err := store.SetModelHandoffActions(session.ID, handoffActions); err != nil {
t.Fatalf("SetModelHandoffActions failed: %v", err)
}
gotActions, err := store.GetModelHandoffActions(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffActions failed: %v", err)
}
if len(gotActions) != 1 {
t.Fatalf("handoff actions = %#v, want one normalized action", gotActions)
}
if gotActions[0] != (HandoffAction{
FindingID: "finding-123",
RecordID: "record-123",
ApprovalID: "approval-123",
FixID: "fix-123",
Description: "Restart the workload service",
RiskLevel: "medium",
Destructive: true,
TargetHost: "pve-1",
TargetResourceID: "vm-100",
TargetResourceName: "web-server",
TargetResourceType: "vm",
TargetNode: "pve-1",
}) {
t.Fatalf("handoff action = %#v, want normalized pending action", gotActions[0])
}
reloadedStore, err := NewSessionStore(filepath.Dir(store.dataDir))
if err != nil {
t.Fatalf("failed to reload session store: %v", err)
}
reloadedResources, err := reloadedStore.GetModelHandoffResources(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffResources after reload failed: %v", err)
}
if len(reloadedResources) != 1 || reloadedResources[0].ID != "vm-100" {
t.Fatalf("reloaded handoff resources = %#v, want persisted VM reference", reloadedResources)
}
reloadedFindingID, err := reloadedStore.GetModelHandoffFindingID(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffFindingID after reload failed: %v", err)
}
if reloadedFindingID != "finding-123" {
t.Fatalf("reloaded handoff finding ID = %q, want finding-123", reloadedFindingID)
}
reloadedActions, err := reloadedStore.GetModelHandoffActions(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffActions after reload failed: %v", err)
}
if len(reloadedActions) != 1 || reloadedActions[0].ApprovalID != "approval-123" {
t.Fatalf("reloaded handoff actions = %#v, want persisted approval reference", reloadedActions)
}
if err := store.SetModelHandoffResources(session.ID, nil); err != nil {
t.Fatalf("SetModelHandoffResources clear failed: %v", err)
}
gotResources, err = store.GetModelHandoffResources(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffResources after resource clear failed: %v", err)
}
if len(gotResources) != 0 {
t.Fatalf("handoff resources after resource clear = %#v, want empty", gotResources)
}
got, err = store.GetModelHandoffContext(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffContext after resource clear failed: %v", err)
}
if got != strings.TrimSpace(handoffContext) {
t.Fatalf("handoff context after resource clear = %q, want retained context", got)
}
gotFindingID, err = store.GetModelHandoffFindingID(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffFindingID after resource clear failed: %v", err)
}
if gotFindingID != "finding-123" {
t.Fatalf("handoff finding ID after resource clear = %q, want retained finding reference", gotFindingID)
}
gotActions, err = store.GetModelHandoffActions(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffActions after resource clear failed: %v", err)
}
if len(gotActions) != 1 {
t.Fatalf("expected resource clear to retain handoff actions, got %#v", gotActions)
}
if err := store.SetModelHandoffActions(session.ID, nil); err != nil {
t.Fatalf("SetModelHandoffActions clear failed: %v", err)
}
gotActions, err = store.GetModelHandoffActions(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffActions after action clear failed: %v", err)
}
if len(gotActions) != 0 {
t.Fatalf("handoff actions after action clear = %#v, want empty", gotActions)
}
got, err = store.GetModelHandoffContext(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffContext after action clear failed: %v", err)
}
if got != strings.TrimSpace(handoffContext) {
t.Fatalf("handoff context after action clear = %q, want retained context", got)
}
if err := store.SetModelHandoffResources(session.ID, handoffResources); err != nil {
t.Fatalf("SetModelHandoffResources restore failed: %v", err)
}
if err := store.SetModelHandoffActions(session.ID, handoffActions); err != nil {
t.Fatalf("SetModelHandoffActions restore failed: %v", err)
}
if err := store.AddMessage(session.ID, Message{Role: "user", Content: "What happened?"}); err != nil {
t.Fatalf("AddMessage failed: %v", err)
}
messages, err := store.GetMessages(session.ID)
if err != nil {
t.Fatalf("GetMessages failed: %v", err)
}
if len(messages) != 1 || messages[0].Content != "What happened?" {
t.Fatalf("stored messages = %#v, want clean user prompt only", messages)
}
store.ClearSessionState(session.ID, true)
got, err = store.GetModelHandoffContext(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffContext after keep-pinned clear failed: %v", err)
}
if got == "" {
t.Fatalf("expected keep-pinned context clear to retain model handoff")
}
gotResources, err = store.GetModelHandoffResources(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffResources after keep-pinned clear failed: %v", err)
}
if len(gotResources) != 1 {
t.Fatalf("expected keep-pinned context clear to retain handoff resources, got %#v", gotResources)
}
gotActions, err = store.GetModelHandoffActions(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffActions after keep-pinned clear failed: %v", err)
}
if len(gotActions) != 1 {
t.Fatalf("expected keep-pinned context clear to retain handoff actions, got %#v", gotActions)
}
gotFindingID, err = store.GetModelHandoffFindingID(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffFindingID after keep-pinned clear failed: %v", err)
}
if gotFindingID != "finding-123" {
t.Fatalf("expected keep-pinned context clear to retain finding reference, got %q", gotFindingID)
}
store.ClearSessionState(session.ID, false)
got, err = store.GetModelHandoffContext(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffContext after full clear failed: %v", err)
}
if got != "" {
t.Fatalf("handoff context after full clear = %q, want empty", got)
}
gotResources, err = store.GetModelHandoffResources(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffResources after full clear failed: %v", err)
}
if len(gotResources) != 0 {
t.Fatalf("handoff resources after full clear = %#v, want empty", gotResources)
}
gotActions, err = store.GetModelHandoffActions(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffActions after full clear failed: %v", err)
}
if len(gotActions) != 0 {
t.Fatalf("handoff actions after full clear = %#v, want empty", gotActions)
}
gotFindingID, err = store.GetModelHandoffFindingID(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffFindingID after full clear failed: %v", err)
}
if gotFindingID != "" {
t.Fatalf("handoff finding ID after full clear = %q, want empty", gotFindingID)
}
}
func TestSessionStore_ListIncludesSafeHandoffSummary(t *testing.T) {
store, err := NewSessionStore(t.TempDir())
if err != nil {
t.Fatalf("failed to create session store: %v", err)
}
session, err := store.Create()
if err != nil {
t.Fatalf("failed to create session: %v", err)
}
if err := store.SetModelHandoffFindingID(session.ID, " finding-123 "); err != nil {
t.Fatalf("SetModelHandoffFindingID failed: %v", err)
}
if err := store.SetModelHandoffContext(session.ID, "[Finding Context]\nID: finding-123\nConclusion: CPU saturated after backup."); err != nil {
t.Fatalf("SetModelHandoffContext failed: %v", err)
}
if err := store.SetModelHandoffResources(session.ID, []HandoffResource{{
ID: " vm-100 ",
Name: " web-server ",
Type: " vm ",
Node: " pve-1 ",
}}); err != nil {
t.Fatalf("SetModelHandoffResources failed: %v", err)
}
if err := store.SetModelHandoffActions(session.ID, []HandoffAction{{
FindingID: " finding-123 ",
ApprovalID: " approval-123 ",
ApprovalStatus: " pending ",
ActionID: " action-123 ",
ActionState: " awaiting_approval ",
ActionRequiresApproval: true,
ActionPreflight: "systemctl restart web-server",
ActionResult: "restart succeeded",
Description: "Restart the workload service",
RiskLevel: " medium ",
TargetResourceID: " vm-100 ",
TargetResourceName: " web-server ",
TargetResourceType: " vm ",
TargetNode: " pve-1 ",
}}); err != nil {
t.Fatalf("SetModelHandoffActions failed: %v", err)
}
sessions, err := store.List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(sessions) != 1 {
t.Fatalf("sessions = %#v, want one session", sessions)
}
summary := sessions[0].HandoffSummary
if summary == nil {
t.Fatalf("expected handoff summary")
}
if summary.Kind != sessionHandoffKindPatrolFinding {
t.Fatalf("handoff kind = %q, want %q", summary.Kind, sessionHandoffKindPatrolFinding)
}
if summary.FindingID != "finding-123" {
t.Fatalf("finding ID = %q, want finding-123", summary.FindingID)
}
if !summary.HasModelContext {
t.Fatalf("expected summary to report model context")
}
if summary.ResourceCount != 1 || summary.PrimaryResource == nil || summary.PrimaryResource.ID != "vm-100" || summary.PrimaryResource.Name != "web-server" {
t.Fatalf("resource summary = %#v, want normalized primary resource", summary)
}
if summary.ActionCount != 1 || !summary.RequiresApproval {
t.Fatalf("action summary = %#v, want one approval-required action", summary)
}
if summary.LastKnownApprovalStatus != "pending" || summary.LastKnownActionState != "awaiting_approval" || summary.LastKnownActionRisk != "medium" {
t.Fatalf("action status summary = %#v, want normalized last-known status fields", summary)
}
retrieved, err := store.Get(session.ID)
if err != nil {
t.Fatalf("Get failed: %v", err)
}
if retrieved.HandoffSummary == nil || retrieved.HandoffSummary.FindingID != "finding-123" {
t.Fatalf("retrieved handoff summary = %#v, want same safe finding summary", retrieved.HandoffSummary)
}
payload, err := json.Marshal(sessions)
if err != nil {
t.Fatalf("Marshal sessions failed: %v", err)
}
publicJSON := string(payload)
for _, forbidden := range []string{
"CPU saturated",
"systemctl restart",
"restart succeeded",
"Restart the workload service",
} {
if strings.Contains(publicJSON, forbidden) {
t.Fatalf("public session JSON leaked %q: %s", forbidden, publicJSON)
}
}
}
func TestSessionStore_ListIncludesSafePatrolRunHandoffSummary(t *testing.T) {
store, err := NewSessionStore(t.TempDir())
if err != nil {
t.Fatalf("failed to create session store: %v", err)
}
session, err := store.Create()
if err != nil {
t.Fatalf("failed to create session: %v", err)
}
handoffContext := strings.Join([]string{
"[Patrol Run Context]",
"Source: Pulse Patrol run history",
"Run ID: run-runtime-error",
"Run Type: Scoped run",
"Status: error",
"Runtime Failure: Selected model does not support Patrol tools: No endpoints found that support the provided 'tool_choice' value.",
"Patrol Analysis: Runtime failure prevented analysis.",
}, "\n")
if err := store.SetModelHandoffEnvelope(session.ID, "", handoffContext, []HandoffResource{{
ID: " vm-100 ",
Type: " vm ",
}}, nil, HandoffMetadata{
Kind: " patrol_run ",
RunID: " run-runtime-error ",
RunType: " Scoped run ",
RunStatus: " error ",
RuntimeFailure: true,
}); err != nil {
t.Fatalf("SetModelHandoffEnvelope failed: %v", err)
}
metadata, err := store.GetModelHandoffMetadata(session.ID)
if err != nil {
t.Fatalf("GetModelHandoffMetadata failed: %v", err)
}
if metadata != (HandoffMetadata{
Kind: sessionHandoffKindPatrolRun,
RunID: "run-runtime-error",
RunType: "Scoped run",
RunStatus: "error",
RuntimeFailure: true,
}) {
t.Fatalf("handoff metadata = %#v, want normalized Patrol run identity", metadata)
}
sessions, err := store.List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(sessions) != 1 {
t.Fatalf("sessions = %#v, want one session", sessions)
}
summary := sessions[0].HandoffSummary
if summary == nil {
t.Fatalf("expected handoff summary")
}
if summary.Kind != sessionHandoffKindPatrolRun {
t.Fatalf("handoff kind = %q, want %q", summary.Kind, sessionHandoffKindPatrolRun)
}
if summary.FindingID != "" {
t.Fatalf("finding ID = %q, want empty Patrol run summary", summary.FindingID)
}
if summary.RunID != "run-runtime-error" || summary.RunType != "Scoped run" || summary.RunStatus != "error" || !summary.RuntimeFailure {
t.Fatalf("run summary = %#v, want safe Patrol run identity", summary)
}
if !summary.HasModelContext {
t.Fatalf("expected summary to report model context")
}
if summary.ResourceCount != 1 || summary.PrimaryResource == nil || summary.PrimaryResource.ID != "vm-100" {
t.Fatalf("resource summary = %#v, want normalized primary resource", summary)
}
if summary.ActionCount != 0 || summary.RequiresApproval {
t.Fatalf("action summary = %#v, want review-only run context without action authority", summary)
}
payload, err := json.Marshal(sessions)
if err != nil {
t.Fatalf("Marshal sessions failed: %v", err)
}
publicJSON := string(payload)
for _, forbidden := range []string{
"Selected model",
"No endpoints found",
"tool_choice",
"Runtime failure prevented analysis",
} {
if strings.Contains(publicJSON, forbidden) {
t.Fatalf("public session JSON leaked %q: %s", forbidden, publicJSON)
}
}
}
func TestSessionStore_ListKeepsPatrolAssessmentHandoffIdentity(t *testing.T) {
store, err := NewSessionStore(t.TempDir())
if err != nil {
t.Fatalf("failed to create session store: %v", err)
}
session, err := store.Create()
if err != nil {
t.Fatalf("failed to create session: %v", err)
}
handoffContext := strings.Join([]string{
"[Patrol Assessment Context]",
"Source: Pulse Patrol current assessment",
"Assessment: Coverage incomplete",
"Recommended Next Step: Verify full coverage",
"Recommended Next Step Detail: Run a full Patrol sweep before treating the assessment as current.",
"Recommended Next Step Action: Run Patrol (run_patrol)",
}, "\n")
if err := store.SetModelHandoffEnvelope(session.ID, "", handoffContext, nil, []HandoffAction{{
FindingID: "finding-123",
ApprovalID: "approval-123",
ApprovalStatus: "pending",
FixID: "fix-123",
Description: "Restart the workload service",
RiskLevel: "high",
TargetResourceID: "vm-100",
}}, HandoffMetadata{
Kind: "patrol_assessment",
}); err != nil {
t.Fatalf("SetModelHandoffEnvelope failed: %v", err)
}
sessions, err := store.List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(sessions) != 1 || sessions[0].HandoffSummary == nil {
t.Fatalf("sessions = %#v, want one session with handoff summary", sessions)
}
summary := sessions[0].HandoffSummary
if summary.Kind != sessionHandoffKindPatrolAssessment {
t.Fatalf("handoff kind = %q, want %q", summary.Kind, sessionHandoffKindPatrolAssessment)
}
if summary.FindingID != "" {
t.Fatalf("finding ID = %q, want assessment summary not pinned to one action finding", summary.FindingID)
}
if summary.ActionCount != 1 || !summary.RequiresApproval {
t.Fatalf("action summary = %#v, want assessment action posture preserved", summary)
}
if summary.LastKnownApprovalStatus != "pending" || summary.LastKnownActionRisk != "high" {
t.Fatalf("approval posture = %#v, want safe action summary", summary)
}
if summary.RecommendedNextStep != "Verify full coverage" ||
summary.RecommendedNextStepDetail != "Run a full Patrol sweep before treating the assessment as current." ||
summary.RecommendedNextStepAction != "Run Patrol" ||
summary.RecommendedNextStepActionKind != "run_patrol" {
t.Fatalf("recommended next step summary = %#v, want safe Patrol recommendation", summary)
}
payload, err := json.Marshal(sessions)
if err != nil {
t.Fatalf("Marshal sessions failed: %v", err)
}
publicJSON := string(payload)
for _, forbidden := range []string{
"Coverage incomplete",
"Restart the workload service",
"vm-100",
} {
if strings.Contains(publicJSON, forbidden) {
t.Fatalf("public session JSON leaked %q: %s", forbidden, publicJSON)
}
}
}
func TestSessionStore_ListKeepsSafePatrolFindingNextStepRoute(t *testing.T) {
store, err := NewSessionStore(t.TempDir())
if err != nil {
t.Fatalf("failed to create session store: %v", err)
}
session, err := store.Create()
if err != nil {
t.Fatalf("failed to create session: %v", err)
}
handoffContext := strings.Join([]string{
"[Patrol Finding Context]",
"Source: Pulse Patrol finding handoff",
"Finding ID: finding-provider-settings",
"Patrol Next Step: Open Patrol provider settings",
"Patrol Next Step Route: /settings/system-ai",
}, "\n")
if err := store.SetModelHandoffEnvelope(session.ID, "finding-provider-settings", handoffContext, nil, nil, HandoffMetadata{}); err != nil {
t.Fatalf("SetModelHandoffEnvelope failed: %v", err)
}
sessions, err := store.List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(sessions) != 1 || sessions[0].HandoffSummary == nil {
t.Fatalf("sessions = %#v, want one session with handoff summary", sessions)
}
summary := sessions[0].HandoffSummary
if summary.Kind != sessionHandoffKindPatrolFinding {
t.Fatalf("handoff kind = %q, want %q", summary.Kind, sessionHandoffKindPatrolFinding)
}
if summary.FindingID != "finding-provider-settings" {
t.Fatalf("finding ID = %q, want finding-provider-settings", summary.FindingID)
}
if summary.RecommendedNextStep != "Open Patrol provider settings" ||
summary.RecommendedNextStepAction != "Open Patrol provider settings" ||
summary.RecommendedNextStepActionHref != "/settings/system-ai" {
t.Fatalf("next step summary = %#v, want safe route-owned Patrol next step", summary)
}
}
func TestSessionStore_ListPrefersStructuredPatrolFindingNextStepMetadata(t *testing.T) {
store, err := NewSessionStore(t.TempDir())
if err != nil {
t.Fatalf("failed to create session store: %v", err)
}
session, err := store.Create()
if err != nil {
t.Fatalf("failed to create session: %v", err)
}
handoffContext := strings.Join([]string{
"[Patrol Finding Context]",
"Source: Pulse Patrol finding handoff",
"Finding ID: finding-provider-settings",
"Patrol Next Step: sudo restart",
"Patrol Next Step Route: javascript:alert(1)",
}, "\n")
if err := store.SetModelHandoffEnvelope(session.ID, "finding-provider-settings", handoffContext, nil, nil, HandoffMetadata{
Kind: "patrol_finding",
RecommendedNextStep: "Open Patrol provider settings",
RecommendedNextStepDetail: "Fix the Patrol runtime issue before continuing.",
RecommendedNextStepAction: "Open Patrol provider settings",
RecommendedNextStepActionHref: "/settings/system-ai",
}); err != nil {
t.Fatalf("SetModelHandoffEnvelope failed: %v", err)
}
sessions, err := store.List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(sessions) != 1 || sessions[0].HandoffSummary == nil {
t.Fatalf("sessions = %#v, want one session with handoff summary", sessions)
}
summary := sessions[0].HandoffSummary
if summary.RecommendedNextStep != "Open Patrol provider settings" ||
summary.RecommendedNextStepDetail != "Fix the Patrol runtime issue before continuing." ||
summary.RecommendedNextStepAction != "Open Patrol provider settings" ||
summary.RecommendedNextStepActionHref != "/settings/system-ai" {
t.Fatalf("next step summary = %#v, want structured safe Patrol next step", summary)
}
}
func TestSessionStore_ListWithholdsUnsafePatrolFindingNextStepRoute(t *testing.T) {
store, err := NewSessionStore(t.TempDir())
if err != nil {
t.Fatalf("failed to create session store: %v", err)
}
session, err := store.Create()
if err != nil {
t.Fatalf("failed to create session: %v", err)
}
handoffContext := strings.Join([]string{
"[Patrol Finding Context]",
"Source: Pulse Patrol finding handoff",
"Finding ID: finding-provider-settings",
"Patrol Next Step: Open Patrol provider settings",
"Patrol Next Step Route: javascript:alert(1)",
}, "\n")
if err := store.SetModelHandoffEnvelope(session.ID, "finding-provider-settings", handoffContext, nil, nil, HandoffMetadata{}); err != nil {
t.Fatalf("SetModelHandoffEnvelope failed: %v", err)
}
sessions, err := store.List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(sessions) != 1 || sessions[0].HandoffSummary == nil {
t.Fatalf("sessions = %#v, want one session with handoff summary", sessions)
}
summary := sessions[0].HandoffSummary
if summary.RecommendedNextStepAction != "Open Patrol provider settings" {
t.Fatalf("next step action = %q, want safe label preserved", summary.RecommendedNextStepAction)
}
if summary.RecommendedNextStepActionHref != "" {
t.Fatalf("next step href = %q, want unsafe route withheld", summary.RecommendedNextStepActionHref)
}
}
func TestSessionStore_ListWithholdsUnsafePatrolAssessmentRecommendationSummary(t *testing.T) {
store, err := NewSessionStore(t.TempDir())
if err != nil {
t.Fatalf("failed to create session store: %v", err)
}
session, err := store.Create()
if err != nil {
t.Fatalf("failed to create session: %v", err)
}
handoffContext := strings.Join([]string{
"[Patrol Assessment Context]",
"Source: Pulse Patrol current assessment",
"Recommended Next Step: Run sudo systemctl restart workload.service",
"Recommended Next Step Detail: Use token abc123 before running curl against the host.",
"Recommended Next Step Action: sudo restart (run_patrol)",
}, "\n")
if err := store.SetModelHandoffEnvelope(session.ID, "", handoffContext, nil, nil, HandoffMetadata{
Kind: "patrol_assessment",
}); err != nil {
t.Fatalf("SetModelHandoffEnvelope failed: %v", err)
}
sessions, err := store.List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(sessions) != 1 || sessions[0].HandoffSummary == nil {
t.Fatalf("sessions = %#v, want one session with handoff summary", sessions)
}
summary := sessions[0].HandoffSummary
if summary.RecommendedNextStep != "" ||
summary.RecommendedNextStepDetail != "" ||
summary.RecommendedNextStepAction != "" ||
summary.RecommendedNextStepActionKind != "" {
t.Fatalf("unsafe recommendation summary = %#v, want withheld recommendation fields", summary)
}
}
func TestSessionStore_ListKeepsPatrolConfigurationFailureHandoffIdentity(t *testing.T) {
store, err := NewSessionStore(t.TempDir())
if err != nil {
t.Fatalf("failed to create session store: %v", err)
}
session, err := store.Create()
if err != nil {
t.Fatalf("failed to create session: %v", err)
}
if err := store.SetModelHandoffEnvelope(session.ID, "", "[Patrol Configuration Failure Context]\nSource: Pulse Patrol configuration surface", nil, nil, HandoffMetadata{
Kind: "patrol_configuration_failure",
RuntimeFailure: true,
}); err != nil {
t.Fatalf("SetModelHandoffEnvelope failed: %v", err)
}
sessions, err := store.List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(sessions) != 1 || sessions[0].HandoffSummary == nil {
t.Fatalf("sessions = %#v, want one session with handoff summary", sessions)
}
summary := sessions[0].HandoffSummary
if summary.Kind != sessionHandoffKindPatrolConfigurationFailure || !summary.RuntimeFailure {
t.Fatalf("handoff summary = %#v, want configuration failure runtime identity", summary)
}
if summary.RunID != "" || summary.RunStatus != "" {
t.Fatalf("run fields = %#v, want no Patrol run identity for configuration failure", summary)
}
}
func TestModelContextHandoffSummaryInfersPatrolRunMetadataForExistingSessions(t *testing.T) {
summary := modelContextHandoffSummary(&sessionModelContext{
HandoffContext: strings.Join([]string{
"[Patrol Run Context]",
"Source: Pulse Patrol run history",
"Run ID: run-existing",
"Run Type: Scheduled run",
"Status: completed",
"Runtime Failure: Selected model failed during Patrol execution.",
}, "\n"),
})
if summary == nil {
t.Fatalf("summary is nil")
}
if summary.Kind != sessionHandoffKindPatrolRun {
t.Fatalf("handoff kind = %q, want %q", summary.Kind, sessionHandoffKindPatrolRun)
}
if summary.RunID != "run-existing" || summary.RunType != "Scheduled run" || summary.RunStatus != "completed" || !summary.RuntimeFailure {
t.Fatalf("run summary = %#v, want inferred Patrol run identity", summary)
}
}
func TestModelContextHandoffSummaryRequiresApprovalOnlyForCurrentPendingApproval(t *testing.T) {
tests := []struct {
name string
action HandoffAction
requiresApproval bool
}{
{
name: "pending approval",
action: HandoffAction{
ApprovalID: "approval-pending",
ApprovalStatus: "pending",
ActionState: "pending_approval",
ActionRequiresApproval: true,
},
requiresApproval: true,
},
{
name: "legacy awaiting approval state",
action: HandoffAction{
ApprovalID: "approval-awaiting",
ActionState: "awaiting_approval",
ActionRequiresApproval: true,
},
requiresApproval: true,
},
{
name: "approved completed action",
action: HandoffAction{
ApprovalID: "approval-approved",
ApprovalStatus: "approved",
ActionState: "completed",
ActionRequiresApproval: true,
},
requiresApproval: false,
},
{
name: "denied historical action",
action: HandoffAction{
ApprovalID: "approval-denied",
ApprovalStatus: "denied",
ActionState: "rejected",
ActionRequiresApproval: true,
},
requiresApproval: false,
},
{
name: "approval reference without current pending state",
action: HandoffAction{
ApprovalID: "approval-unknown",
ActionRequiresApproval: true,
},
requiresApproval: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
summary := modelContextHandoffSummary(&sessionModelContext{
HandoffActions: []HandoffAction{tt.action},
})
if summary == nil {
t.Fatalf("summary is nil")
}
if summary.RequiresApproval != tt.requiresApproval {
t.Fatalf("requires approval = %v, want %v; summary=%#v", summary.RequiresApproval, tt.requiresApproval, summary)
}
})
}
}
func TestSessionStore_ClearModelHandoffContext(t *testing.T) {
store, err := NewSessionStore(t.TempDir())
if err != nil {
t.Fatalf("failed to create session store: %v", err)
}
session, err := store.Create()
if err != nil {
t.Fatalf("failed to create session: %v", err)
}
if err := store.SetModelHandoffFindingID(session.ID, "finding-123"); err != nil {
t.Fatalf("SetModelHandoffFindingID failed: %v", err)
}
if err := store.SetModelHandoffContext(session.ID, "[Finding Context]\nID: finding-123"); err != nil {
t.Fatalf("SetModelHandoffContext failed: %v", err)
}
if err := store.SetModelHandoffResources(session.ID, []HandoffResource{{
ID: "vm-100",
Name: "web-server",
Type: "vm",
}}); err != nil {
t.Fatalf("SetModelHandoffResources failed: %v", err)
}
if err := store.SetModelHandoffActions(session.ID, []HandoffAction{{
FindingID: "finding-123",
ApprovalID: "approval-123",
FixID: "fix-123",
}}); err != nil {
t.Fatalf("SetModelHandoffActions failed: %v", err)
}
if err := store.AddMessage(session.ID, Message{Role: "user", Content: "What changed?"}); err != nil {
t.Fatalf("AddMessage failed: %v", err)
}
if err := store.ClearModelHandoffContext(session.ID); err != nil {
t.Fatalf("ClearModelHandoffContext failed: %v", err)
}
if got, err := store.GetModelHandoffFindingID(session.ID); err != nil {
t.Fatalf("GetModelHandoffFindingID failed: %v", err)
} else if got != "" {
t.Fatalf("handoff finding ID after clear = %q, want empty", got)
}
if got, err := store.GetModelHandoffContext(session.ID); err != nil {
t.Fatalf("GetModelHandoffContext failed: %v", err)
} else if got != "" {
t.Fatalf("handoff context after clear = %q, want empty", got)
}
if got, err := store.GetModelHandoffResources(session.ID); err != nil {
t.Fatalf("GetModelHandoffResources failed: %v", err)
} else if len(got) != 0 {
t.Fatalf("handoff resources after clear = %#v, want empty", got)
}
if got, err := store.GetModelHandoffActions(session.ID); err != nil {
t.Fatalf("GetModelHandoffActions failed: %v", err)
} else if len(got) != 0 {
t.Fatalf("handoff actions after clear = %#v, want empty", got)
}
messages, err := store.GetMessages(session.ID)
if err != nil {
t.Fatalf("GetMessages failed: %v", err)
}
if len(messages) != 1 || messages[0].Content != "What changed?" {
t.Fatalf("messages after handoff clear = %#v, want user history retained", messages)
}
}
func TestSessionStore_ResolvedContextLifecycle(t *testing.T) {
store, err := NewSessionStore(t.TempDir())
if err != nil {
t.Fatalf("failed to create session store: %v", err)
}
session, err := store.Create()
if err != nil {
t.Fatalf("failed to create session: %v", err)
}
res := &ResolvedResource{
ResourceID: "vm:node1:101",
ResourceType: "vm",
Name: "alpha",
TargetHost: "alpha",
AllowedActions: []string{"start"},
}
store.AddResolvedResource(session.ID, res.Name, res)
if _, err := store.ValidateResourceForAction(session.ID, res.ResourceID, "start"); err != nil {
t.Fatalf("expected action to be allowed: %v", err)
}
if _, err := store.ValidateResourceForAction(session.ID, res.ResourceID, "stop"); err == nil {
t.Fatalf("expected action to be blocked")
}
store.ClearResolvedContext(session.ID)
if _, err := store.ValidateResourceForAction(session.ID, res.ResourceID, "start"); err == nil {
t.Fatalf("expected resource to be unresolved after clear")
}
}
func TestSessionStore_ClearSessionState(t *testing.T) {
store, err := NewSessionStore(t.TempDir())
if err != nil {
t.Fatalf("failed to create session store: %v", err)
}
session, err := store.Create()
if err != nil {
t.Fatalf("failed to create session: %v", err)
}
// Set up context, FSM, and toolset
res := &ResolvedResource{ResourceID: "node:node1", Name: "node1", ResourceType: "node"}
store.AddResolvedResource(session.ID, res.Name, res)
ctx := store.GetResolvedContext(session.ID)
ctx.PinResource(res.ResourceID)
fsm := store.GetSessionFSM(session.ID)
fsm.State = StateVerifying
store.SetToolSet(session.ID, map[string]bool{"pulse_query": true})
store.GetKnowledgeAccumulator(session.ID)
store.ClearSessionState(session.ID, true)
if !store.GetResolvedContext(session.ID).HasAnyResources() {
t.Fatalf("expected pinned resources to remain")
}
if fsm.State != StateReading {
t.Fatalf("expected FSM to keep progress when pinned resources remain")
}
if store.GetToolSet(session.ID) == nil {
t.Fatalf("expected toolset to remain when keepPinned=true")
}
store.ClearSessionState(session.ID, false)
if store.GetToolSet(session.ID) != nil {
t.Fatalf("expected toolset to be cleared when keepPinned=false")
}
}
func TestSessionStore_ResetFSMAndCleanupContext(t *testing.T) {
store, err := NewSessionStore(t.TempDir())
if err != nil {
t.Fatalf("failed to create session store: %v", err)
}
session, err := store.Create()
if err != nil {
t.Fatalf("failed to create session: %v", err)
}
fsm := store.GetSessionFSM(session.ID)
fsm.State = StateVerifying
store.ResetSessionFSM(session.ID, true)
if fsm.State != StateReading {
t.Fatalf("expected ResetSessionFSM keep progress to move to READING")
}
fsm.State = StateVerifying
store.ResetSessionFSM(session.ID, false)
if fsm.State != StateResolving {
t.Fatalf("expected ResetSessionFSM full reset to move to RESOLVING")
}
store.AddResolvedResource(session.ID, "node1", &ResolvedResource{ResourceID: "node:node1", Name: "node1"})
store.cleanupResolvedContext(session.ID)
if store.GetResolvedContext(session.ID).HasAnyResources() {
t.Fatalf("expected cleanupResolvedContext to remove resources")
}
}
func TestSessionStore_RejectsInvalidSessionIDs(t *testing.T) {
root := t.TempDir()
store, err := NewSessionStore(root)
if err != nil {
t.Fatalf("failed to create session store: %v", err)
}
type op func(string) error
ops := []struct {
name string
run op
}{
{
name: "get",
run: func(id string) error {
_, err := store.Get(id)
return err
},
},
{
name: "delete",
run: func(id string) error {
return store.Delete(id)
},
},
{
name: "add_message",
run: func(id string) error {
return store.AddMessage(id, Message{Role: "user", Content: "test"})
},
},
{
name: "ensure_session",
run: func(id string) error {
_, err := store.EnsureSession(id)
return err
},
},
}
badIDs := []string{
"../escape",
"..\\escape",
"nested/session",
"contains space",
"evil.json",
strings.Repeat("a", maxSessionIDLength+1),
}
for _, tc := range ops {
for _, id := range badIDs {
err := tc.run(id)
if err == nil {
t.Fatalf("%s should reject invalid id %q", tc.name, id)
}
if !strings.Contains(err.Error(), "invalid session id") {
t.Fatalf("%s returned unexpected error for id %q: %v", tc.name, id, err)
}
}
}
// Ensure traversal-style IDs never created files outside ai_sessions.
if _, err := os.Stat(filepath.Join(root, "escape.json")); !os.IsNotExist(err) {
t.Fatalf("expected no escaped session file to be created, stat err=%v", err)
}
}