feat(FN-3725): add shell regression matrix and stabilize action gate and ro
This merge adds two policy features (executor role enforcement in core and internal coordination tool exemptions in the engine), introduces a shell regression matrix covering mobile, desktop, and dashboard with corresponding readme entries, and fixes a broad suite of tests across the dashboard, engi Fusion-Task-Id: FN-3725
This commit is contained in:
@@ -1313,7 +1313,7 @@ describe("MailboxView", () => {
|
||||
expect(splitPaneBlockMatch).toBeTruthy();
|
||||
const splitPaneBlock = splitPaneBlockMatch![1];
|
||||
expect(splitPaneBlock).toContain("overflow-y: auto;");
|
||||
expect(splitPaneBlock).toContain("border: 1px solid var(--border);");
|
||||
expect(splitPaneBlock).toContain("border: var(--btn-border-width) solid var(--border);");
|
||||
expect(splitPaneBlock).toContain("background: var(--surface);");
|
||||
|
||||
const splitEmptyBlockMatch = css.match(/\.mailbox-view\s+\.mailbox-split-empty\s*\{([^}]*)\}/);
|
||||
@@ -1377,23 +1377,15 @@ describe("MailboxView", () => {
|
||||
const path = await import("path");
|
||||
const css = loadAllAppCss();
|
||||
|
||||
const mailboxTabBlockMatch = css.match(/\.mailbox-tab\s*\{([^}]*)\}/);
|
||||
expect(mailboxTabBlockMatch).toBeTruthy();
|
||||
const mailboxTabBlock = mailboxTabBlockMatch![1];
|
||||
expect(mailboxTabBlock).toContain("border-color: var(--border);");
|
||||
expect(mailboxTabBlock).toContain("background: var(--surface);");
|
||||
expect(mailboxTabBlock).not.toContain("border: none");
|
||||
expect(mailboxTabBlock).not.toContain("background: none");
|
||||
expect(mailboxTabBlock).not.toContain("border-bottom: 2px solid transparent");
|
||||
expect(css).toMatch(/\.mailbox-tab\s*\{[^}]*border-color:\s*var\(--border\);[^}]*background:\s*var\(--surface\);[^}]*\}/);
|
||||
expect(css).not.toMatch(/\.mailbox-tab\s*\{[^}]*border:\s*none;[^}]*\}/);
|
||||
expect(css).not.toMatch(/\.mailbox-tab\s*\{[^}]*background:\s*none;[^}]*\}/);
|
||||
expect(css).not.toMatch(/\.mailbox-tab\s*\{[^}]*border-bottom:\s*2px\s+solid\s+transparent;[^}]*\}/);
|
||||
|
||||
const agentSubtabBlockMatch = css.match(/\.mailbox-agent-subtab\s*\{([^}]*)\}/);
|
||||
expect(agentSubtabBlockMatch).toBeTruthy();
|
||||
const agentSubtabBlock = agentSubtabBlockMatch![1];
|
||||
expect(agentSubtabBlock).toContain("border-color: var(--border);");
|
||||
expect(agentSubtabBlock).toContain("background: var(--surface);");
|
||||
expect(agentSubtabBlock).not.toContain("border-radius: 0");
|
||||
expect(agentSubtabBlock).not.toContain("border: none");
|
||||
expect(agentSubtabBlock).not.toContain("background: transparent");
|
||||
expect(css).toMatch(/\.mailbox-agent-subtab\s*\{[^}]*border-color:\s*var\(--border\);[^}]*background:\s*var\(--surface\);[^}]*\}/);
|
||||
expect(css).not.toMatch(/\.mailbox-agent-subtab\s*\{[^}]*border-radius:\s*0;[^}]*\}/);
|
||||
expect(css).not.toMatch(/\.mailbox-agent-subtab\s*\{[^}]*border:\s*none;[^}]*\}/);
|
||||
expect(css).not.toMatch(/\.mailbox-agent-subtab\s*\{[^}]*background:\s*transparent;[^}]*\}/);
|
||||
});
|
||||
|
||||
it("renders structural elements that mobile CSS targets", async () => {
|
||||
|
||||
@@ -1001,7 +1001,7 @@ describe("PlanningModeModal", () => {
|
||||
expect(screen.getByText("Planning Complete!")).toBeDefined();
|
||||
});
|
||||
|
||||
const sizeSelect = screen.getByRole("combobox") as HTMLSelectElement;
|
||||
const sizeSelect = screen.getByLabelText("Suggested Size") as HTMLSelectElement;
|
||||
expect(sizeSelect.value).toBe("M");
|
||||
expect(Array.from(sizeSelect.options).map((option) => option.textContent)).toEqual([
|
||||
"S (Small)",
|
||||
@@ -1130,7 +1130,7 @@ describe("PlanningModeModal", () => {
|
||||
});
|
||||
|
||||
const firstSubtask = screen.getByTestId("subtask-item-0");
|
||||
const sizeSelect = within(firstSubtask).getByRole("combobox") as HTMLSelectElement;
|
||||
const sizeSelect = within(firstSubtask).getByLabelText("Size") as HTMLSelectElement;
|
||||
|
||||
expect(sizeSelect.value).toBe("M");
|
||||
expect(Array.from(sizeSelect.options).map((option) => option.textContent)).toEqual([
|
||||
|
||||
@@ -567,7 +567,7 @@ describe("PluginManager", () => {
|
||||
expect(screen.getByText("Test Plugin A")).toBeTruthy();
|
||||
});
|
||||
|
||||
const uninstallButtons = screen.getAllByTitle("Uninstall");
|
||||
const uninstallButtons = screen.getAllByTitle("Uninstall globally");
|
||||
await userEvent.click(uninstallButtons[0]);
|
||||
|
||||
expect(mockConfirm).toHaveBeenCalledWith({
|
||||
@@ -588,7 +588,7 @@ describe("PluginManager", () => {
|
||||
expect(screen.getByText("Test Plugin A")).toBeTruthy();
|
||||
});
|
||||
|
||||
const uninstallButtons = screen.getAllByTitle("Uninstall");
|
||||
const uninstallButtons = screen.getAllByTitle("Uninstall globally");
|
||||
await userEvent.click(uninstallButtons[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -951,7 +951,7 @@ describe("PluginManager", () => {
|
||||
});
|
||||
|
||||
// Verify initial state - toggle should NOT be checked
|
||||
const toggle = screen.getByRole("checkbox");
|
||||
const toggle = screen.getByRole("checkbox", { name: /Test Plugin A/ });
|
||||
expect(toggle).not.toBeChecked();
|
||||
|
||||
// Now send an SSE event from a DIFFERENT project trying to enable the plugin
|
||||
@@ -965,6 +965,7 @@ describe("PluginManager", () => {
|
||||
transition: "enabled",
|
||||
sourceEvent: "plugin:enabled",
|
||||
timestamp: new Date().toISOString(),
|
||||
scope: "project",
|
||||
projectId: "other-project", // Different project - this event should be filtered
|
||||
enabled: true,
|
||||
state: "started",
|
||||
@@ -975,10 +976,8 @@ describe("PluginManager", () => {
|
||||
});
|
||||
|
||||
// Toggle should STILL NOT be checked since event is from different project (filtered)
|
||||
await waitFor(() => {
|
||||
const filteredToggle = screen.getByRole("checkbox");
|
||||
expect(filteredToggle).not.toBeChecked();
|
||||
});
|
||||
const filteredToggle = screen.getByRole("checkbox", { name: /Test Plugin A/ });
|
||||
expect(filteredToggle).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("cleans up EventSource on unmount", async () => {
|
||||
|
||||
@@ -182,19 +182,20 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// In-progress tasks show exactly 9 tabs:
|
||||
// Definition, Logs, Changes, Comments, Documents, Model, Workflow, Stats, Routing
|
||||
// In-progress tasks show exactly 10 tabs:
|
||||
// Definition, Logs, Changes, Review, Comments, Documents, Model, Workflow, Stats, Routing
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(10);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
expect(tabs[3].textContent).toBe("Comments");
|
||||
expect(tabs[4].textContent).toBe("Documents");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
expect(tabs[7].textContent).toBe("Stats");
|
||||
expect(tabs[8].textContent).toBe("Routing");
|
||||
expect(tabs[3].textContent).toBe("Review");
|
||||
expect(tabs[4].textContent).toBe("Comments");
|
||||
expect(tabs[5].textContent).toBe("Documents");
|
||||
expect(tabs[6].textContent).toBe("Model");
|
||||
expect(tabs[7].textContent).toBe("Workflow");
|
||||
expect(tabs[8].textContent).toBe("Stats");
|
||||
expect(tabs[9].textContent).toBe("Routing");
|
||||
// Commits tab should NOT be present for non-done tasks
|
||||
expect(screen.queryByText("Commits")).toBeNull();
|
||||
});
|
||||
@@ -212,18 +213,19 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// In-progress task with workflow steps: 9 tabs (Workflow after Model, Stats then Routing)
|
||||
// In-progress task with workflow steps: 10 tabs (Review after Changes, Workflow after Model)
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(10);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
expect(tabs[3].textContent).toBe("Comments");
|
||||
expect(tabs[4].textContent).toBe("Documents");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
expect(tabs[7].textContent).toBe("Stats");
|
||||
expect(tabs[8].textContent).toBe("Routing");
|
||||
expect(tabs[3].textContent).toBe("Review");
|
||||
expect(tabs[4].textContent).toBe("Comments");
|
||||
expect(tabs[5].textContent).toBe("Documents");
|
||||
expect(tabs[6].textContent).toBe("Model");
|
||||
expect(tabs[7].textContent).toBe("Workflow");
|
||||
expect(tabs[8].textContent).toBe("Stats");
|
||||
expect(tabs[9].textContent).toBe("Routing");
|
||||
});
|
||||
|
||||
it("does NOT show Commits tab for done task with mergeDetails.commitSha (changes merged into Changes tab)", () => {
|
||||
@@ -242,18 +244,19 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Done task with commit SHA: Definition, Logs, Changes, Comments, Documents, Model, Workflow, Stats, Routing (9 tabs, no Commits)
|
||||
// Done task with commit SHA: Definition, Logs, Changes, Review, Comments, Documents, Model, Workflow, Stats, Routing (10 tabs, no Commits)
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(10);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
expect(tabs[3].textContent).toBe("Comments");
|
||||
expect(tabs[4].textContent).toBe("Documents");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
expect(tabs[7].textContent).toBe("Stats");
|
||||
expect(tabs[8].textContent).toBe("Routing");
|
||||
expect(tabs[3].textContent).toBe("Review");
|
||||
expect(tabs[4].textContent).toBe("Comments");
|
||||
expect(tabs[5].textContent).toBe("Documents");
|
||||
expect(tabs[6].textContent).toBe("Model");
|
||||
expect(tabs[7].textContent).toBe("Workflow");
|
||||
expect(tabs[8].textContent).toBe("Stats");
|
||||
expect(tabs[9].textContent).toBe("Routing");
|
||||
// Commits tab should NOT be present
|
||||
expect(screen.queryByText("Commits")).toBeNull();
|
||||
});
|
||||
@@ -275,18 +278,19 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Done task with workflow steps and commit SHA: 9 tabs (no Commits)
|
||||
// Done task with workflow steps and commit SHA: 10 tabs including Review (no Commits)
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(10);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
expect(tabs[3].textContent).toBe("Comments");
|
||||
expect(tabs[4].textContent).toBe("Documents");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
expect(tabs[7].textContent).toBe("Stats");
|
||||
expect(tabs[8].textContent).toBe("Routing");
|
||||
expect(tabs[3].textContent).toBe("Review");
|
||||
expect(tabs[4].textContent).toBe("Comments");
|
||||
expect(tabs[5].textContent).toBe("Documents");
|
||||
expect(tabs[6].textContent).toBe("Model");
|
||||
expect(tabs[7].textContent).toBe("Workflow");
|
||||
expect(tabs[8].textContent).toBe("Stats");
|
||||
expect(tabs[9].textContent).toBe("Routing");
|
||||
// Commits tab should NOT be present
|
||||
expect(screen.queryByText("Commits")).toBeNull();
|
||||
});
|
||||
@@ -305,9 +309,9 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
const triageTabs = triageContainer.querySelectorAll(".detail-tab");
|
||||
expect(triageTabs.length).toBe(8); // Definition, Logs, Comments, Documents, Model, Workflow, Stats, Routing
|
||||
expect(triageTabs.length).toBe(9); // Definition, Logs, Review, Comments, Documents, Model, Workflow, Stats, Routing
|
||||
expect(Array.from(triageTabs).map(t => t.textContent)).toEqual([
|
||||
"Definition", "Logs", "Comments", "Documents", "Model", "Workflow", "Stats", "Routing",
|
||||
"Definition", "Logs", "Review", "Comments", "Documents", "Model", "Workflow", "Stats", "Routing",
|
||||
]);
|
||||
|
||||
const { container: todoContainer } = render(
|
||||
@@ -323,9 +327,9 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
const todoTabs = todoContainer.querySelectorAll(".detail-tab");
|
||||
expect(todoTabs.length).toBe(8); // Definition, Logs, Comments, Documents, Model, Workflow, Stats, Routing
|
||||
expect(todoTabs.length).toBe(9); // Definition, Logs, Review, Comments, Documents, Model, Workflow, Stats, Routing
|
||||
expect(Array.from(todoTabs).map(t => t.textContent)).toEqual([
|
||||
"Definition", "Logs", "Comments", "Documents", "Model", "Workflow", "Stats", "Routing",
|
||||
"Definition", "Logs", "Review", "Comments", "Documents", "Model", "Workflow", "Stats", "Routing",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -3,12 +3,14 @@ import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { TaskReviewTab } from "../TaskReviewTab";
|
||||
import { makeTask } from "./TaskDetailModal.test-helpers";
|
||||
|
||||
const refreshTaskReview = vi.fn();
|
||||
const reviseTaskReviewItems = vi.fn();
|
||||
const apiMocks = vi.hoisted(() => ({
|
||||
refreshTaskReview: vi.fn(),
|
||||
reviseTaskReviewItems: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
refreshTaskReview,
|
||||
reviseTaskReviewItems,
|
||||
refreshTaskReview: apiMocks.refreshTaskReview,
|
||||
reviseTaskReviewItems: apiMocks.reviseTaskReviewItems,
|
||||
}));
|
||||
|
||||
describe("TaskReviewTab", () => {
|
||||
@@ -24,10 +26,10 @@ describe("TaskReviewTab", () => {
|
||||
|
||||
it("calls refresh endpoint", async () => {
|
||||
const task = makeTask({ review: { mode: "direct", source: "reviewer-agent", decision: "pending", items: [] } });
|
||||
refreshTaskReview.mockResolvedValue({ review: task.review, automationStatus: null });
|
||||
apiMocks.refreshTaskReview.mockResolvedValue({ review: task.review, automationStatus: null });
|
||||
render(<TaskReviewTab task={task} addToast={vi.fn()} />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Refresh" }));
|
||||
expect(refreshTaskReview).toHaveBeenCalledWith(task.id, undefined);
|
||||
expect(apiMocks.refreshTaskReview).toHaveBeenCalledWith(task.id, undefined);
|
||||
});
|
||||
|
||||
it("renders PR decision and status modifiers", () => {
|
||||
@@ -75,14 +77,14 @@ describe("TaskReviewTab", () => {
|
||||
},
|
||||
});
|
||||
|
||||
reviseTaskReviewItems.mockResolvedValue({ task, review: task.review });
|
||||
refreshTaskReview.mockResolvedValue({ review: task.review, automationStatus: null });
|
||||
apiMocks.reviseTaskReviewItems.mockResolvedValue({ task, review: task.review });
|
||||
apiMocks.refreshTaskReview.mockResolvedValue({ review: task.review, automationStatus: null });
|
||||
|
||||
render(<TaskReviewTab task={task} addToast={vi.fn()} />);
|
||||
|
||||
fireEvent.click(screen.getByRole("checkbox"));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Request revision" }));
|
||||
|
||||
expect(reviseTaskReviewItems).toHaveBeenCalledWith(task.id, ["ri-1"], undefined);
|
||||
expect(apiMocks.reviseTaskReviewItems).toHaveBeenCalledWith(task.id, ["ri-1"], undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,6 +134,7 @@ describe("remote access headless parity", () => {
|
||||
getMissionAutopilot: vi.fn(),
|
||||
getMissionExecutionLoop: vi.fn(),
|
||||
}),
|
||||
getMessageStore: vi.fn().mockReturnValue(undefined),
|
||||
getHeartbeatMonitor: vi.fn(),
|
||||
getWorkingDirectory: vi.fn().mockReturnValue("/fake/root"),
|
||||
getRoutineStore: vi.fn(),
|
||||
|
||||
@@ -318,11 +318,13 @@ describe("normalizeSubtaskItem", () => {
|
||||
8,
|
||||
);
|
||||
|
||||
// Priority now normalizes to "normal" when omitted.
|
||||
expect(result).toEqual({
|
||||
id: "subtask-9",
|
||||
title: "Title",
|
||||
description: "Description",
|
||||
suggestedSize: "L",
|
||||
priority: "normal",
|
||||
dependsOn: ["subtask-1"],
|
||||
});
|
||||
});
|
||||
@@ -341,6 +343,7 @@ describe("normalizeSubtaskItem", () => {
|
||||
title: "Plan",
|
||||
description: "Work",
|
||||
suggestedSize: "M",
|
||||
priority: "normal",
|
||||
dependsOn: [],
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user