Files
kandev/apps/web/e2e/tests/settings/mobile-agent-runtime-update.spec.ts
T

202 lines
8.5 KiB
TypeScript

import { test, expect } from "../../fixtures/test-base";
import { settledBoundingBox } from "../../helpers/settled-box";
import { installRuntimeUpdateFixture, updateJob } from "./agent-runtime-update-helpers";
test.describe("managed agent runtime updates on mobile", () => {
test("uses a touch-safe drawer to preview and stream an update without horizontal overflow", async ({
testPage,
prCapture,
}) => {
const runtime = await installRuntimeUpdateFixture(testPage);
await testPage.goto("/settings/agents");
const trigger = testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`);
await expect(
testPage.getByTestId(`agent-update-available-dot-${runtime.agentName}`),
).toBeVisible();
await trigger.scrollIntoViewIfNeeded();
const triggerBox = await trigger.boundingBox();
expect(triggerBox).not.toBeNull();
expect(triggerBox!.height).toBeGreaterThanOrEqual(44);
expect(triggerBox!.width).toBeGreaterThanOrEqual(44);
await trigger.tap();
const drawer = testPage.getByTestId(`agent-update-drawer-${runtime.agentName}`);
await expect(drawer).toBeVisible();
await expect(drawer).toContainText("0.62.0 → 0.63.0");
const body = drawer.getByTestId(`agent-update-dialog-body-${runtime.agentName}`);
await expect
.poll(() => body.evaluate((element) => element.scrollHeight <= element.clientHeight))
.toBe(true);
expect(runtime.postCount()).toBe(0);
await prCapture.screenshot("mobile-update-preview", {
caption: "Mobile update preview before approval",
});
await testPage.getByTestId(`agent-update-confirm-${runtime.agentName}`).tap();
expect(runtime.postCount()).toBe(1);
expect(runtime.postTargets()).toEqual(["0.63.0"]);
await runtime.emitUpdate(
updateJob({
output: Array.from(
{ length: 60 },
(_, index) => `Downloading runtime chunk ${index + 1} for the latest agent package`,
).join("\n"),
}),
);
await expect(drawer.getByTestId(`agent-update-phase-${runtime.agentName}`)).toContainText(
"Updating runtime",
);
await body.scrollIntoViewIfNeeded();
await expect(
body.evaluate((element) => element.scrollHeight > element.clientHeight),
).resolves.toBe(true);
await expect(testPage.locator("html")).toHaveJSProperty(
"scrollWidth",
await testPage.locator("html").evaluate((element) => element.clientWidth),
);
});
test("shows an up-to-date preview and disables approval when versions match", async ({
testPage,
prCapture,
}) => {
const runtime = await installRuntimeUpdateFixture(testPage, {
previewResponse: {
agent_name: "claude-acp",
package: "@agentclientprotocol/claude-agent-acp",
current_version: "0.64.0",
target_version: "0.64.0",
operation: "up_to_date",
command: ["npm", "exec"],
command_string: "npm exec",
},
});
await testPage.goto("/settings/agents");
await testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`).tap();
const drawer = testPage.getByTestId(`agent-update-drawer-${runtime.agentName}`);
await expect(
drawer.getByTestId(`agent-update-version-summary-${runtime.agentName}`).getByText("0.64.0", {
exact: true,
}),
).toBeVisible();
await expect(drawer.getByRole("status").filter({ hasText: "Up to date" })).toBeVisible();
await expect(drawer).not.toContainText("0.64.0 → 0.64.0");
await expect(testPage.getByTestId(`agent-update-confirm-${runtime.agentName}`)).toBeDisabled();
expect(runtime.postCount()).toBe(0);
await prCapture.screenshot("mobile-update-up-to-date", {
caption: "Mobile update preview when the managed runtime is up to date",
});
});
test("selects an older stable version for rollback in the drawer", async ({ testPage }) => {
const runtime = await installRuntimeUpdateFixture(testPage);
await testPage.goto("/settings/agents");
await testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`).tap();
const drawer = testPage.getByTestId(`agent-update-drawer-${runtime.agentName}`);
const selector = drawer.getByTestId(`agent-update-version-${runtime.agentName}`);
const selectorBox = await selector.boundingBox();
expect(selectorBox).not.toBeNull();
expect(selectorBox!.height).toBeGreaterThanOrEqual(44);
const drawerBefore = await settledBoundingBox(drawer);
await selector.tap();
const browser = testPage.getByTestId(`agent-update-version-browser-${runtime.agentName}`);
await expect(browser).toBeVisible();
await expect(browser).toHaveAttribute("data-slot", "popover-content");
const browserBox = await settledBoundingBox(browser);
const viewport = testPage.viewportSize();
expect(viewport).not.toBeNull();
expect(browserBox.x).toBeGreaterThanOrEqual(0);
expect(browserBox.y).toBeGreaterThanOrEqual(0);
expect(browserBox.x + browserBox.width).toBeLessThanOrEqual(viewport!.width);
expect(browserBox.y + browserBox.height).toBeLessThanOrEqual(viewport!.height);
const drawerAfter = await settledBoundingBox(drawer);
expect(Math.abs(drawerAfter.height - drawerBefore.height)).toBeLessThan(2);
expect(
await browser.evaluate(
(element) =>
element.closest('[data-slot="dialog-content"], [data-slot="drawer-content"]') === null,
),
).toBe(true);
const option = browser.getByTestId(`agent-update-version-option-${runtime.agentName}-0.61.0`);
const optionBox = await option.boundingBox();
expect(optionBox).not.toBeNull();
expect(optionBox!.height).toBeGreaterThanOrEqual(44);
await option.tap();
await expect(drawer).toContainText("0.62.0 → 0.61.0");
await expect(testPage.getByTestId(`agent-update-confirm-${runtime.agentName}`)).toHaveText(
"Roll back runtime",
);
await testPage.getByTestId(`agent-update-confirm-${runtime.agentName}`).tap();
expect(runtime.postTargets()).toEqual(["0.61.0"]);
expect(runtime.previewTargets()).toEqual(["", "0.61.0"]);
});
test("searches the full version history in the drawer", async ({ testPage }) => {
const runtime = await installRuntimeUpdateFixture(testPage, {
previewResponse: {
agent_name: "claude-acp",
package: "@agentclientprotocol/claude-agent-acp",
current_version: "0.62.0",
default_version: "0.64.0",
active_version: "0.62.0",
effective_version: "0.62.0",
target_version: "0.63.0",
available_versions: Array.from({ length: 12 }, (_, index) => ({
version: `0.${74 - index}.0`,
latest: index === 0,
})),
command: ["npm", "exec"],
command_string: "npm exec",
},
});
await testPage.goto("/settings/agents");
await testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`).tap();
const drawer = testPage.getByTestId(`agent-update-drawer-${runtime.agentName}`);
await drawer.getByTestId(`agent-update-version-${runtime.agentName}`).tap();
const browser = testPage.getByTestId(`agent-update-version-browser-${runtime.agentName}`);
await expect(browser).toBeVisible();
const search = browser.getByPlaceholder("Search versions");
await expect(search).toBeVisible();
await search.fill("0.70");
const option = browser.getByTestId(`agent-update-version-option-${runtime.agentName}-0.70.0`);
await expect(option).toBeVisible();
// The popover entrance animation scales the option row briefly. Poll until
// the touch target reaches its settled 44px height before tapping it.
await expect
.poll(async () => Math.round((await option.boundingBox())?.height ?? 0))
.toBeGreaterThanOrEqual(44);
await option.tap();
await expect(drawer).toContainText("0.62.0 → 0.70.0");
expect(runtime.previewTargets()).toEqual(["", "0.70.0"]);
});
test("keeps retry available after an update job fails", async ({ testPage }) => {
const runtime = await installRuntimeUpdateFixture(testPage);
await testPage.goto("/settings/agents");
await testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`).tap();
const retryUpdate = testPage.getByTestId(`agent-update-confirm-${runtime.agentName}`);
await retryUpdate.tap();
expect(runtime.postCount()).toBe(1);
await runtime.emitUpdate(
updateJob({
status: "failed",
error: "The package registry is unavailable",
finished_at: "2026-07-26T12:01:00.000Z",
}),
);
await expect(retryUpdate).toHaveText("Retry update");
await retryUpdate.tap();
expect(runtime.postCount()).toBe(2);
});
});