| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "path/filepath" |
| 5 | "testing" |
| 6 | |
| 7 | "reasonix/internal/event" |
| 8 | "reasonix/internal/provider" |
| 9 | ) |
| 10 | |
| 11 | // Planner notices are persisted through plannerDisplay into session history; |
| 12 | // dropping Detail here would make the expandable diagnostics disappear after |
| 13 | // a reload even though the live transcript showed them. |
| 14 | func TestRecordPlannerDisplayEventKeepsNoticeDetail(t *testing.T) { |
| 15 | tab := &WorkspaceTab{} |
| 16 | tab.recordDisplayEvent(event.Event{ |
| 17 | Kind: event.Notice, |
| 18 | Level: event.LevelWarn, |
| 19 | Source: event.UsageSourcePlanner, |
| 20 | Text: "An MCP server failed to start.", |
| 21 | Detail: `mcp server "github" failed to start: command not found`, |
| 22 | }) |
| 23 | messages := tab.takeDisplayTurn(false) |
| 24 | if len(messages) != 1 { |
| 25 | t.Fatalf("planner display len = %d, want 1", len(messages)) |
| 26 | } |
| 27 | got := messages[0] |
| 28 | if got.Role != "notice" || got.Level != "warn" { |
| 29 | t.Fatalf("unexpected notice message: %+v", got) |
| 30 | } |
| 31 | if got.Content != "An MCP server failed to start." { |
| 32 | t.Fatalf("Content = %q", got.Content) |
| 33 | } |
| 34 | if got.Detail != `mcp server "github" failed to start: command not found` { |
| 35 | t.Fatalf("Detail = %q, want diagnostic detail preserved", got.Detail) |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | func TestCancelledDisplaySidecarReplaysStructuredDecisionReceipt(t *testing.T) { |
| 40 | const userContent = "run the build" |
| 41 | receipt := &provider.DecisionReceipt{ |
| 42 | ID: "approval-1", |
| 43 | Kind: "tool", |
| 44 | Tool: "bash", |
| 45 | Subject: "npm run build", |
| 46 | Outcome: "allow_once", |
| 47 | } |
| 48 | tab := &WorkspaceTab{} |
| 49 | tab.recordDisplayEvent(event.Event{ |
| 50 | Kind: event.Notice, |
| 51 | Level: event.LevelInfo, |
| 52 | Source: event.UsageSourceExecutor, |
| 53 | Text: "Decision recorded: allow_once", |
| 54 | Code: event.NoticeCodeDecisionReceipt, |
| 55 | DecisionReceipt: receipt, |
| 56 | }) |
| 57 | display := tab.takeDisplayTurn(true) |
| 58 | if len(display) != 2 || display[0].DecisionReceipt == nil || display[1].Code != event.NoticeCodeCancelledTurn { |
| 59 | t.Fatalf("cancelled display = %+v, want structured receipt plus cancellation notice", display) |
| 60 | } |
| 61 | |
| 62 | dir := t.TempDir() |
| 63 | sessionPath := filepath.Join(dir, "session.jsonl") |
| 64 | if err := recordSessionPlannerDisplay(dir, sessionPath, userContent, display); err != nil { |
| 65 | t.Fatalf("record cancelled display: %v", err) |
| 66 | } |
| 67 | plannerTurns := sessionPlannerDisplayTurns(dir, sessionPath) |
| 68 | if len(plannerTurns) != 1 || len(plannerTurns[0].Messages) != 2 || plannerTurns[0].Messages[0].DecisionReceipt == nil { |
| 69 | t.Fatalf("persisted display lost decision receipt: %+v", plannerTurns) |
| 70 | } |
| 71 | |
| 72 | visible := historyMessagesWithPlannerDisplays([]provider.Message{ |
| 73 | {Role: provider.RoleUser, Content: userContent}, |
| 74 | {Role: provider.RoleAssistant, DecisionReceipts: []*provider.DecisionReceipt{receipt}}, |
| 75 | }, func(content string) string { return content }, plannerTurns, nil) |
| 76 | if len(visible) != 3 { |
| 77 | t.Fatalf("visible history = %+v, want user, sidecar receipt, and cancellation notice", visible) |
| 78 | } |
| 79 | got := visible[1] |
| 80 | if got.Code != event.NoticeCodeDecisionReceipt || got.DecisionReceipt == nil { |
| 81 | t.Fatalf("structured decision receipt missing after reload: %+v", got) |
| 82 | } |
| 83 | if got.DecisionReceipt.ID != receipt.ID || got.DecisionReceipt.Tool != receipt.Tool || got.DecisionReceipt.Subject != receipt.Subject || got.DecisionReceipt.Outcome != receipt.Outcome { |
| 84 | t.Fatalf("replayed receipt = %+v, want %+v", got.DecisionReceipt, receipt) |
| 85 | } |
| 86 | for i, message := range visible { |
| 87 | if i != 1 && message.DecisionReceipt != nil { |
| 88 | t.Fatalf("decision receipt replayed more than once: %+v", visible) |
| 89 | } |
| 90 | } |
| 91 | } |
| 92 |