Files
kandev/apps/web/e2e/tests/system/storage-maintenance.spec.ts
T
2026-09-04 00:37:41 +00:00

459 lines
18 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";
function seedOrphanWorkspace(tmpDir: string): { root: string; artifact: string } {
const root = path.join(tmpDir, ".kandev", "tasks", "e2e-storage-orphan_abc");
const artifact = path.join(root, "repo", "node_modules", "fixture", "index.js");
fs.mkdirSync(path.dirname(artifact), { recursive: true });
fs.writeFileSync(artifact, "orphan-node-modules-fixture");
fs.writeFileSync(
path.join(root, ".kandev-workspace.json"),
JSON.stringify({
task_id: "e2e-storage-orphan",
workspace_id: "e2e-orphan-workspace",
task_dir_name: "e2e-storage-orphan_abc",
layout_version: 1,
created_at: "2026-06-01T00:00:00Z",
}),
);
const old = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000);
fs.utimesSync(root, old, old);
return { root, artifact };
}
test.describe("System storage maintenance", () => {
test("shows registered temporary artifacts and cleans them only through quarantine", async ({
testPage,
prCapture,
}) => {
await mockTemporaryArtifactOverview(testPage);
await testPage.route("**/api/v1/system/storage/run", async (route) => {
expect(route.request().method()).toBe("POST");
expect(route.request().postDataJSON()).toEqual({ resources: ["temporary_artifacts"] });
await route.fulfill({
status: 202,
contentType: "application/json",
body: JSON.stringify({ job_id: "temporary-artifacts-cleanup" }),
});
});
await testPage.route("**/api/v1/system/jobs/**", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
id: "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 expect(trigger).toContainText("0.05 GB");
await trigger.click();
await expect(testPage.getByTestId("storage-resource-temporary-artifacts")).toContainText(
"1 stale eligible",
);
const cleanButton = testPage.getByTestId("storage-temporary-artifacts-clean");
await expect(cleanButton).toBeEnabled();
await cleanButton.click();
await expect(testPage.getByText("Clean stale Kandev artifacts?")).toBeVisible();
await prCapture.screenshot("temporary-artifacts-confirmation", {
caption: "Desktop storage confirms stale registered artifacts before quarantine",
});
await testPage.getByTestId("storage-temporary-artifacts-confirm").click();
await expect(testPage.getByTestId("storage-run-now")).toHaveAttribute(
"data-job-state",
"succeeded",
{ timeout: 30_000 },
);
});
test("cleans a disabled managed Go cache only through its explicit action", async ({
testPage,
backend,
}) => {
const cache = seedManagedGoCache(backend.tmpDir);
expect(fs.statSync(cache.artifact).size).toBeGreaterThan(15 * 1024 * 1024 * 1024);
const overviewResponse = testPage.waitForResponse(
(response) => new URL(response.url()).pathname === "/api/v1/system/storage",
);
await testPage.goto("/settings/system/storage");
await overviewResponse;
// The preceding temporary-artifact scenario loads and caches a Storage
// snapshot. Force a fresh read after seeding this test's cache so this
// assertion exercises the filesystem fixture rather than the prior page.
await testPage.getByTestId("storage-analyze").click();
await expect(testPage.getByTestId("storage-analyze")).toHaveAttribute(
"data-job-state",
"succeeded",
);
const overview = await testPage.evaluate(async () => {
const response = await fetch("/api/v1/system/storage");
return response.json();
});
expect(overview.summary.go_cache).toMatchObject({ owned: true });
expect(overview.summary.go_cache.size_bytes).toBeGreaterThan(15 * 1024 * 1024 * 1024);
await testPage.getByTestId("storage-resource-go-cache-trigger").click();
const cleanButton = testPage.getByTestId("storage-go-cache-clean");
await expect(cleanButton).toBeEnabled();
const globalRequest = testPage.waitForRequest(
(request) =>
request.method() === "POST" &&
new URL(request.url()).pathname === "/api/v1/system/storage/run",
);
await testPage.getByTestId("storage-run-now").click();
expect((await globalRequest).postDataJSON()).toEqual({});
await expect(testPage.getByTestId("storage-run-now")).toHaveAttribute(
"data-job-state",
"succeeded",
);
expect(fs.existsSync(cache.artifact)).toBe(true);
const explicitRequest = testPage.waitForRequest(
(request) =>
request.method() === "POST" &&
new URL(request.url()).pathname === "/api/v1/system/storage/run",
);
await cleanButton.click();
expect((await explicitRequest).postDataJSON()).toEqual({ resources: ["go_cache"] });
await expect.poll(() => fs.existsSync(cache.artifact)).toBe(false);
});
test("reuses the cached snapshot until Analyze refreshes it", async ({ testPage }) => {
await testPage.goto("/settings/system/storage");
const analyzedTime = testPage.locator("time[datetime]").filter({ hasText: "Last analyzed" });
await expect(analyzedTime).toHaveText(/^Last analyzed .+/);
const initialAnalyzedAt = await analyzedTime.getAttribute("datetime");
expect(initialAnalyzedAt).toBeTruthy();
await testPage.reload();
await expect(analyzedTime).toHaveAttribute("datetime", initialAnalyzedAt!);
const scheduling = testPage.getByTestId("storage-scheduling-enabled");
const initialSchedulingState = await scheduling.getAttribute("data-state");
try {
await scheduling.click();
await testPage.getByRole("button", { name: "Save changes" }).click();
await expect(testPage.getByText("Storage policy saved")).toBeVisible();
await expect(analyzedTime).toHaveAttribute("datetime", initialAnalyzedAt!);
await testPage.getByTestId("storage-analyze").click();
await expect(testPage.getByTestId("storage-analyze")).toHaveAttribute(
"data-job-state",
"succeeded",
);
await expect.poll(() => analyzedTime.getAttribute("datetime")).not.toBe(initialAnalyzedAt);
} finally {
if ((await scheduling.getAttribute("data-state")) !== initialSchedulingState) {
await scheduling.click();
await testPage.getByRole("button", { name: "Save changes" }).click();
await expect(testPage.getByText("Storage policy saved")).toBeVisible();
}
}
});
test("shows fast storage sections while the overview scan is pending", 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) => {
const request = route.request();
const pathname = new URL(request.url()).pathname;
if (request.method() !== "GET" || pathname !== "/api/v1/system/storage") {
await route.continue();
return;
}
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;
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-disk-capacity-card")).toBeVisible();
await expect(testPage.getByRole("progressbar")).toBeVisible();
await expect(testPage.getByTestId("storage-dependency-allowlist")).toContainText(
"node_modules",
);
await expect(testPage.getByTestId("storage-overview-spinner")).toBeVisible();
await expect(testPage.getByTestId("storage-analysis-total")).toHaveCount(0);
await expect(testPage.getByTestId("toast-message")).toHaveCount(0);
await prCapture.screenshot("progressive-loading", {
caption: "Desktop storage shows policy, history, and quarantine while analysis scans",
fullPage: true,
});
releaseOverview();
await expect(testPage.getByTestId("storage-analysis-total")).toBeVisible();
await expect(testPage.getByTestId("storage-quarantine-total")).toBeVisible();
await expect(testPage.getByTestId("storage-overview-spinner")).toHaveCount(0);
} finally {
releaseOverview();
if (overviewRequestStarted) await overviewSettled;
await testPage.unroute(overviewPattern, holdOverview);
}
});
test("persists policy and analyzes, quarantines, and restores an orphan workspace", async ({
testPage,
backend,
}) => {
const orphan = seedOrphanWorkspace(backend.tmpDir);
await testPage.goto("/settings/system/storage");
const overviewBox = await testPage.getByTestId("storage-overview-card").boundingBox();
const policyBox = await testPage.getByTestId("storage-policy-card").boundingBox();
expect(overviewBox).not.toBeNull();
expect(policyBox).not.toBeNull();
expect(policyBox!.y).toBeGreaterThanOrEqual(overviewBox!.y + overviewBox!.height);
const scheduling = testPage.getByTestId("storage-scheduling-enabled");
await expect(scheduling).toHaveAttribute("data-state", "unchecked");
await expect(testPage.getByTestId("storage-check-interval")).toHaveValue("24");
await expect(testPage.getByTestId("storage-check-interval")).toBeDisabled();
await expect(testPage.getByTestId("storage-idle-period")).toBeDisabled();
await scheduling.click();
await expect(testPage.getByTestId("storage-check-interval")).toBeEnabled();
await expect(testPage.getByTestId("storage-idle-period")).toBeEnabled();
await testPage.getByTestId("storage-idle-period").fill("11");
await expect(testPage.getByTestId("storage-idle-period")).toHaveAttribute(
"data-settings-dirty",
"true",
);
await expect(testPage.getByTestId("storage-policy-section-schedule")).toHaveAttribute(
"data-settings-dirty",
"true",
);
await testPage.getByRole("button", { name: "Save changes" }).click();
await expect(testPage.getByText("Storage policy saved")).toBeVisible();
await testPage.reload();
await expect(scheduling).toHaveAttribute("data-state", "checked");
await expect(testPage.getByTestId("storage-idle-period")).toHaveValue("11");
// Stop the newly enabled scheduler before exercising a deterministic manual run.
await scheduling.click();
await testPage.getByRole("button", { name: "Save changes" }).click();
await expect(scheduling).toHaveAttribute("data-state", "unchecked");
await testPage.getByTestId("storage-analyze").click();
await expect(testPage.getByTestId("storage-analyze")).toHaveAttribute(
"data-job-state",
"succeeded",
);
await testPage.getByTestId("storage-resource-workspaces-trigger").click();
await expect(testPage.getByTestId("storage-resource-workspaces-trigger")).toContainText(
"Task workspaces<0.01 GB",
);
expect(fs.existsSync(orphan.artifact)).toBe(true);
await backend.restart();
await testPage.reload();
await expect(testPage.getByTestId("storage-idle-period")).toHaveValue("11");
await testPage.getByTestId("storage-run-now").click();
await expect(testPage.getByTestId("storage-run-now")).toHaveAttribute(
"data-job-state",
"succeeded",
);
const quarantineCard = testPage.getByTestId("storage-quarantine-card");
const entry = quarantineCard
.locator('[data-testid^="storage-quarantine-"]')
.filter({ hasText: orphan.root })
.last();
await expect(entry).toBeVisible();
expect(fs.existsSync(orphan.root)).toBe(false);
await entry.getByRole("button", { name: "Restore" }).click();
await expect.poll(() => fs.existsSync(orphan.artifact)).toBe(true);
await expect(quarantineCard.getByText(orphan.root)).toHaveCount(0);
});
test("shows busy feedback instead of forcing maintenance over an active task", async ({
testPage,
apiClient,
seedData,
prCapture,
}) => {
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Storage activity gate",
seedData.agentProfileId,
{
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
if (!task.session_id) throw new Error("createTaskWithAgent did not return session_id");
await expect
.poll(
async () => {
const { sessions } = await apiClient.listTaskSessions(task.id);
return sessions.find((session) => session.id === task.session_id)?.state ?? "";
},
{ timeout: 20_000, message: "Waiting for initial task turn to finish" },
)
.toBe("WAITING_FOR_INPUT");
await apiClient.addUserMessage(
task.id,
task.session_id,
'e2e:delay(15000)\ne2e:message("activity finished")',
);
await expect
.poll(
async () => {
const { sessions } = await apiClient.listTaskSessions(task.id);
return sessions.find((session) => session.id === task.session_id)?.state ?? "";
},
{ timeout: 20_000, message: "Waiting for active task storage gate" },
)
.toBe("RUNNING");
await testPage.goto("/settings/system/storage");
const responsePromise = testPage.waitForResponse(
(response) =>
response.request().method() === "POST" &&
new URL(response.url()).pathname === "/api/v1/system/storage/run",
);
await testPage.getByTestId("storage-run-now").click();
const response = await responsePromise;
const responseBody = await response.json();
expect({ status: response.status(), body: responseBody }).toMatchObject({
status: 409,
body: { busy_resources: expect.any(Array) },
});
expect(responseBody.force_available).toBe(true);
expect(responseBody.busy_resources[0]).toMatchObject({
kind: expect.any(String),
label: expect.any(String),
});
await expect(testPage.getByTestId("storage-busy")).toContainText(
responseBody.busy_resources[0].label,
);
await expect(testPage.getByTestId("storage-busy")).toContainText(/may disrupt/i);
await prCapture.screenshot("busy-feedback", {
caption: "Desktop storage cleanup explains the active work and offers Run anyway",
fullPage: true,
});
const forceRequest = testPage.waitForRequest(
(request) =>
request.method() === "POST" &&
new URL(request.url()).pathname === "/api/v1/system/storage/run",
);
await testPage.getByTestId("storage-run-anyway").click();
expect((await forceRequest).postDataJSON()).toEqual({ force: true });
await expect(testPage.getByTestId("storage-run-now")).toHaveAttribute(
"data-job-state",
"succeeded",
{ timeout: 20_000 },
);
});
test("shows quarantine deadlines and clears only eligible entries", async ({
testPage,
prCapture,
}) => {
const entries = [
{
id: "eligible-entry",
resource_type: "task_workspace",
original_path: "/tmp/eligible",
quarantine_path: "/tmp/trash/eligible",
size_bytes: 1024,
state: "quarantined",
quarantined_at: "2026-07-20T00:00:00Z",
delete_after: new Date(Date.now() - 60_000).toISOString(),
last_error: "",
metadata: {},
},
{
id: "protected-entry",
resource_type: "task_workspace",
original_path: "/tmp/protected",
quarantine_path: "/tmp/trash/protected",
size_bytes: 2048,
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 }),
});
return;
}
expect(route.request().method()).toBe("DELETE");
expect(route.request().postDataJSON()).toEqual({
scope: "eligible",
confirm: "DELETE ELIGIBLE",
});
await route.fulfill({
status: 202,
contentType: "application/json",
body: JSON.stringify({ job_id: "eligible-purge" }),
});
});
await testPage.goto("/settings/system/storage");
await expect(
testPage.getByTestId("storage-quarantine-eligible-entry").getByText("Eligible now"),
).toBeVisible();
await expect(testPage.getByText(/Protected until/)).toBeVisible();
await expect(testPage.getByTestId("storage-quarantine-protected-entry-delete")).toBeDisabled();
await testPage.getByTestId("storage-quarantine-card").scrollIntoViewIfNeeded();
await prCapture.screenshot("quarantine-deadlines", {
caption: "Desktop quarantine shows eligibility and protected retention deadlines",
});
await testPage.getByTestId("storage-quarantine-clear-eligible").click();
await testPage
.getByTestId("storage-quarantine-clear-eligible-confirm-confirmation")
.fill("DELETE ELIGIBLE");
await testPage.getByTestId("storage-quarantine-clear-eligible-confirm").click();
await expect(testPage.getByText("Eligible quarantine cleanup started")).toBeVisible();
await prCapture.screenshot("quarantine-clear-eligible", {
caption: "Desktop typed confirmation starts eligible-only quarantine cleanup",
});
});
});