Use content digests, key fingerprints, binding and trust identities, immutable mutation receipts, and observation tokens instead of state-update ordinals across API, storage, Runtime, and Web contracts. Preserve historical replay and secret material through frozen migrations; preflight uncertain removals before cutover, resume provisioning receipts, validate encrypted migration candidates, and align fresh/upgraded schemas. Include reviewed regression fixes, generated artifacts, and validation evidence.
485 lines
18 KiB
TypeScript
485 lines
18 KiB
TypeScript
/// <reference lib="dom" />
|
|
import { assert, assertEquals, assertStringIncludes } from "@std/assert";
|
|
import { dirname, fromFileUrl, join } from "@std/path";
|
|
import { chromium, type Page } from "playwright";
|
|
import { startOwnedProcesses, stopOwnedProcesses } from "../src/processes.ts";
|
|
const root = join(dirname(fromFileUrl(import.meta.url)), "../../..");
|
|
const evidence = join(root, "target/web-ux/t730-browser");
|
|
|
|
async function noOverflow(page: Page) {
|
|
assertEquals(
|
|
await page.locator("main").evaluate((main) => ({
|
|
page: document.documentElement.scrollWidth > innerWidth,
|
|
main: main.scrollWidth > main.clientWidth,
|
|
})),
|
|
{ page: false, main: false },
|
|
);
|
|
}
|
|
|
|
Deno.test("ordinary removal refreshes failed and successful inventory, supports exact retry, and preserves permission and live guards across viewports and themes", async () => {
|
|
await Deno.mkdir(evidence, { recursive: true });
|
|
const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 });
|
|
const port = (listener.addr as Deno.NetAddr).port;
|
|
listener.close();
|
|
const url = `http://127.0.0.1:${port}`;
|
|
const server = await startOwnedProcesses(
|
|
[{
|
|
id: "workdirs",
|
|
command: Deno.execPath(),
|
|
cwd: root,
|
|
args: [
|
|
"run",
|
|
"--allow-net",
|
|
"--allow-read",
|
|
"tools/web-ux/browser-tests/workspace_home_fixture_server.ts",
|
|
String(port),
|
|
"web/workspace/build",
|
|
],
|
|
readyUrl: `${url}/health`,
|
|
}],
|
|
join(root, "tools/web-ux/scenarios/workdir-removal-light.json"),
|
|
join(evidence, "process-logs"),
|
|
[],
|
|
);
|
|
const observations: unknown[] = [];
|
|
const injectedConsoleErrors: string[] = [];
|
|
const injectedUnexpectedErrors: string[] = [];
|
|
const observeInjectedFailure = (page: Page) => {
|
|
page.on("console", (message) => {
|
|
if (message.type() === "error") {
|
|
injectedConsoleErrors.push(message.text());
|
|
}
|
|
});
|
|
page.on(
|
|
"pageerror",
|
|
(error) => injectedUnexpectedErrors.push(String(error)),
|
|
);
|
|
page.on(
|
|
"requestfailed",
|
|
(request) => injectedUnexpectedErrors.push(request.url()),
|
|
);
|
|
};
|
|
const control = async (value: unknown) => {
|
|
const response = await fetch(`${url}/fixture-workdirs-control`, {
|
|
method: "POST",
|
|
body: JSON.stringify(value),
|
|
});
|
|
assert(response.ok);
|
|
await response.body?.cancel();
|
|
};
|
|
const state = async () => await (await fetch(`${url}/fixture-workdirs`)).json();
|
|
const browser = await chromium.launch({ headless: true });
|
|
try {
|
|
for (const colorScheme of ["light", "dark"] as const) {
|
|
for (const width of [1440, 768, 390, 320]) {
|
|
await control({ reset: true });
|
|
const context = await browser.newContext({
|
|
viewport: { width, height: 900 },
|
|
colorScheme,
|
|
reducedMotion: "reduce",
|
|
});
|
|
try {
|
|
const page = await context.newPage();
|
|
const errors: string[] = [];
|
|
page.on("pageerror", (error) => errors.push(String(error)));
|
|
page.on("console", (message) => {
|
|
if (message.type() === "error") errors.push(message.text());
|
|
});
|
|
page.on("requestfailed", (request) => errors.push(request.url()));
|
|
const route = `${url}/w/home-owner/settings/runtimes/fixture-runtime/workdirs`;
|
|
await page.goto(route);
|
|
const deletion = page.getByRole("button", {
|
|
name: "Delete retry-dir",
|
|
exact: true,
|
|
});
|
|
await deletion.waitFor();
|
|
assert(
|
|
await deletion.isEnabled(),
|
|
"past pending/unknown state permanently disabled ordinary removal",
|
|
);
|
|
assert(
|
|
await page.getByRole("button", {
|
|
name: "Delete dirty-dir",
|
|
exact: true,
|
|
}).isDisabled(),
|
|
);
|
|
assert(
|
|
await page.getByRole("button", {
|
|
name: "Delete occupied-dir",
|
|
exact: true,
|
|
}).isDisabled(),
|
|
);
|
|
await noOverflow(page);
|
|
assertEquals(await page.locator("thead th").allTextContents(), [
|
|
"Workdir",
|
|
"Repository",
|
|
"Checkout",
|
|
"Status",
|
|
"Cleanliness",
|
|
"Occupied by",
|
|
"Action",
|
|
]);
|
|
assertEquals(await page.locator("tbody td:first-child button").count(), 0);
|
|
assertEquals(
|
|
await page.locator("tbody td").first().evaluate((cell) => ({
|
|
padding: getComputedStyle(cell).padding,
|
|
secondaryDisplay: getComputedStyle(cell.querySelector("small")!).display,
|
|
})),
|
|
{ padding: "10px 8px", secondaryDisplay: "block" },
|
|
"Shared table density and secondary identity styling must remain applied",
|
|
);
|
|
assertEquals(await deletion.innerText(), "");
|
|
assertEquals(await deletion.locator("svg").count(), 1);
|
|
assertEquals(
|
|
await deletion.evaluate((button) => {
|
|
const r = button.getBoundingClientRect();
|
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
return {
|
|
width: r.width / rem,
|
|
height: r.height / rem,
|
|
column: (button.closest("td") as HTMLTableCellElement).cellIndex,
|
|
};
|
|
}),
|
|
{ width: 2, height: 2, column: 6 },
|
|
"Delete must remain a compact icon in the dedicated Action column",
|
|
);
|
|
await page.screenshot({
|
|
path: join(evidence, `${colorScheme}-${width}-initial.png`),
|
|
});
|
|
// Narrow screens keep the table scrollable, rather than moving actions into the name.
|
|
await deletion.scrollIntoViewIfNeeded();
|
|
await noOverflow(page);
|
|
// Hold the actual request boundary to verify duplicate gestures cannot dispatch another DELETE.
|
|
let release!: () => void;
|
|
const gate = new Promise<void>((resolve) => release = resolve);
|
|
let observed!: () => void;
|
|
const entered = new Promise<void>((resolve) => observed = resolve);
|
|
let deletes = 0;
|
|
await page.route(
|
|
"**/working-directories/retry-dir",
|
|
async (intercept) => {
|
|
deletes++;
|
|
observed();
|
|
await gate;
|
|
await intercept.continue();
|
|
},
|
|
);
|
|
await deletion.focus();
|
|
await page.keyboard.press("Enter");
|
|
await page.locator("button[aria-label='Delete retry-dir'][aria-busy='true'] .spinner")
|
|
.waitFor();
|
|
assert(await deletion.isDisabled());
|
|
await page.keyboard.press("Enter");
|
|
await entered;
|
|
assertEquals(deletes, 1);
|
|
await page.screenshot({
|
|
path: join(evidence, `${colorScheme}-${width}-deleting.png`),
|
|
});
|
|
release();
|
|
await page.getByRole("alert").filter({
|
|
hasText: "mounted filesystem",
|
|
}).waitFor();
|
|
const retry = page.getByRole("button", {
|
|
name: "Retry Delete retry-dir",
|
|
exact: true,
|
|
});
|
|
await retry.waitFor();
|
|
await page.waitForFunction(() =>
|
|
!(document.querySelector(
|
|
"button[aria-label='Retry Delete retry-dir']",
|
|
) as HTMLButtonElement)?.disabled
|
|
);
|
|
const failed = await state();
|
|
const beforeGets = failed.requests.filter((
|
|
r: { workspace: string; method: string },
|
|
) => r.workspace === "home-owner" && r.method === "GET");
|
|
assertEquals(
|
|
beforeGets.filter((r: { path: string }) => r.path.endsWith("/working-directories"))
|
|
.length,
|
|
2,
|
|
);
|
|
assertEquals(
|
|
beforeGets.filter((r: { path: string }) => r.path.endsWith("/cleanup-plan")).length,
|
|
2,
|
|
);
|
|
assertEquals(await page.locator("tbody tr").count(), 3);
|
|
await noOverflow(page);
|
|
await page.screenshot({
|
|
path: join(evidence, `${colorScheme}-${width}-mount-failure.png`),
|
|
});
|
|
await control({ mountReleased: true });
|
|
await retry.focus();
|
|
await page.keyboard.press("Enter");
|
|
await page.getByRole("status").filter({
|
|
hasText: "Deletion confirmed",
|
|
}).waitFor();
|
|
await page.waitForFunction(() => document.querySelectorAll("tbody tr").length === 2);
|
|
assertEquals(
|
|
await page.getByRole("button", { name: /retry-dir/ }).count(),
|
|
0,
|
|
);
|
|
assert(
|
|
await page.getByRole("button", {
|
|
name: "Delete dirty-dir",
|
|
exact: true,
|
|
}).isDisabled(),
|
|
);
|
|
const final = await state();
|
|
const mutations = final.requests.filter((r: { method: string }) => r.method === "DELETE");
|
|
assertEquals(mutations.length, 2);
|
|
assertEquals(
|
|
mutations[0],
|
|
mutations[1],
|
|
"ordinary retry changed its request",
|
|
);
|
|
const gets = final.requests.filter((
|
|
r: { workspace: string; method: string },
|
|
) => r.workspace === "home-owner" && r.method === "GET");
|
|
assertEquals(
|
|
gets.filter((r: { path: string }) => r.path.endsWith("/working-directories")).length,
|
|
3,
|
|
);
|
|
assertEquals(
|
|
gets.filter((r: { path: string }) => r.path.endsWith("/cleanup-plan")).length,
|
|
3,
|
|
);
|
|
await page.screenshot({
|
|
path: join(evidence, `${colorScheme}-${width}-removed.png`),
|
|
});
|
|
await page.goto(
|
|
`${url}/w/home-member/settings/runtimes/fixture-runtime/workdirs`,
|
|
);
|
|
await page.locator("tbody tr").first().waitFor();
|
|
assertEquals(
|
|
await page.getByRole("button", { name: /Delete/ }).count(),
|
|
0,
|
|
);
|
|
await noOverflow(page);
|
|
await page.screenshot({
|
|
path: join(evidence, `${colorScheme}-${width}-member.png`),
|
|
});
|
|
await page.goto(
|
|
`${url}/w/home-empty/settings/runtimes/fixture-runtime/workdirs`,
|
|
);
|
|
await page.getByText("No workdirs are visible for this Runtime.")
|
|
.waitFor();
|
|
await noOverflow(page);
|
|
assertEquals(errors, []);
|
|
observations.push({
|
|
colorScheme,
|
|
width,
|
|
requests: final.requests,
|
|
errors,
|
|
});
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lost/HTTP failure responses still trigger fresh inventory and eligibility. Never echo provider bodies.
|
|
await control({ reset: true });
|
|
const page = await browser.newPage();
|
|
observeInjectedFailure(page);
|
|
await page.goto(
|
|
`${url}/w/home-error/settings/runtimes/fixture-runtime/workdirs`,
|
|
);
|
|
await page.getByRole("button", { name: "Delete retry-dir", exact: true })
|
|
.click();
|
|
await page.getByRole("alert").filter({ hasText: "provider is unavailable" })
|
|
.waitFor();
|
|
await page.waitForFunction(() =>
|
|
!(document.querySelector(
|
|
"button[aria-label='Delete retry-dir']",
|
|
) as HTMLButtonElement)?.disabled
|
|
);
|
|
assert(
|
|
!(await page.locator("main").innerText()).includes("Synthetic private"),
|
|
);
|
|
const failed = await state();
|
|
assertEquals(
|
|
failed.requests.filter((r: { method: string; path: string }) =>
|
|
r.method === "GET" && r.path.endsWith("/working-directories")
|
|
).length,
|
|
2,
|
|
);
|
|
assertEquals(
|
|
failed.requests.filter((r: { method: string; path: string }) =>
|
|
r.method === "GET" && r.path.endsWith("/cleanup-plan")
|
|
).length,
|
|
2,
|
|
);
|
|
await page.screenshot({ path: join(evidence, "http-failure.png") });
|
|
await page.close();
|
|
|
|
// Plan refresh failure cannot leave stale delete authority enabled; manual Refresh recovers it.
|
|
await control({ reset: true });
|
|
const stale = await browser.newPage();
|
|
observeInjectedFailure(stale);
|
|
let planGets = 0;
|
|
let planFails = true;
|
|
await stale.route("**/cleanup-plan", async (route) => {
|
|
planGets++;
|
|
if (planGets > 1 && planFails) {
|
|
await route.fulfill({
|
|
status: 503,
|
|
json: { message: "Synthetic private path" },
|
|
});
|
|
} else await route.continue();
|
|
});
|
|
await stale.goto(
|
|
`${url}/w/home-owner/settings/runtimes/fixture-runtime/workdirs`,
|
|
);
|
|
await stale.getByRole("button", { name: "Delete retry-dir", exact: true })
|
|
.click();
|
|
await stale.getByRole("alert").filter({ hasText: "could not be refreshed" })
|
|
.waitFor();
|
|
assert(
|
|
await stale.getByRole("button", {
|
|
name: "Retry Delete retry-dir",
|
|
exact: true,
|
|
}).isDisabled(),
|
|
);
|
|
assert(
|
|
!(await stale.locator("main").innerText()).includes("Synthetic private"),
|
|
);
|
|
await stale.screenshot({ path: join(evidence, "refresh-failure.png") });
|
|
planFails = false;
|
|
await stale.getByRole("button", { name: "Refresh", exact: true }).click();
|
|
await stale.waitForFunction(() =>
|
|
!(document.querySelector(
|
|
"button[aria-label='Retry Delete retry-dir']",
|
|
) as HTMLButtonElement)?.disabled
|
|
);
|
|
await stale.close();
|
|
|
|
// Current dirty changes after a failure override a retryable historical result.
|
|
await control({ reset: true });
|
|
const changed = await browser.newPage();
|
|
let changedGets = 0;
|
|
await changed.route("**/cleanup-plan", async (route) => {
|
|
const response = await route.fetch();
|
|
const plan = await response.json();
|
|
if (++changedGets > 1) plan.workdirs[0].action = "workdir_dirty_discard";
|
|
await route.fulfill({ response, json: plan });
|
|
});
|
|
await changed.goto(
|
|
`${url}/w/home-owner/settings/runtimes/fixture-runtime/workdirs`,
|
|
);
|
|
await changed.getByRole("button", { name: "Delete retry-dir", exact: true })
|
|
.click();
|
|
await changed.getByRole("button", {
|
|
name: "Retry Delete retry-dir",
|
|
exact: true,
|
|
}).waitFor();
|
|
await changed.waitForFunction(() =>
|
|
document.querySelector("button[aria-label='Retry Delete retry-dir']")?.getAttribute("title")
|
|
?.includes("Changes are protected")
|
|
);
|
|
assert(
|
|
await changed.getByRole("button", {
|
|
name: "Retry Delete retry-dir",
|
|
exact: true,
|
|
}).isDisabled(),
|
|
);
|
|
assertStringIncludes(
|
|
await changed.getByRole("button", { name: "Retry Delete retry-dir", exact: true })
|
|
.getAttribute("title") ?? "",
|
|
"Changes are protected",
|
|
);
|
|
await changed.screenshot({
|
|
path: join(evidence, "new-changes-protected.png"),
|
|
});
|
|
await changed.close();
|
|
// A malformed response may follow a real side effect. Inventory still removes the now-absent row.
|
|
await control({ reset: true, mountReleased: true });
|
|
const lost = await browser.newPage();
|
|
observeInjectedFailure(lost);
|
|
await lost.route("**/working-directories/retry-dir", async (route) => {
|
|
await route.fetch();
|
|
await route.fulfill({
|
|
status: 200,
|
|
json: {
|
|
message: "Synthetic private path",
|
|
failure_category: "/private/path token=synthetic",
|
|
},
|
|
});
|
|
});
|
|
await lost.goto(
|
|
`${url}/w/home-owner/settings/runtimes/fixture-runtime/workdirs`,
|
|
);
|
|
await lost.getByRole("button", { name: "Delete retry-dir", exact: true })
|
|
.click();
|
|
await lost.getByRole("alert").filter({
|
|
hasText: "Removal could not be confirmed",
|
|
}).waitFor();
|
|
await lost.waitForFunction(() => document.querySelectorAll("tbody tr").length === 2);
|
|
assert(
|
|
!(await lost.locator("main").innerText()).includes("Synthetic private"),
|
|
);
|
|
assert(!(await lost.locator("main").innerText()).includes("/private/path"));
|
|
await lost.screenshot({
|
|
path: join(evidence, "malformed-response-refreshed.png"),
|
|
});
|
|
await lost.close();
|
|
|
|
// Even a failed initial inventory can recover into a confirmed empty list with manual Refresh.
|
|
const emptyRecovery = await browser.newPage();
|
|
observeInjectedFailure(emptyRecovery);
|
|
let inventoryFails = true;
|
|
await emptyRecovery.route(
|
|
"**/runtimes/fixture-runtime/working-directories",
|
|
async (route) => {
|
|
if (inventoryFails) {
|
|
await route.fulfill({
|
|
status: 503,
|
|
json: { message: "Synthetic private path" },
|
|
});
|
|
} else await route.continue();
|
|
},
|
|
);
|
|
await emptyRecovery.goto(
|
|
`${url}/w/home-empty/settings/runtimes/fixture-runtime/workdirs`,
|
|
);
|
|
await emptyRecovery.getByText("Workdir inventory is unavailable.")
|
|
.waitFor();
|
|
inventoryFails = false;
|
|
await emptyRecovery.getByRole("button", { name: "Refresh", exact: true })
|
|
.click();
|
|
await emptyRecovery.getByText("No workdirs are visible for this Runtime.")
|
|
.waitFor();
|
|
await emptyRecovery.close();
|
|
assertEquals(injectedUnexpectedErrors, []);
|
|
assert(
|
|
injectedConsoleErrors.every((message) => message.includes("503")),
|
|
"unexpected injected failure console diagnostic",
|
|
);
|
|
await Deno.writeTextFile(
|
|
join(evidence, "evidence.json"),
|
|
JSON.stringify(
|
|
{
|
|
observations,
|
|
injectedConsoleErrors,
|
|
injectedUnexpectedErrors,
|
|
failureRefresh: failed.requests,
|
|
refreshFailureRecovered: true,
|
|
newChangesProtected: true,
|
|
malformedResponseReconciled: true,
|
|
emptyInventoryRecovered: true,
|
|
expectedFailureDiagnostics: [
|
|
"Synthetic DELETE HTTP 503 on home-error",
|
|
"Synthetic cleanup-plan HTTP 503 after removal",
|
|
"Malformed DELETE result after fixture physical removal",
|
|
"Synthetic initial inventory HTTP 503",
|
|
],
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
} finally {
|
|
await browser.close();
|
|
assertEquals(await stopOwnedProcesses(server), []);
|
|
}
|
|
});
|