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:
Fusion
2026-05-08 02:14:36 -07:00
committed by gsxdsm
parent f8a0903538
commit 951b0d543f
7 changed files with 69 additions and 68 deletions

View File

@@ -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 () => {

View File

@@ -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([

View File

@@ -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 () => {

View File

@@ -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",
]);
});

View File

@@ -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);
});
});

View File

@@ -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(),

View File

@@ -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: [],
});
});