|
| 1 | +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; |
| 2 | +import { describe, expect, it, vi } from "vitest"; |
| 3 | + |
| 4 | +import { DesktopCopilotPanel } from "./DesktopCopilotPanel"; |
| 5 | + |
| 6 | +describe("DesktopCopilotPanel", () => { |
| 7 | + it("renders operator-brief truth surfaces and grounded takeaways after generation", async () => { |
| 8 | + const loadBrief = vi.fn().mockResolvedValue({ |
| 9 | + report_type: "operator_copilot_brief", |
| 10 | + status: "AVAILABLE", |
| 11 | + scope: "run_detail", |
| 12 | + subject_id: "run-123", |
| 13 | + summary: "The operator should compare the staged diff before accepting the run.", |
| 14 | + likely_cause: "The last proof pack is stale.", |
| 15 | + compare_takeaway: "Compare the staged diff against the last approved run.", |
| 16 | + proof_takeaway: "Refresh the proof pack before asking for review.", |
| 17 | + incident_takeaway: "Treat stale proof as an incident until it is re-generated.", |
| 18 | + queue_takeaway: "Keep the queue paused until proof is current.", |
| 19 | + approval_takeaway: "Approval should wait for a fresh proof receipt.", |
| 20 | + used_truth_surfaces: ["run_detail", "", "proof_pack"], |
| 21 | + limitations: ["review not started", " "], |
| 22 | + recommended_actions: ["Refresh proof", "Request review", " "], |
| 23 | + top_risks: ["stale-proof", "", "queue drift"], |
| 24 | + }); |
| 25 | + |
| 26 | + render( |
| 27 | + <DesktopCopilotPanel |
| 28 | + intro="Only grounded control-plane truth belongs here." |
| 29 | + questionSet={["What is blocked?", "What should the operator do next?"]} |
| 30 | + loadBrief={loadBrief} |
| 31 | + />, |
| 32 | + ); |
| 33 | + |
| 34 | + expect(screen.getByText("Only grounded control-plane truth belongs here.")).toBeInTheDocument(); |
| 35 | + expect(screen.getByText("What is blocked?")).toBeInTheDocument(); |
| 36 | + expect(screen.getByText("What should the operator do next?")).toBeInTheDocument(); |
| 37 | + expect(screen.getByText("On demand")).toBeInTheDocument(); |
| 38 | + |
| 39 | + fireEvent.click(screen.getByRole("button", { name: "Generate operator brief" })); |
| 40 | + |
| 41 | + expect(await screen.findByText("Grounded brief")).toBeInTheDocument(); |
| 42 | + expect(await screen.findByText("The operator should compare the staged diff before accepting the run.")).toBeInTheDocument(); |
| 43 | + expect(screen.getByText("The last proof pack is stale.")).toBeInTheDocument(); |
| 44 | + expect(screen.getByText("Scope: run_detail")).toBeInTheDocument(); |
| 45 | + expect(screen.getByText("Subject: run-123")).toBeInTheDocument(); |
| 46 | + expect(screen.getByText("Truth surfaces: run_detail | proof_pack")).toBeInTheDocument(); |
| 47 | + expect(screen.getByText("Limitations: review not started")).toBeInTheDocument(); |
| 48 | + expect(screen.getByText("Compare the staged diff against the last approved run.")).toBeInTheDocument(); |
| 49 | + expect(screen.getByText("Keep the queue paused until proof is current.")).toBeInTheDocument(); |
| 50 | + expect(screen.getByText("Refresh proof")).toBeInTheDocument(); |
| 51 | + expect(screen.getByText("queue drift")).toBeInTheDocument(); |
| 52 | + expect(screen.getByRole("button", { name: "Regenerate brief" })).toBeInTheDocument(); |
| 53 | + |
| 54 | + expect(loadBrief).toHaveBeenCalledTimes(1); |
| 55 | + }); |
| 56 | + |
| 57 | + it("covers flight-plan fallback labels and empty action/risk lists", async () => { |
| 58 | + const loadBrief = vi.fn().mockResolvedValue({ |
| 59 | + report_type: "flight_plan_copilot_brief", |
| 60 | + status: "UNAVAILABLE", |
| 61 | + summary: "The plan is still advisory because execution has not started yet.", |
| 62 | + risk_takeaway: "Approval is still blocked on a missing operator confirmation.", |
| 63 | + capability_takeaway: "Runtime capability is unresolved until the runner binds.", |
| 64 | + approval_takeaway: "An operator must confirm the start gate before execution.", |
| 65 | + used_truth_surfaces: ["execution_plan_preview"], |
| 66 | + recommended_actions: ["", " "], |
| 67 | + top_risks: [], |
| 68 | + limitations: undefined, |
| 69 | + }); |
| 70 | + |
| 71 | + render(<DesktopCopilotPanel title="Flight plan panel" intro={undefined} questionSet={["Why this plan?"]} loadBrief={loadBrief} />); |
| 72 | + |
| 73 | + fireEvent.click(screen.getByRole("button", { name: "Generate operator brief" })); |
| 74 | + |
| 75 | + expect(await screen.findByText("Unavailable")).toBeInTheDocument(); |
| 76 | + expect(screen.getByText("Scope: flight_plan")).toBeInTheDocument(); |
| 77 | + expect(screen.getByText("Subject: execution_plan_report")).toBeInTheDocument(); |
| 78 | + expect(screen.getByText("Truth surfaces: execution_plan_preview")).toBeInTheDocument(); |
| 79 | + expect(screen.getByText("Limitations: -")).toBeInTheDocument(); |
| 80 | + expect(screen.getAllByText("Approval is still blocked on a missing operator confirmation.").length).toBeGreaterThan(0); |
| 81 | + expect(screen.getByText("This brief stays advisory until a run actually starts.")).toBeInTheDocument(); |
| 82 | + expect(screen.getByText("No recommended actions were returned.")).toBeInTheDocument(); |
| 83 | + expect(screen.getByText("No explicit risks were returned.")).toBeInTheDocument(); |
| 84 | + }); |
| 85 | + |
| 86 | + it("surfaces load failures without leaving the panel in generating state", async () => { |
| 87 | + const loadBrief = vi.fn().mockRejectedValue("brief backend unavailable"); |
| 88 | + |
| 89 | + render(<DesktopCopilotPanel questionSet={["Why did this fail?"]} loadBrief={loadBrief} />); |
| 90 | + |
| 91 | + fireEvent.click(screen.getByRole("button", { name: "Generate operator brief" })); |
| 92 | + |
| 93 | + expect(await screen.findByText("brief backend unavailable")).toBeInTheDocument(); |
| 94 | + await waitFor(() => { |
| 95 | + expect(screen.getByRole("button", { name: "Generate operator brief" })).toBeEnabled(); |
| 96 | + }); |
| 97 | + }); |
| 98 | +}); |
0 commit comments