0eaaa54e25
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
114 lines
5.6 KiB
JavaScript
114 lines
5.6 KiB
JavaScript
/**
|
|
* Regression: clicking the "currently selected" Project in the Project dropdown must not clear
|
|
* the Agent list and the Sessions under it.
|
|
*
|
|
* Old bug: setCurrentProjectId unconditionally called setAgents([]) + setAgentsLoading(true),
|
|
* while currentProjectId itself didn't change — reloadAgents' effect depends on it and wouldn't
|
|
* re-run, so the sidebar's Agents and the Sessions hanging under them would all disappear and
|
|
* never come back (reproducible by simply clicking the current Project in the dropdown).
|
|
*
|
|
* A Project is deliberately created with a display name different from the username: a newly
|
|
* registered user's initial Project display name defaults to the username, and the bottom-left
|
|
* user menu also shows the username, so locating by name would match both.
|
|
*/
|
|
import { test, expect } from "@playwright/test";
|
|
import { provisionAndLogin } from "./auth.mjs";
|
|
|
|
const BASE = process.env.BASE_URL;
|
|
const MOCK = process.env.MOCK_URL;
|
|
const U = "projswitcher";
|
|
const P = "password123";
|
|
const PROJ = "SwitchTarget";
|
|
|
|
test("clicking the current Project in the dropdown: Agent and Session lists must not disappear", async ({
|
|
page,
|
|
}) => {
|
|
await provisionAndLogin(page.request, U, P);
|
|
|
|
const initial = (await (await page.request.get(`${BASE}/api/projects`)).json()).projects[0];
|
|
|
|
// A non-admin's Project id must be prefixed with <username>-.
|
|
const created = await page.request.post(`${BASE}/api/projects`, {
|
|
data: { projectId: `${U}-target`, name: PROJ },
|
|
});
|
|
expect(created.ok(), "create project").toBeTruthy();
|
|
const projectId = (await created.json()).project.projectId;
|
|
|
|
// Configure Model + credentials on both Projects: a Project without credentials pops an
|
|
// onboarding overlay (fixed inset-0) as soon as the page loads, which would block every click on the sidebar.
|
|
for (const id of [initial.projectId, projectId]) {
|
|
const put = await page.request.put(`${BASE}/api/projects/${id}/models`, {
|
|
data: {
|
|
defaultModel: { provider: "custom", modelId: "claude-4-8" },
|
|
models: [
|
|
{
|
|
provider: "custom",
|
|
modelId: "claude-4-8",
|
|
apiKey: "sk-mock",
|
|
baseUrl: MOCK,
|
|
contextWindow: 200000,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(put.ok(), `put models ${id}`).toBeTruthy();
|
|
}
|
|
|
|
await page.goto("/");
|
|
|
|
// First wait for the initial Project's sidebar to settle: during this time it fetches the
|
|
// Agent list and lands on the draft page (the brand heading marks the draft page); operating
|
|
// the dropdown midway through would get closed by a subsequent re-render.
|
|
const generalAgent = page.getByText("General Agent").first();
|
|
const draftTitle = page.getByRole("heading", { name: "PenguinHarness" });
|
|
await expect(generalAgent).toBeVisible();
|
|
await expect(draftTitle).toBeVisible();
|
|
|
|
// Switch to this newly created Project (a different id: takes the normal switch path).
|
|
const byName = page.getByRole("button", { name: PROJ });
|
|
await page.getByRole("button", { name: U }).first().click(); // the initial Project's display name defaults to the username
|
|
await byName.first().click();
|
|
|
|
// Wait for the new Project's sidebar to settle too (Agent list + draft page).
|
|
await expect(generalAgent).toBeVisible();
|
|
await expect(draftTitle).toBeVisible();
|
|
|
|
// The key action: click this "currently selected" Project again in the dropdown.
|
|
await byName.first().click(); // the top trigger button (currently displaying PROJ)
|
|
await expect(byName).toHaveCount(2); // the trigger button + the same-named item in the menu
|
|
await byName.nth(1).click();
|
|
|
|
// The regression point: both the Agent and the draft page are still there (before the fix,
|
|
// these two assertions would fail — the sidebar gets cleared and stuck in loading).
|
|
await expect(generalAgent).toBeVisible();
|
|
await expect(draftTitle).toBeVisible();
|
|
|
|
// —— Regression (#74 comment): switching Project on the skill library page must not leave
|
|
// installation state from the previous Project —— both Projects' default_agent share the same
|
|
// name (agentId is the page state table's key): first uninstall agent-creation from the current
|
|
// (target) Project via the API, then switch over from the initial Project's skill library page
|
|
// — before the fix, the old Project's snapshot would overwrite the freshly fetched data, so
|
|
// "Manage installation" would always show "Installed."
|
|
const del = await page.request.delete(
|
|
`${BASE}/api/projects/${projectId}/agents/default_agent/skills/agent-creation`,
|
|
);
|
|
expect(del.ok(), "uninstall agent-creation on target project").toBeTruthy();
|
|
|
|
// Switch back to the initial Project, populating the skill library page's snapshot (default_agent has everything preinstalled -> Installed).
|
|
await byName.first().click();
|
|
await page.getByRole("button", { name: U }).first().click();
|
|
await expect(generalAgent).toBeVisible();
|
|
await page.getByRole("link", { name: "技能库" }).click();
|
|
await expect(page).toHaveURL(/\/skills$/);
|
|
await page.getByRole("button", { name: "管理安装 agent-creation" }).click();
|
|
await expect(page.getByRole("button", { name: "卸载 default_agent" })).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
|
|
// Staying on the skill library page while switching to target: the same-named default_agent's state must flip to "Install" (not installed).
|
|
await page.getByRole("button", { name: U }).first().click();
|
|
await byName.first().click();
|
|
await page.getByRole("button", { name: "管理安装 agent-creation" }).click();
|
|
await expect(page.getByRole("button", { name: "安装 default_agent" })).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
});
|