feat: import Chinese-localized Buzz source snapshot
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
This commit is contained in:
@@ -0,0 +1,562 @@
|
||||
/**
|
||||
* Screenshot spec for the needsRestart badge and config diff (PR #1853 + diff
|
||||
* overlay work).
|
||||
*
|
||||
* Exercises:
|
||||
* - Agent grid restart actions without a duplicate status badge.
|
||||
* - Profile badge tooltip with itemised before→after diff.
|
||||
* - Runtime-tab banner with full uncapped diff list.
|
||||
* - Side-panel badge visible on the default (Info) tab — not only Runtime.
|
||||
* - DOM validity: tooltip trigger has no <button> ancestor.
|
||||
* - Generic rendering: unknown field ids, number/array values, masked values.
|
||||
*/
|
||||
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
|
||||
const SHOTS = "test-results/restart-diff-screenshots";
|
||||
|
||||
// ── Sample diff entries ───────────────────────────────────────────────────────
|
||||
|
||||
const DIFF_ENTRIES = [
|
||||
{
|
||||
field: "model",
|
||||
change: {
|
||||
kind: "value" as const,
|
||||
before: "gpt-4",
|
||||
after: "claude-3-5-sonnet",
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "system_prompt",
|
||||
change: { kind: "text" as const, before_chars: 512, after_chars: 1024 },
|
||||
},
|
||||
{
|
||||
field: "env.OPENAI_API_KEY",
|
||||
change: { kind: "masked" as const, before: "••••abc1", after: "••••xyz9" },
|
||||
},
|
||||
{ field: "env.BUZZ_LOG", change: { kind: "added" as const } },
|
||||
{
|
||||
field: "relay_url",
|
||||
change: { kind: "masked" as const, before: "••••", after: "••••" },
|
||||
},
|
||||
// Array value — args are atomic; rendered via JSON.stringify (position 6: last visible in tooltip)
|
||||
{
|
||||
field: "agent_args",
|
||||
change: {
|
||||
kind: "value" as const,
|
||||
before: ["acp"],
|
||||
after: ["acp", "--verbose"],
|
||||
},
|
||||
},
|
||||
// 7th entry — truncated in tooltip (cap is 6); visible in uncapped banner
|
||||
{
|
||||
field: "parallelism",
|
||||
change: { kind: "value" as const, before: 1, after: 4 },
|
||||
},
|
||||
// 8th entry — truncated in tooltip; visible in uncapped banner
|
||||
{
|
||||
field: "args",
|
||||
change: { kind: "masked" as const, before: "••••", after: "••••" },
|
||||
},
|
||||
];
|
||||
|
||||
const STANDALONE_AGENT = {
|
||||
pubkey: TEST_IDENTITIES.alice.pubkey,
|
||||
name: "Local Agent",
|
||||
status: "running" as const,
|
||||
needsRestart: true,
|
||||
restartDiff: DIFF_ENTRIES,
|
||||
};
|
||||
|
||||
const PERSONA_AGENT = {
|
||||
pubkey: TEST_IDENTITIES.bob.pubkey,
|
||||
name: "Persona Agent",
|
||||
personaId: "builtin:fizz",
|
||||
status: "running" as const,
|
||||
needsRestart: true,
|
||||
restartDiff: DIFF_ENTRIES,
|
||||
};
|
||||
|
||||
/** Running agent with no config drift — restart action must be absent. */
|
||||
const NO_DRIFT_AGENT = {
|
||||
pubkey: TEST_IDENTITIES.tyler.pubkey,
|
||||
name: "Stable Agent",
|
||||
status: "running" as const,
|
||||
needsRestart: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Inactive agent with a friendly error AND a restart diff. Opening its card
|
||||
* forces the panel to open on the Runtime tab (opensRuntimeTab logic).
|
||||
* Used to assert: Runtime tab is active, hero badge visible, uncapped banner.
|
||||
*/
|
||||
const INACTIVE_FRIENDLY_ERROR_AGENT = {
|
||||
pubkey: TEST_IDENTITIES.outsider.pubkey,
|
||||
name: "Error Restart Agent",
|
||||
status: "stopped" as const,
|
||||
needsRestart: true,
|
||||
restartDiff: DIFF_ENTRIES,
|
||||
lastError: "Agent reported error (code -32002): llm model not found",
|
||||
lastErrorCode: -32002,
|
||||
};
|
||||
|
||||
const RESTART_AGENT = {
|
||||
pubkey: "cd".repeat(32),
|
||||
name: "Restart Agent",
|
||||
status: "running" as const,
|
||||
needsRestart: true,
|
||||
restartDiff: DIFF_ENTRIES,
|
||||
};
|
||||
|
||||
const START_AGENT = {
|
||||
pubkey: "ef".repeat(32),
|
||||
name: "Start Agent",
|
||||
status: "stopped" as const,
|
||||
needsRestart: false,
|
||||
};
|
||||
|
||||
async function gotoAgentsView(page: import("@playwright/test").Page) {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await expect(page.getByTestId("open-agents-view")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await page.getByTestId("open-agents-view").click();
|
||||
await expect(page.getByTestId("agents-library-personas")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
}
|
||||
|
||||
const WCAG_AA_NORMAL_TEXT_CONTRAST = 4.5;
|
||||
|
||||
async function renderedTextContrast(
|
||||
locator: import("@playwright/test").Locator,
|
||||
): Promise<number> {
|
||||
return locator.evaluate((element) => {
|
||||
type Rgba = { r: number; g: number; b: number; a: number };
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
const context = canvas.getContext("2d", { willReadFrequently: true });
|
||||
if (!context) throw new Error("Could not create contrast test canvas");
|
||||
|
||||
const parseColor = (color: string): Rgba => {
|
||||
context.clearRect(0, 0, 1, 1);
|
||||
context.fillStyle = color;
|
||||
context.fillRect(0, 0, 1, 1);
|
||||
const [r, g, b, alpha] = context.getImageData(0, 0, 1, 1).data;
|
||||
return { r, g, b, a: alpha / 255 };
|
||||
};
|
||||
|
||||
const composite = (foreground: Rgba, background: Rgba): Rgba => {
|
||||
const alpha = foreground.a + background.a * (1 - foreground.a);
|
||||
if (alpha === 0) return { r: 0, g: 0, b: 0, a: 0 };
|
||||
|
||||
return {
|
||||
r:
|
||||
(foreground.r * foreground.a +
|
||||
background.r * background.a * (1 - foreground.a)) /
|
||||
alpha,
|
||||
g:
|
||||
(foreground.g * foreground.a +
|
||||
background.g * background.a * (1 - foreground.a)) /
|
||||
alpha,
|
||||
b:
|
||||
(foreground.b * foreground.a +
|
||||
background.b * background.a * (1 - foreground.a)) /
|
||||
alpha,
|
||||
a: alpha,
|
||||
};
|
||||
};
|
||||
|
||||
const backgroundLayers: Rgba[] = [];
|
||||
let current: Element | null = element;
|
||||
while (current) {
|
||||
backgroundLayers.push(
|
||||
parseColor(window.getComputedStyle(current).backgroundColor),
|
||||
);
|
||||
current = current.parentElement;
|
||||
}
|
||||
|
||||
let renderedBackground: Rgba = { r: 255, g: 255, b: 255, a: 1 };
|
||||
for (const layer of backgroundLayers.reverse()) {
|
||||
renderedBackground = composite(layer, renderedBackground);
|
||||
}
|
||||
|
||||
const renderedForeground = composite(
|
||||
parseColor(window.getComputedStyle(element).color),
|
||||
renderedBackground,
|
||||
);
|
||||
const luminance = (color: Rgba) => {
|
||||
const channels = [color.r, color.g, color.b].map((value) => {
|
||||
const channel = value / 255;
|
||||
return channel <= 0.04045
|
||||
? channel / 12.92
|
||||
: ((channel + 0.055) / 1.055) ** 2.4;
|
||||
});
|
||||
return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
|
||||
};
|
||||
const foregroundLuminance = luminance(renderedForeground);
|
||||
const backgroundLuminance = luminance(renderedBackground);
|
||||
|
||||
return (
|
||||
(Math.max(foregroundLuminance, backgroundLuminance) + 0.05) /
|
||||
(Math.min(foregroundLuminance, backgroundLuminance) + 0.05)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test.describe("restart-diff screenshots", () => {
|
||||
test.use({ viewport: { width: 1280, height: 900 } });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
page.on("pageerror", (err) => {
|
||||
console.error(
|
||||
"PAGE ERROR:",
|
||||
err.message,
|
||||
err.stack?.split("\n").slice(0, 5).join("\n"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Badge presence ──────────────────────────────────────────────────────────
|
||||
|
||||
test("01-grid-standalone-restart-badge", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
managedAgents: [STANDALONE_AGENT, NO_DRIFT_AGENT],
|
||||
});
|
||||
|
||||
await gotoAgentsView(page);
|
||||
|
||||
const agentCard = page.getByTestId(
|
||||
`managed-agent-${STANDALONE_AGENT.pubkey}`,
|
||||
);
|
||||
await expect(agentCard).toBeVisible({ timeout: 10_000 });
|
||||
await expect(agentCard.getByText("Restart", { exact: true })).toBeVisible();
|
||||
await expect(agentCard.getByTestId("restart-diff-badge")).toHaveCount(0);
|
||||
|
||||
const stableCard = page.getByTestId(
|
||||
`managed-agent-${NO_DRIFT_AGENT.pubkey}`,
|
||||
);
|
||||
await expect(stableCard).toBeVisible({ timeout: 10_000 });
|
||||
await expect(stableCard.getByText("Restart", { exact: true })).toHaveCount(
|
||||
0,
|
||||
);
|
||||
|
||||
await waitForAnimations(page);
|
||||
await agentCard.screenshot({
|
||||
path: `${SHOTS}/01-grid-standalone-restart-badge.png`,
|
||||
});
|
||||
});
|
||||
|
||||
test("02-grid-persona-restart-badge", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
activePersonaIds: ["builtin:fizz"],
|
||||
managedAgents: [PERSONA_AGENT],
|
||||
});
|
||||
|
||||
await gotoAgentsView(page);
|
||||
|
||||
const personaCard = page.getByTestId(
|
||||
`persona-agent-row-${PERSONA_AGENT.personaId}`,
|
||||
);
|
||||
await expect(personaCard).toBeVisible({ timeout: 10_000 });
|
||||
await expect(
|
||||
personaCard.getByText("Restart", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(personaCard.getByTestId("restart-diff-badge")).toHaveCount(0);
|
||||
|
||||
await waitForAnimations(page);
|
||||
await personaCard.screenshot({
|
||||
path: `${SHOTS}/02-grid-persona-restart-badge.png`,
|
||||
});
|
||||
});
|
||||
|
||||
test("03-running-restart-action", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
managedAgents: [RESTART_AGENT],
|
||||
});
|
||||
|
||||
await gotoAgentsView(page);
|
||||
|
||||
const agentCard = page.getByTestId(`managed-agent-${RESTART_AGENT.pubkey}`);
|
||||
await expect(agentCard).toBeVisible({ timeout: 10_000 });
|
||||
const restartAction = page.getByTestId(
|
||||
`agent-runtime-start-${RESTART_AGENT.pubkey}`,
|
||||
);
|
||||
await expect(restartAction).toHaveText("Restart");
|
||||
await expect(restartAction).toHaveAttribute("aria-label", "Restart Agent");
|
||||
await expect(restartAction).toHaveClass(/bg-transparent/);
|
||||
await expect(restartAction).toHaveClass(/text-amber-800/);
|
||||
await expect(restartAction).toHaveClass(/dark:text-amber-400/);
|
||||
await expect(restartAction.locator("svg")).toHaveCount(0);
|
||||
await expect(restartAction).toHaveCSS("width", "72px");
|
||||
await expect(restartAction).toHaveCSS("height", "36px");
|
||||
await expect(agentCard.getByTestId("restart-diff-badge")).toHaveCount(0);
|
||||
await expect(page.locator("html")).toHaveClass(/light/);
|
||||
expect(await renderedTextContrast(restartAction)).toBeGreaterThanOrEqual(
|
||||
WCAG_AA_NORMAL_TEXT_CONTRAST,
|
||||
);
|
||||
|
||||
await waitForAnimations(page);
|
||||
await agentCard.screenshot({
|
||||
path: `${SHOTS}/03-running-restart-action.png`,
|
||||
});
|
||||
});
|
||||
|
||||
test("restart action meets WCAG AA contrast in dark mode", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("buzz-theme", "catppuccin-mocha");
|
||||
});
|
||||
await installMockBridge(page, {
|
||||
managedAgents: [RESTART_AGENT],
|
||||
});
|
||||
|
||||
await gotoAgentsView(page);
|
||||
|
||||
const restartAction = page.getByTestId(
|
||||
`agent-runtime-start-${RESTART_AGENT.pubkey}`,
|
||||
);
|
||||
await expect(restartAction).toBeVisible();
|
||||
await expect(page.locator("html")).toHaveClass(/dark/);
|
||||
expect(await renderedTextContrast(restartAction)).toBeGreaterThanOrEqual(
|
||||
WCAG_AA_NORMAL_TEXT_CONTRAST,
|
||||
);
|
||||
});
|
||||
|
||||
test("start and restart pills share geometry except for width", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page, {
|
||||
managedAgents: [START_AGENT, RESTART_AGENT],
|
||||
});
|
||||
|
||||
await gotoAgentsView(page);
|
||||
|
||||
const startAction = page.getByTestId(
|
||||
`agent-runtime-start-${START_AGENT.pubkey}`,
|
||||
);
|
||||
const restartAction = page.getByTestId(
|
||||
`agent-runtime-start-${RESTART_AGENT.pubkey}`,
|
||||
);
|
||||
await expect(startAction).toHaveText("Start");
|
||||
await expect(restartAction).toHaveText("Restart");
|
||||
|
||||
const relativeGeometry = async (locator: typeof startAction) =>
|
||||
locator.evaluate((element) => {
|
||||
const frame = element.parentElement?.parentElement?.parentElement;
|
||||
if (!frame) throw new Error("Could not resolve avatar frame");
|
||||
const bounds = element.getBoundingClientRect();
|
||||
const frameBounds = frame.getBoundingClientRect();
|
||||
return {
|
||||
centerX: bounds.x + bounds.width / 2 - frameBounds.x,
|
||||
centerY: bounds.y + bounds.height / 2 - frameBounds.y,
|
||||
height: bounds.height,
|
||||
width: bounds.width,
|
||||
};
|
||||
});
|
||||
|
||||
const [start, restart] = await Promise.all([
|
||||
relativeGeometry(startAction),
|
||||
relativeGeometry(restartAction),
|
||||
]);
|
||||
expect(start.height).toBeCloseTo(restart.height, 2);
|
||||
expect(start.centerX).toBeCloseTo(restart.centerX, 2);
|
||||
expect(start.centerY).toBeCloseTo(restart.centerY, 2);
|
||||
expect(start.width).toBeCloseTo(56, 2);
|
||||
expect(restart.width).toBeCloseTo(72, 2);
|
||||
});
|
||||
|
||||
// ── Runtime-tab banner with full uncapped diff ────────────────────────────
|
||||
|
||||
test("07-runtime-tab-restart-banner-with-diff", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
managedAgents: [STANDALONE_AGENT],
|
||||
});
|
||||
|
||||
await gotoAgentsView(page);
|
||||
|
||||
const agentButton = page.getByRole("button", {
|
||||
name: `${STANDALONE_AGENT.name} agent profile`,
|
||||
});
|
||||
await expect(agentButton).toBeVisible({ timeout: 10_000 });
|
||||
await agentButton.click();
|
||||
|
||||
const panel = page.getByTestId("user-profile-panel");
|
||||
await expect(panel).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Switch to the Runtime tab
|
||||
await panel.getByRole("tab", { name: "Runtime" }).click();
|
||||
|
||||
const banner = panel.getByTestId("needs-restart-banner");
|
||||
await expect(banner).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Banner shows the full uncapped diff list (8 entries, no "and N more")
|
||||
const diffList = banner.getByTestId("restart-diff-list");
|
||||
await expect(diffList).toBeVisible();
|
||||
// All 8 entries visible in banner (no cap).
|
||||
// exact: true prevents substring collision with "Agent args:" label.
|
||||
await expect(diffList.getByText("Args:", { exact: true })).toBeVisible();
|
||||
|
||||
await waitForAnimations(page);
|
||||
await banner.screenshot({
|
||||
path: `${SHOTS}/07-runtime-tab-banner-with-diff.png`,
|
||||
});
|
||||
});
|
||||
|
||||
test("08-runtime-tab-restart-banner-auto-on", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
managedAgents: [STANDALONE_AGENT],
|
||||
});
|
||||
|
||||
await gotoAgentsView(page);
|
||||
|
||||
const agentButton = page.getByRole("button", {
|
||||
name: `${STANDALONE_AGENT.name} agent profile`,
|
||||
});
|
||||
await agentButton.click();
|
||||
|
||||
const panel = page.getByTestId("user-profile-panel");
|
||||
await expect(panel).toBeVisible({ timeout: 10_000 });
|
||||
await panel.getByRole("tab", { name: "Runtime" }).click();
|
||||
|
||||
const banner = panel.getByTestId("needs-restart-banner");
|
||||
await expect(banner).toBeVisible({ timeout: 10_000 });
|
||||
await expect(
|
||||
banner.getByText("Buzz can restart it automatically"),
|
||||
).toBeVisible();
|
||||
|
||||
await waitForAnimations(page);
|
||||
await banner.screenshot({
|
||||
path: `${SHOTS}/08-runtime-tab-banner-auto-on.png`,
|
||||
});
|
||||
});
|
||||
|
||||
test("09-runtime-tab-restart-banner-auto-off", async ({ page }) => {
|
||||
const agentAutoOff = {
|
||||
...STANDALONE_AGENT,
|
||||
autoRestartOnConfigChange: false,
|
||||
};
|
||||
|
||||
await installMockBridge(page, {
|
||||
managedAgents: [agentAutoOff],
|
||||
});
|
||||
|
||||
await gotoAgentsView(page);
|
||||
|
||||
const agentButton = page.getByRole("button", {
|
||||
name: `${agentAutoOff.name} agent profile`,
|
||||
});
|
||||
await agentButton.click();
|
||||
|
||||
const panel = page.getByTestId("user-profile-panel");
|
||||
await expect(panel).toBeVisible({ timeout: 10_000 });
|
||||
await panel.getByRole("tab", { name: "Runtime" }).click();
|
||||
|
||||
const banner = panel.getByTestId("needs-restart-banner");
|
||||
await expect(banner).toBeVisible({ timeout: 10_000 });
|
||||
await expect(banner.getByText("Automatic restart is off")).toBeVisible();
|
||||
|
||||
await waitForAnimations(page);
|
||||
await banner.screenshot({
|
||||
path: `${SHOTS}/09-runtime-tab-banner-auto-off.png`,
|
||||
});
|
||||
});
|
||||
|
||||
// ── Side-panel badge on default (Info) tab ─────────────────────────────────
|
||||
|
||||
test("10-profile-panel-badge-on-default-info-tab", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
managedAgents: [STANDALONE_AGENT],
|
||||
});
|
||||
|
||||
await gotoAgentsView(page);
|
||||
|
||||
// Open profile panel via the agent card button — opens on Info tab by default
|
||||
const agentButton = page.getByRole("button", {
|
||||
name: `${STANDALONE_AGENT.name} agent profile`,
|
||||
});
|
||||
await expect(agentButton).toBeVisible({ timeout: 10_000 });
|
||||
await agentButton.click();
|
||||
|
||||
const panel = page.getByTestId("user-profile-panel");
|
||||
await expect(panel).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Tab-independent badge: visible even on the Info tab (not only Runtime)
|
||||
const heroBadge = panel.getByTestId("restart-diff-badge");
|
||||
await expect(heroBadge).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// Hero badge tooltip is functional: hover shows the diff list
|
||||
await heroBadge.hover();
|
||||
const heroTooltip = page.locator("[role=tooltip]");
|
||||
await expect(heroTooltip).toBeVisible({ timeout: 5_000 });
|
||||
await expect(heroTooltip.getByText("Model:")).toBeVisible();
|
||||
|
||||
// The Runtime tab banner is NOT visible on Info tab
|
||||
await expect(panel.getByTestId("needs-restart-banner")).toHaveCount(0);
|
||||
|
||||
await waitForAnimations(page);
|
||||
await panel.screenshot({
|
||||
path: `${SHOTS}/10-panel-badge-default-info-tab.png`,
|
||||
});
|
||||
});
|
||||
|
||||
// ── Inactive + friendly-error: panel opens on Runtime, hero badge + banner ─
|
||||
|
||||
test("11-inactive-friendly-error-panel-opens-runtime-tab", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page, {
|
||||
managedAgents: [INACTIVE_FRIENDLY_ERROR_AGENT],
|
||||
});
|
||||
|
||||
await gotoAgentsView(page);
|
||||
|
||||
// The card for an inactive agent with a friendly error forces Runtime tab on open.
|
||||
const agentCard = page.getByTestId(
|
||||
`managed-agent-${INACTIVE_FRIENDLY_ERROR_AGENT.pubkey}`,
|
||||
);
|
||||
await expect(agentCard).toBeVisible({ timeout: 10_000 });
|
||||
await agentCard.click();
|
||||
|
||||
const panel = page.getByTestId("user-profile-panel");
|
||||
await expect(panel).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Panel opens on Runtime tab (opensRuntimeTab = true for inactive+friendlyError)
|
||||
const runtimeTab = panel.getByRole("tab", { name: "Runtime" });
|
||||
await expect(runtimeTab).toHaveAttribute("aria-selected", "true", {
|
||||
timeout: 5_000,
|
||||
});
|
||||
|
||||
// Hero badge is visible on the Runtime tab (tab-independent)
|
||||
const heroBadge = panel.getByTestId("restart-diff-badge");
|
||||
await expect(heroBadge).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// Hero badge tooltip works on the Runtime tab too
|
||||
await heroBadge.hover();
|
||||
const heroTooltip = page.locator("[role=tooltip]");
|
||||
await expect(heroTooltip).toBeVisible({ timeout: 5_000 });
|
||||
await expect(heroTooltip.getByText("Model:")).toBeVisible();
|
||||
// Tooltip is capped at 6 + "and 2 more"
|
||||
await expect(heroTooltip.getByText("and 2 more")).toBeVisible();
|
||||
|
||||
// Full uncapped banner also visible on Runtime tab
|
||||
const banner = panel.getByTestId("needs-restart-banner");
|
||||
await expect(banner).toBeVisible({ timeout: 5_000 });
|
||||
const diffList = banner.getByTestId("restart-diff-list");
|
||||
await expect(diffList).toBeVisible();
|
||||
// All 8 entries visible in banner (no cap) — last entry "args" is present.
|
||||
// exact: true prevents substring collision with "Agent args:" label.
|
||||
await expect(diffList.getByText("Args:", { exact: true })).toBeVisible();
|
||||
|
||||
await waitForAnimations(page);
|
||||
await panel.screenshot({
|
||||
path: `${SHOTS}/11-inactive-error-panel-runtime-tab.png`,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user