deer-flow/frontend/tests/e2e/agents-feature-disabled.spec.ts
Nan Gao 21b3510226
feat(agents): feature-gate the agents UI behind the agents_api flag (#3757) (#3769)
* feat(gateway): add GET /api/features for frontend feature gating (#3757)

* feat(agents): add useAgentsApiEnabled feature-flag hook (#3757)

* feat(agents): gate /workspace/agents segment on agents_api flag (#3757)

* feat(sidebar): grey out Agents button with tooltip when agents_api disabled (#3757)

* fix(sidebar): show disabled Agents tooltip on hover, harden a11y + e2e (#3757)

- Wrap the disabled Agents button in a hoverable tooltip trigger so the
  'feature not enabled' hint shows in the expanded sidebar, not only when
  collapsed (the SidebarMenuButton tooltip prop is hidden unless collapsed).
- Add tabIndex={-1} and drop the redundant onClick preventDefault.
- Add e2e coverage for the disabled state + a default /api/features mock.

* style(sidebar): move cursor-not-allowed to the hoverable span (#3757)

* test(agents): anchor e2e agents-API request filter with a path regex (#3757)

* fix(agents): don't expose backend config in disabled message; tell user to contact admin (#3757)

The disabled panel previously said 'Set agents_api.enabled: true in
config.yaml', leaking backend configuration to end users. Replace with a
generic 'not enabled on this server, contact your administrator' message
(matching the existing nameStepApiDisabledError copy). e2e now asserts the
contact-admin message and that no config.yaml/agents_api text is rendered.

* make format

* fix(agents): keep agents_api flag sticky during /api/features outage (#3757)

Failing open re-mounted the agents UI and re-triggered the 403 storm when
agents_api was genuinely disabled and /api/features was down. Persist the
last definitive answer and fall back to it (sticky) before failing open,
only failing open when nothing has ever been observed. Read the cached
value after mount so the first client render matches the server (no
hydration mismatch on the non-loading-gated sidebar).

* fix(sidebar): make disabled Agents entry keyboard/SR accessible (#3757)

The disabled-state explanation was hover-only: tabIndex={-1} removed the
entry from the tab order and the reason lived only in a pointer-triggered
tooltip. Keep it in the tab order and wire aria-describedby to a
visually-hidden reason so keyboard and screen-reader users learn why it is
disabled.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-07-02 11:33:32 +08:00

94 lines
3.3 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { mockLangGraphAPI } from "./utils/mock-api";
test.describe("Agents feature disabled", () => {
test("shows disabled message and issues no /api/agents requests when feature is off", async ({
page,
}) => {
// Track any request to the agents API — there should be none. Anchor the
// match so it only catches the real agents routes (/api/agents,
// /api/agents/check, /api/agents/{name}) and never a future unrelated
// path that merely contains the substring.
const AGENTS_API = /\/api\/agents(\/|$)/;
const agentRequests: string[] = [];
page.on("request", (req) => {
if (AGENTS_API.test(new URL(req.url()).pathname)) {
agentRequests.push(req.url());
}
});
// Shell/auth endpoints + the agents API mock (which should never be hit).
mockLangGraphAPI(page, { agents: [] });
// Feature flag reports the agents API as disabled.
await page.route("**/api/features", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ agents_api: { enabled: false } }),
}),
);
await page.goto("/workspace/agents");
// The disabled message renders and directs the user to an administrator
// (en-US or zh-CN copy) without leaking backend config details.
await expect(
page.getByText(/contact your administrator|联系管理员/i),
).toBeVisible({ timeout: 15_000 });
await expect(page.getByText(/config\.yaml|agents_api/i)).toHaveCount(0);
// Gate prevented every agents API call, including direct navigation.
expect(agentRequests).toEqual([]);
});
test("stays disabled (no 403 storm) when /api/features goes down after a known-disabled result", async ({
page,
}) => {
const AGENTS_API = /\/api\/agents(\/|$)/;
const agentRequests: string[] = [];
page.on("request", (req) => {
if (AGENTS_API.test(new URL(req.url()).pathname)) {
agentRequests.push(req.url());
}
});
mockLangGraphAPI(page, { agents: [] });
// /api/features first reports disabled, then starts failing — simulating
// an outage of the features endpoint after the flag is already known.
let featuresUp = true;
await page.route("**/api/features", (route) =>
featuresUp
? route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ agents_api: { enabled: false } }),
})
: route.fulfill({
status: 500,
contentType: "application/json",
body: "{}",
}),
);
// First visit observes a definitive "disabled" and persists it.
await page.goto("/workspace/agents");
await expect(
page.getByText(/contact your administrator|联系管理员/i),
).toBeVisible({ timeout: 15_000 });
// The features endpoint now fails. A reload must NOT fail open and remount
// the agents page (which would re-trigger the 403 storm of #3757); the
// last-known "disabled" value is sticky.
featuresUp = false;
agentRequests.length = 0;
await page.goto("/workspace/agents");
await expect(
page.getByText(/contact your administrator|联系管理员/i),
).toBeVisible({ timeout: 15_000 });
expect(agentRequests).toEqual([]);
});
});