305 lines
13 KiB
TypeScript
305 lines
13 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import type { Route } from "@playwright/test";
|
|
import { test, expect } from "../../fixtures/test-base";
|
|
import {
|
|
mockTemporaryArtifactOverview,
|
|
seedManagedGoCache,
|
|
} from "../../helpers/storage-maintenance";
|
|
import { MobileKanbanPage } from "../../pages/mobile-kanban-page";
|
|
|
|
test.describe("Mobile storage maintenance", () => {
|
|
test("keeps temporary artifact cleanup reachable with a touch-sized action", async ({
|
|
testPage,
|
|
prCapture,
|
|
}) => {
|
|
await mockTemporaryArtifactOverview(testPage);
|
|
await testPage.route("**/api/v1/system/storage/run", async (route) => {
|
|
expect(route.request().postDataJSON()).toEqual({ resources: ["temporary_artifacts"] });
|
|
await route.fulfill({
|
|
status: 202,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ job_id: "mobile-temporary-artifacts-cleanup" }),
|
|
});
|
|
});
|
|
await testPage.route("**/api/v1/system/jobs/**", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
id: "mobile-temporary-artifacts-cleanup",
|
|
kind: "storage-cleanup",
|
|
state: "succeeded",
|
|
started_at: new Date().toISOString(),
|
|
}),
|
|
});
|
|
});
|
|
|
|
await testPage.goto("/settings/system/storage");
|
|
const trigger = testPage.getByTestId("storage-resource-temporary-artifacts-trigger");
|
|
await trigger.tap();
|
|
const cleanButton = testPage.getByTestId("storage-temporary-artifacts-clean");
|
|
await expect(cleanButton).toBeVisible();
|
|
const box = await cleanButton.boundingBox();
|
|
expect(box).not.toBeNull();
|
|
expect(box!.height).toBeGreaterThanOrEqual(44);
|
|
await cleanButton.tap();
|
|
await expect(testPage.getByText("Clean stale Kandev artifacts?")).toBeVisible();
|
|
await prCapture.screenshot("temporary-artifacts-confirmation", {
|
|
caption: "Mobile storage keeps stale artifact cleanup in a reachable confirmation",
|
|
});
|
|
await testPage.getByTestId("storage-temporary-artifacts-confirm").tap();
|
|
await expect(testPage.getByTestId("storage-run-now")).toHaveAttribute(
|
|
"data-job-state",
|
|
"succeeded",
|
|
);
|
|
await expect
|
|
.poll(() =>
|
|
testPage.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth),
|
|
)
|
|
.toBe(true);
|
|
});
|
|
|
|
test("explains busy activity and allows Run anyway without horizontal overflow", async ({
|
|
testPage,
|
|
prCapture,
|
|
}) => {
|
|
let runAttempts = 0;
|
|
await testPage.route("**/api/v1/system/storage/run", async (route) => {
|
|
runAttempts += 1;
|
|
if (runAttempts === 1) {
|
|
await route.fulfill({
|
|
status: 409,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
error: "storage cleanup is blocked by active Kandev work",
|
|
busy_resources: [{ kind: "test_command", label: "A test command is running" }],
|
|
force_available: true,
|
|
}),
|
|
});
|
|
return;
|
|
}
|
|
await route.fulfill({
|
|
status: 202,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ job_id: "mobile-force-cleanup" }),
|
|
});
|
|
});
|
|
|
|
await testPage.goto("/settings/system/storage");
|
|
await testPage.getByTestId("storage-run-now").tap();
|
|
await expect(testPage.getByTestId("storage-busy")).toContainText("A test command is running");
|
|
await expect(testPage.getByTestId("storage-run-anyway")).toBeVisible();
|
|
expect(
|
|
await testPage.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth),
|
|
).toBe(true);
|
|
await prCapture.screenshot("busy-feedback", {
|
|
caption: "Mobile storage cleanup keeps the warning and Run anyway action in one column",
|
|
fullPage: true,
|
|
});
|
|
const forceRequest = testPage.waitForRequest(
|
|
(request) =>
|
|
request.method() === "POST" &&
|
|
new URL(request.url()).pathname === "/api/v1/system/storage/run" &&
|
|
request.postDataJSON()?.force === true,
|
|
);
|
|
await testPage.getByTestId("storage-run-anyway").tap();
|
|
expect((await forceRequest).postDataJSON()).toEqual({ force: true });
|
|
});
|
|
|
|
test("opens Storage from mobile navigation and analyzes without horizontal overflow", async ({
|
|
testPage,
|
|
backend,
|
|
}) => {
|
|
const cache = seedManagedGoCache(backend.tmpDir);
|
|
const externalGoCache = `${backend.tmpDir}/external-go-cache`;
|
|
fs.mkdirSync(externalGoCache, { recursive: true });
|
|
const mobile = new MobileKanbanPage(testPage);
|
|
await mobile.goto();
|
|
await mobile.mobileMenuButton.click();
|
|
await testPage.getByRole("link", { name: "Settings" }).click();
|
|
const index = testPage.getByTestId("settings-index");
|
|
await index.locator('a[href="/settings/system/storage"]').click();
|
|
|
|
await expect(testPage.getByTestId("storage-settings-page")).toBeVisible();
|
|
await expect(testPage.getByTestId("storage-disk-capacity-card")).toBeVisible();
|
|
await expect(testPage.getByRole("progressbar")).toBeVisible();
|
|
await testPage
|
|
.getByRole("button", { name: "More information about Scheduled maintenance" })
|
|
.click();
|
|
await expect(testPage.getByRole("tooltip")).toContainText(
|
|
"Turning it off does not disable Analyze or Run now",
|
|
);
|
|
await testPage.keyboard.press("Escape");
|
|
await testPage.getByTestId("storage-scheduling-enabled").click();
|
|
await testPage.getByTestId("storage-go-cache-enabled").click();
|
|
await testPage.getByTestId("storage-idle-period").fill("12");
|
|
await expect(testPage.getByTestId("storage-policy-section-schedule")).toHaveAttribute(
|
|
"data-settings-dirty",
|
|
"true",
|
|
);
|
|
await expect(testPage.getByTestId("settings-floating-save")).toBeVisible();
|
|
await testPage.getByRole("button", { name: "Save changes" }).click();
|
|
await expect(testPage.getByText("Storage policy saved")).toBeVisible();
|
|
const analyzedTime = testPage.locator("time[datetime]").filter({ hasText: "Last analyzed" });
|
|
await expect(analyzedTime).toHaveText(/^Last analyzed .+/);
|
|
await testPage.getByTestId("storage-analyze").click();
|
|
await expect(testPage.getByTestId("storage-analyze")).toHaveAttribute(
|
|
"data-job-state",
|
|
"succeeded",
|
|
);
|
|
await expect(testPage.getByTestId("storage-analyze")).toHaveText("Analysis complete");
|
|
await testPage.getByTestId("storage-resource-workspaces-trigger").click();
|
|
await expect(testPage.getByTestId("storage-resource-workspaces")).toBeVisible();
|
|
await testPage.getByTestId("storage-resource-unmanaged-go-cache-trigger").click();
|
|
await expect(testPage.getByTestId("storage-resource-unmanaged-go-cache")).toBeVisible();
|
|
await testPage.getByTestId("storage-resource-docker-image-layers-trigger").click();
|
|
await expect(testPage.getByTestId("storage-resource-docker-image-layers")).toBeVisible();
|
|
await testPage.getByTestId("storage-resource-go-cache-trigger").click();
|
|
const explicitRequest = testPage.waitForRequest(
|
|
(request) =>
|
|
request.method() === "POST" &&
|
|
new URL(request.url()).pathname === "/api/v1/system/storage/run",
|
|
);
|
|
await testPage.getByTestId("storage-go-cache-clean").click();
|
|
expect((await explicitRequest).postDataJSON()).toEqual({ resources: ["go_cache"] });
|
|
await expect.poll(() => fs.existsSync(cache.artifact)).toBe(false);
|
|
await testPage.getByTestId("storage-go-cache-adopt-path").fill(externalGoCache);
|
|
await testPage.getByTestId("storage-go-cache-adopt").click();
|
|
await testPage.getByTestId("storage-go-cache-adopt-confirm-confirmation").fill("ADOPT");
|
|
await testPage.getByTestId("storage-go-cache-adopt-confirm").click();
|
|
await expect(testPage.getByText("Go build cache adopted")).toBeVisible();
|
|
await testPage.reload();
|
|
await expect(testPage.getByTestId("storage-go-cache-adopt-path")).toHaveValue(externalGoCache);
|
|
await expect(testPage.getByTestId("storage-dependency-allowlist")).toContainText(".yarn/cache");
|
|
await testPage
|
|
.getByRole("button", { name: "More information about Folders Kandev will check" })
|
|
.tap();
|
|
await expect(testPage.getByRole("tooltip")).toContainText("recursively");
|
|
await testPage.reload();
|
|
await expect(testPage.getByTestId("storage-settings-page")).toBeVisible();
|
|
await testPage.getByRole("button", { name: "More information about Quarantine" }).click();
|
|
await expect(testPage.getByRole("tooltip")).toContainText("recoverable holding area");
|
|
await expect
|
|
.poll(() =>
|
|
testPage.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth),
|
|
)
|
|
.toBe(true);
|
|
});
|
|
|
|
test("shows progress while storage data is loading", async ({ testPage, prCapture }) => {
|
|
let overviewRequestStarted = false;
|
|
let releaseOverview: () => void = () => {};
|
|
let markOverviewObserved: () => void = () => {};
|
|
let markOverviewSettled: () => void = () => {};
|
|
const overviewGate = new Promise<void>((resolve) => {
|
|
releaseOverview = resolve;
|
|
});
|
|
const overviewObserved = new Promise<void>((resolve) => {
|
|
markOverviewObserved = resolve;
|
|
});
|
|
const overviewSettled = new Promise<void>((resolve) => {
|
|
markOverviewSettled = resolve;
|
|
});
|
|
const overviewPattern = "**/api/v1/system/storage";
|
|
const holdOverview = async (route: Route) => {
|
|
overviewRequestStarted = true;
|
|
markOverviewObserved();
|
|
await overviewGate;
|
|
try {
|
|
await route.continue();
|
|
} finally {
|
|
markOverviewSettled();
|
|
}
|
|
};
|
|
|
|
await testPage.route(overviewPattern, holdOverview);
|
|
try {
|
|
await testPage.goto("/settings/system/storage");
|
|
await overviewObserved;
|
|
|
|
const spinner = testPage.getByTestId("storage-overview-spinner");
|
|
await expect(spinner).toBeVisible();
|
|
await expect(testPage.getByText("Loading storage data…")).toBeVisible();
|
|
await expect(testPage.getByTestId("storage-overview-card")).toBeVisible();
|
|
await expect(testPage.getByTestId("storage-policy-card")).toBeVisible();
|
|
await expect(testPage.getByTestId("storage-run-history")).toBeVisible();
|
|
await expect(testPage.getByTestId("storage-quarantine-card")).toBeVisible();
|
|
await expect(testPage.getByTestId("storage-analysis-total")).toHaveCount(0);
|
|
await expect(testPage.getByTestId("toast-message")).toHaveCount(0);
|
|
await prCapture.screenshot("progressive-loading", {
|
|
caption: "Mobile storage keeps policy, history, and quarantine visible during analysis",
|
|
fullPage: true,
|
|
});
|
|
expect(
|
|
await testPage.evaluate(() => document.documentElement.scrollWidth > window.innerWidth),
|
|
).toBe(false);
|
|
|
|
releaseOverview();
|
|
await expect(spinner).toBeHidden();
|
|
await expect(testPage.getByText("Storage analysis")).toBeVisible();
|
|
await expect(testPage.getByTestId("storage-analysis-total")).toBeVisible();
|
|
await expect(testPage.getByTestId("storage-quarantine-total")).toBeVisible();
|
|
} finally {
|
|
releaseOverview();
|
|
if (overviewRequestStarted) await overviewSettled;
|
|
await testPage.unroute(overviewPattern, holdOverview);
|
|
}
|
|
});
|
|
|
|
test("keeps both quarantine cleanup actions reachable on a phone", async ({
|
|
testPage,
|
|
prCapture,
|
|
}) => {
|
|
const entry = {
|
|
id: "mobile-protected",
|
|
resource_type: "task_workspace",
|
|
original_path: "/tmp/mobile-protected",
|
|
quarantine_path: "/tmp/trash/mobile-protected",
|
|
size_bytes: 1024,
|
|
state: "quarantined",
|
|
quarantined_at: "2026-07-29T00:00:00Z",
|
|
delete_after: new Date(Date.now() + 86_400_000).toISOString(),
|
|
last_error: "",
|
|
metadata: {},
|
|
};
|
|
await testPage.route("**/api/v1/system/storage/quarantine", async (route) => {
|
|
if (route.request().method() === "GET") {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ entries: [entry] }),
|
|
});
|
|
return;
|
|
}
|
|
expect(route.request().method()).toBe("DELETE");
|
|
expect(route.request().postDataJSON()).toEqual({ scope: "all", confirm: "DELETE ALL NOW" });
|
|
await route.fulfill({
|
|
status: 202,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ job_id: "mobile-force-purge" }),
|
|
});
|
|
});
|
|
await testPage.goto("/settings/system/storage");
|
|
await expect(testPage.getByTestId("storage-quarantine-force-clear")).toBeVisible();
|
|
await testPage.getByTestId("storage-quarantine-card").scrollIntoViewIfNeeded();
|
|
await prCapture.screenshot("quarantine-actions", {
|
|
caption: "Mobile quarantine keeps both cleanup actions reachable",
|
|
});
|
|
if (process.env.CAPTURE_PR_ASSETS) {
|
|
await testPage.getByTestId("storage-quarantine-card").screenshot({
|
|
path: path.resolve(process.cwd(), ".pr-assets/mobile-quarantine-card.png"),
|
|
});
|
|
}
|
|
await testPage.getByTestId("storage-quarantine-force-clear").tap();
|
|
await testPage
|
|
.getByTestId("storage-quarantine-force-clear-confirm-confirmation")
|
|
.fill("DELETE ALL NOW");
|
|
await testPage.getByTestId("storage-quarantine-force-clear-confirm").tap();
|
|
await expect(testPage.getByText("Forced quarantine cleanup started")).toBeVisible();
|
|
expect(
|
|
await testPage.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth),
|
|
).toBe(true);
|
|
});
|
|
});
|