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

533 lines
22 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", () => {
test("previews, approves, and streams an update without putting details on the card", async ({
testPage,
prCapture,
}) => {
const runtime = await installRuntimeUpdateFixture(testPage);
let documentNavigations = 0;
testPage.on("framenavigated", (frame) => {
if (frame === testPage.mainFrame()) documentNavigations += 1;
});
await testPage.goto("/settings/agents");
const navigationsAfterLoad = documentNavigations;
const trigger = testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`);
await expect(trigger).toBeVisible();
await expect(
testPage.getByTestId(`agent-update-available-dot-${runtime.agentName}`),
).toBeVisible();
await expect(trigger).toHaveAttribute("aria-label", /0\.62\.0.*0\.64\.0/);
await expect(testPage.getByTestId(`agent-update-control-${runtime.agentName}`)).toHaveCount(0);
const profileAction = testPage.getByRole("link", { name: "Setup Profile" });
const [profileActionBox, triggerBox] = await Promise.all([
profileAction.boundingBox(),
trigger.boundingBox(),
]);
expect(profileActionBox).not.toBeNull();
expect(triggerBox).not.toBeNull();
expect(Math.abs(profileActionBox!.y - triggerBox!.y)).toBeLessThanOrEqual(8);
// The update trigger leads the row; "Setup Profile" follows it.
expect(triggerBox!.x).toBeLessThan(profileActionBox!.x);
expect(profileActionBox!.height).toBe(28);
expect(triggerBox!.height).toBe(profileActionBox!.height);
await trigger.click();
const dialog = testPage.getByTestId(`agent-update-dialog-${runtime.agentName}`);
await expect(dialog).toBeVisible();
const dialogBox = await dialog.boundingBox();
expect(dialogBox).not.toBeNull();
expect(dialogBox!.width).toBeGreaterThanOrEqual(640);
const selectorBox = await dialog
.getByTestId(`agent-update-version-${runtime.agentName}`)
.boundingBox();
expect(selectorBox).not.toBeNull();
expect(selectorBox!.height).toBeGreaterThanOrEqual(44);
const body = dialog.getByTestId(`agent-update-dialog-body-${runtime.agentName}`);
await expect
.poll(() => body.evaluate((element) => element.scrollHeight <= element.clientHeight))
.toBe(true);
const quickLatestBox = await dialog
.getByTestId(`agent-update-quick-latest-${runtime.agentName}`)
.boundingBox();
expect(quickLatestBox).not.toBeNull();
expect(quickLatestBox!.height).toBeLessThan(44);
await expect(dialog).toContainText("0.62.0 → 0.63.0");
await expect(dialog).toContainText(
'npm exec --yes --prefer-online --package=@agentclientprotocol/claude-agent-acp -- node -e ""',
);
await expect(dialog).toContainText("Active sessions keep running");
expect(runtime.previewCount()).toBe(1);
expect(runtime.postCount()).toBe(0);
await prCapture.screenshot("desktop-update-preview", {
caption: "Desktop update preview before approval",
});
await testPage.getByTestId(`agent-update-confirm-${runtime.agentName}`).click();
expect(runtime.postCount()).toBe(1);
expect(runtime.postTargets()).toEqual(["0.63.0"]);
await runtime.emitUpdate(updateJob());
await runtime.emitOutput("Installed @agentclientprotocol/claude-agent-acp@0.63.0\n");
await expect(dialog.getByTestId(`agent-update-phase-${runtime.agentName}`)).toContainText(
"Updating runtime",
);
await expect(dialog.getByTestId(`agent-update-log-${runtime.agentName}`)).toContainText(
"Installed @agentclientprotocol/claude-agent-acp@0.63.0",
);
await runtime.emitUpdate(
updateJob({
status: "succeeded",
output: "Installed @agentclientprotocol/claude-agent-acp@0.63.0\n",
finished_at: "2026-07-26T12:01:00.000Z",
}),
);
runtime.setStatusResponse([
{
agent_name: runtime.agentName,
package: "@agentclientprotocol/claude-agent-acp",
default_version: "0.64.0",
active_version: "0.63.0",
effective_version: "0.63.0",
latest_version: "0.63.0",
checked_at: "2026-07-26T12:01:00.000Z",
check_state: "up_to_date",
},
]);
runtime.setPersistedRuntimeVersion("0.63.0");
await runtime.emitCatalogue([
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "claude-opus-5", name: "Claude Opus 5" },
]);
await expect(testPage.getByText("Claude refreshed", { exact: true })).toBeVisible();
await expect(
testPage.getByTestId(`agent-update-available-dot-${runtime.agentName}`),
).toHaveCount(0);
await expect(dialog.getByTestId(`agent-update-result-${runtime.agentName}`)).toContainText(
"Runtime updated successfully",
);
expect(documentNavigations).toBe(navigationsAfterLoad);
await testPage.reload();
await expect(trigger).toBeVisible();
await expect(testPage.getByTestId(`agent-update-dialog-${runtime.agentName}`)).toHaveCount(0);
await expect(testPage.getByTestId(`agent-update-result-${runtime.agentName}`)).toHaveCount(0);
await trigger.click();
await expect(testPage.getByTestId(`agent-update-dialog-${runtime.agentName}`)).toContainText(
"Active version: 0.63.0",
);
});
test("requires a current runtime version before approval", async ({ testPage }) => {
const runtime = await installRuntimeUpdateFixture(testPage, {
previewResponse: {
agent_name: "claude-acp",
package: "@agentclientprotocol/claude-agent-acp",
current_version: "",
target_version: "0.63.0",
command: ["npm", "exec"],
command_string: "npm exec",
},
});
await testPage.goto("/settings/agents");
await testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`).click();
const dialog = testPage.getByTestId(`agent-update-dialog-${runtime.agentName}`);
await expect(dialog).toContainText("Unknown → 0.63.0");
await expect(testPage.getByTestId(`agent-update-confirm-${runtime.agentName}`)).toBeDisabled();
expect(runtime.postCount()).toBe(0);
});
test("keeps the control usable when the latest-version check is unknown", async ({
testPage,
}) => {
const runtime = await installRuntimeUpdateFixture(testPage, {
statusResponse: [
{
agent_name: "claude-acp",
package: "@agentclientprotocol/claude-agent-acp",
default_version: "0.64.0",
active_version: "0.62.0",
effective_version: "0.62.0",
check_state: "unknown",
},
],
});
await testPage.goto("/settings/agents");
const trigger = testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`);
await expect(trigger).toBeVisible();
await expect(
testPage.getByTestId(`agent-update-available-dot-${runtime.agentName}`),
).toHaveCount(0);
await expect(trigger).toHaveAttribute("aria-label", /latest version.*unavailable/i);
await trigger.click();
await expect(testPage.getByTestId(`agent-update-dialog-${runtime.agentName}`)).toBeVisible();
expect(runtime.postCount()).toBe(0);
});
test("uses a structural request to return to the Kandev default", async ({ testPage }) => {
const runtime = await installRuntimeUpdateFixture(testPage);
await testPage.goto("/settings/agents");
await testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`).click();
const dialog = testPage.getByTestId(`agent-update-dialog-${runtime.agentName}`);
await dialog.getByTestId(`agent-update-quick-default-${runtime.agentName}`).click();
await expect(dialog).toContainText("0.62.0 → 0.64.0");
await expect(testPage.getByTestId(`agent-update-confirm-${runtime.agentName}`)).toHaveText(
"Use Kandev default",
);
expect(runtime.previewTargets()).toEqual(["", ""]);
expect(runtime.previewDefaultCount()).toBe(1);
await testPage.getByTestId(`agent-update-confirm-${runtime.agentName}`).click();
expect(runtime.postTargets()).toEqual(["__kandev_default__"]);
});
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}`).click();
const dialog = testPage.getByTestId(`agent-update-dialog-${runtime.agentName}`);
await expect(
dialog.getByTestId(`agent-update-version-summary-${runtime.agentName}`).getByText("0.64.0", {
exact: true,
}),
).toBeVisible();
await expect(dialog.getByRole("status").filter({ hasText: "Up to date" })).toBeVisible();
await expect(dialog).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("desktop-update-up-to-date", {
caption: "Desktop update preview when the managed runtime is up to date",
});
});
test("keeps repair distinct when observed and target versions match", async ({ testPage }) => {
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: "repair",
command: ["npm", "exec"],
command_string: "npm exec",
},
});
await testPage.goto("/settings/agents");
await testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`).click();
const dialog = testPage.getByTestId(`agent-update-dialog-${runtime.agentName}`);
const summary = dialog.getByTestId(`agent-update-version-summary-${runtime.agentName}`);
await expect(summary).toContainText("Repair runtime");
await expect(summary).toContainText("0.64.0 → 0.64.0");
await expect(summary.getByText("Up to date", { exact: true })).toHaveCount(0);
const confirm = testPage.getByTestId(`agent-update-confirm-${runtime.agentName}`);
await expect(confirm).toBeEnabled();
await confirm.click();
await runtime.emitUpdate(
updateJob({
status: "succeeded",
operation: "repair",
current_version: "0.64.0",
target_version: "0.64.0",
finished_at: "2026-07-26T12:01:00.000Z",
}),
);
await expect(dialog.getByTestId(`agent-update-result-${runtime.agentName}`)).toContainText(
"Runtime updated successfully",
);
});
test("selects an older stable version for rollback", async ({ testPage }) => {
const runtime = await installRuntimeUpdateFixture(testPage);
await testPage.goto("/settings/agents");
await testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`).click();
const dialog = testPage.getByTestId(`agent-update-dialog-${runtime.agentName}`);
await testPage.getByTestId(`agent-update-version-${runtime.agentName}`).click();
await testPage
.getByTestId(`agent-update-version-browser-${runtime.agentName}`)
.getByTestId(`agent-update-version-option-${runtime.agentName}-0.61.0`)
.click();
await expect(dialog).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}`).click();
expect(runtime.postTargets()).toEqual(["0.61.0"]);
expect(runtime.previewTargets()).toEqual(["", "0.61.0"]);
});
test("keeps the dialog content visible while changing versions", async ({ testPage }) => {
const runtime = await installRuntimeUpdateFixture(testPage, { previewDelayMs: 500 });
await testPage.goto("/settings/agents");
await testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`).click();
const dialog = testPage.getByTestId(`agent-update-dialog-${runtime.agentName}`);
const body = dialog.getByTestId(`agent-update-dialog-body-${runtime.agentName}`);
await dialog.getByTestId(`agent-update-version-${runtime.agentName}`).click();
await testPage
.getByTestId(`agent-update-version-browser-${runtime.agentName}`)
.getByTestId(`agent-update-version-option-${runtime.agentName}-0.61.0`)
.click();
await expect(
dialog.getByTestId(`agent-update-version-loading-${runtime.agentName}`),
).toBeVisible();
await expect(body).toContainText("Active sessions keep running");
await expect(body).toContainText("Command that will run");
await expect(dialog.getByTestId(`agent-update-version-${runtime.agentName}`)).toBeDisabled();
await expect(
dialog.getByTestId(`agent-update-version-loading-${runtime.agentName}`),
).toBeHidden();
});
test("shows the complete backend version projection", async ({ testPage }) => {
const runtime = await installRuntimeUpdateFixture(testPage);
runtime.setPreviewResponse({
agent_name: "claude-acp",
package: "@agentclientprotocol/claude-agent-acp",
current_version: "0.62.0",
target_version: "0.74.0",
operation: "update",
active_version: "0.62.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}`).click();
const dialog = testPage.getByTestId(`agent-update-dialog-${runtime.agentName}`);
const selector = testPage.getByTestId(`agent-update-version-${runtime.agentName}`);
await expect(
testPage.getByTestId(`agent-update-version-option-${runtime.agentName}-0.74.0`),
).toHaveCount(0);
const dialogBefore = await settledBoundingBox(dialog);
await selector.click();
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 dialogAfter = await settledBoundingBox(dialog);
expect(Math.abs(dialogAfter.height - dialogBefore.height)).toBeLessThan(2);
expect(
await browser.evaluate(
(element) =>
element.closest('[data-slot="dialog-content"], [data-slot="drawer-content"]') === null,
),
).toBe(true);
const options = browser.getByRole("option");
await expect(options).toHaveCount(12);
expect(
await options.evaluateAll((elements) =>
elements.map(
(element) => element.getAttribute("data-value") ?? element.textContent?.trim(),
),
),
).toEqual([
"0.74.0",
"0.73.0",
"0.72.0",
"0.71.0",
"0.70.0",
"0.69.0",
"0.68.0",
"0.67.0",
"0.66.0",
"0.65.0",
"0.64.0",
"0.63.0",
]);
});
test("retries a failed preview for the selected target", async ({ testPage }) => {
const runtime = await installRuntimeUpdateFixture(testPage, {
previewFailures: ["0.61.0"],
});
await testPage.goto("/settings/agents");
await testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`).click();
const dialog = testPage.getByTestId(`agent-update-dialog-${runtime.agentName}`);
await testPage.getByTestId(`agent-update-version-${runtime.agentName}`).click();
await testPage
.getByTestId(`agent-update-version-browser-${runtime.agentName}`)
.getByTestId(`agent-update-version-option-${runtime.agentName}-0.61.0`)
.click();
await expect(dialog.getByRole("alert")).toContainText("preview temporarily unavailable");
await dialog.getByRole("button", { name: "Retry version check" }).click();
await expect(dialog).toContainText("0.62.0 → 0.61.0");
expect(runtime.previewTargets()).toEqual(["", "0.61.0", "0.61.0"]);
});
test("uses the job operation for a stale retry decision", async ({ testPage }) => {
const runtime = await installRuntimeUpdateFixture(testPage, {
postResponse: updateJob({
status: "failed",
operation: "up_to_date",
active_version: "0.63.0",
current_version: "0.63.0",
target_version: "0.63.0",
error: "The package registry is unavailable",
finished_at: "2026-07-26T12:01:00.000Z",
}),
});
await testPage.goto("/settings/agents");
await testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`).click();
const dialog = testPage.getByTestId(`agent-update-dialog-${runtime.agentName}`);
const retryUpdate = testPage.getByTestId(`agent-update-confirm-${runtime.agentName}`);
await retryUpdate.click();
await expect(
dialog.getByTestId(`agent-update-version-summary-${runtime.agentName}`).getByText("0.63.0", {
exact: true,
}),
).toBeVisible();
await expect(dialog.getByRole("status").filter({ hasText: "Up to date" })).toBeVisible();
await expect(retryUpdate).toHaveText("Retry update");
await expect(retryUpdate).toBeDisabled();
expect(runtime.postCount()).toBe(1);
});
test("retries an update after its job fails", async ({ testPage }) => {
const runtime = await installRuntimeUpdateFixture(testPage);
await testPage.goto("/settings/agents");
await testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`).click();
const retryUpdate = testPage.getByTestId(`agent-update-confirm-${runtime.agentName}`);
await retryUpdate.click();
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 expect(retryUpdate).toBeEnabled();
await retryUpdate.click();
expect(runtime.postCount()).toBe(2);
});
test("shows the newly activated version after a successful job", async ({ testPage }) => {
const runtime = await installRuntimeUpdateFixture(testPage);
await testPage.goto("/settings/agents");
await testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`).click();
const dialog = testPage.getByTestId(`agent-update-dialog-${runtime.agentName}`);
await testPage.getByTestId(`agent-update-confirm-${runtime.agentName}`).click();
await runtime.emitUpdate(
updateJob({
status: "succeeded",
current_version: "0.63.0",
active_version: "0.63.0",
target_version: "0.63.0",
finished_at: "2026-07-26T12:01:00.000Z",
}),
);
await expect(dialog).toContainText("Active version: 0.63.0");
await dialog.getByTestId(`agent-update-version-${runtime.agentName}`).click();
const successOptionText = await testPage
.getByTestId(`agent-update-version-browser-${runtime.agentName}`)
.getByRole("option")
.allTextContents();
expect(
successOptionText.some((text) => text.includes("0.63.0") && text.includes("active")),
).toBe(true);
});
test("clears the active version after a successful default reset", async ({ testPage }) => {
const runtime = await installRuntimeUpdateFixture(testPage, {
postResponse: updateJob({
status: "succeeded",
operation: "use_default",
current_version: "0.64.0",
default_version: "0.64.0",
effective_version: "0.64.0",
target_version: "0.64.0",
finished_at: "2026-07-26T12:01:00.000Z",
}),
});
await testPage.goto("/settings/agents");
await testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`).click();
const dialog = testPage.getByTestId(`agent-update-dialog-${runtime.agentName}`);
await dialog.getByTestId(`agent-update-quick-default-${runtime.agentName}`).click();
await testPage.getByTestId(`agent-update-confirm-${runtime.agentName}`).click();
await expect(dialog).toContainText("Effective version: 0.64.0");
await expect(dialog).not.toContainText("Active version: 0.62.0");
});
test("keeps the previous active version after a failed activation", async ({ testPage }) => {
const runtime = await installRuntimeUpdateFixture(testPage);
await testPage.goto("/settings/agents");
await testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`).click();
const dialog = testPage.getByTestId(`agent-update-dialog-${runtime.agentName}`);
await testPage.getByTestId(`agent-update-confirm-${runtime.agentName}`).click();
await runtime.emitUpdate(
updateJob({
status: "failed",
active_version: "0.62.0",
error: "candidate ACP probe failed",
finished_at: "2026-07-26T12:01:00.000Z",
}),
);
await expect(dialog).toContainText("Active version: 0.62.0");
await dialog.getByTestId(`agent-update-version-${runtime.agentName}`).click();
const failedOptionText = await testPage
.getByTestId(`agent-update-version-browser-${runtime.agentName}`)
.getByRole("option")
.allTextContents();
expect(
failedOptionText.some((text) => text.includes("0.62.0") && text.includes("active")),
).toBe(true);
expect(
failedOptionText.some((text) => text.includes("0.63.0") && text.includes("active")),
).toBe(false);
});
});