fix: integrate safe Workdir removal recovery
Merge approved T-730 source b1dc8f9f into develop. Resolve the test-registration conflict by preserving both Drive and Workdir-removal tests. Integration root check, Runtime/Server/Worker suites, real Web Drive adapter, generated Workdir contract, Web check/unit/components/build/browser, fmt and diff checks passed.
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
// Synthetic API boundary only; the production route and shell own all rendering.
|
||||
export function workdirFixture() {
|
||||
let mountReleased = false;
|
||||
const deleted = new Set<string>();
|
||||
const requests: {
|
||||
workspace: string;
|
||||
method: string;
|
||||
path: string;
|
||||
body?: unknown;
|
||||
}[] = [];
|
||||
const summary = (
|
||||
id: string,
|
||||
status = "active",
|
||||
cleanliness = "clean",
|
||||
occupied = false,
|
||||
) => ({
|
||||
working_directory_id: id,
|
||||
display_name: id === "retry-dir" ? "Cleanup retry — distributed development checkout" : id,
|
||||
source: { kind: "repository", repository_key: "main" },
|
||||
materializer_kind: "runtime_git_clone",
|
||||
status,
|
||||
cleanliness,
|
||||
current_ref: "a".repeat(40),
|
||||
current_selector: "develop",
|
||||
occupied_by: occupied
|
||||
? {
|
||||
runtime_id: "fixture-runtime",
|
||||
worker_id: "worker-1",
|
||||
display_name: "Active Worker",
|
||||
linked_at: "2026-01-01T00:00:00Z",
|
||||
}
|
||||
: null,
|
||||
});
|
||||
const items = (workspace: string) =>
|
||||
workspace === "home-empty" ? [] : [
|
||||
...(deleted.has(workspace) ? [] : [summary("retry-dir", "cleanup_pending", "unknown")]),
|
||||
summary("dirty-dir", "active", "dirty"),
|
||||
summary("occupied-dir", "active", "clean", true),
|
||||
];
|
||||
const plan = (workspace: string) => ({
|
||||
workspace_id: workspace,
|
||||
runtime_id: "fixture-runtime",
|
||||
generated_at: "2026-01-01T00:00:00Z",
|
||||
revision: "plan-1",
|
||||
digest: "digest-1",
|
||||
workers: [],
|
||||
diagnostics: [],
|
||||
workdirs: items(workspace).map((item) => ({
|
||||
target_id: `workdir:${item.working_directory_id}`,
|
||||
action: item.cleanliness === "dirty" ? "workdir_dirty_discard" : "workdir_clean_cleanup",
|
||||
workdir_id: item.working_directory_id,
|
||||
runtime_id: "fixture-runtime",
|
||||
repository_key: "main",
|
||||
reason: "Ordinary cleanup rechecks current conditions",
|
||||
linked_worker_ids: item.occupied_by ? ["worker-1"] : [],
|
||||
linked_running_worker_ids: item.occupied_by ? ["worker-1"] : [],
|
||||
running_linked: Boolean(item.occupied_by),
|
||||
pinned_linked: false,
|
||||
file_status: item.status === "cleanup_pending" ? "pending" : "present",
|
||||
cleanliness: item.cleanliness,
|
||||
blocking_reason: item.occupied_by ? "Workdir is linked to a running Worker" : null,
|
||||
})),
|
||||
});
|
||||
return async (request: Request): Promise<Response | null> => {
|
||||
const url = new URL(request.url);
|
||||
if (
|
||||
url.pathname === "/fixture-workdirs-control" && request.method === "POST"
|
||||
) {
|
||||
const control = await request.json();
|
||||
mountReleased = control.mountReleased === true;
|
||||
if (control.reset) {
|
||||
deleted.clear();
|
||||
requests.length = 0;
|
||||
}
|
||||
return Response.json({ ok: true });
|
||||
}
|
||||
if (url.pathname === "/fixture-workdirs") {
|
||||
return Response.json({ requests });
|
||||
}
|
||||
const match = url.pathname.match(/^\/api\/w\/([^/]+)(\/runtimes.*)$/);
|
||||
if (!match) return null;
|
||||
const [, workspace, path] = match;
|
||||
if (path === "/runtimes") {
|
||||
return Response.json({
|
||||
workspace_id: workspace,
|
||||
limit: 100,
|
||||
source: "fixture",
|
||||
diagnostics: [],
|
||||
items: [],
|
||||
});
|
||||
}
|
||||
if (!path.startsWith("/runtimes/fixture-runtime/")) return null;
|
||||
const body = request.method !== "GET" ? await request.json() : undefined;
|
||||
requests.push({ workspace, method: request.method, path, body });
|
||||
if (path.endsWith("/working-directories")) {
|
||||
return Response.json({
|
||||
workspace_id: workspace,
|
||||
items: items(workspace),
|
||||
diagnostics: [],
|
||||
});
|
||||
}
|
||||
if (path.endsWith("/cleanup-plan")) return Response.json(plan(workspace));
|
||||
// Baseline route uses cleanup-executions; same retained outcome for before captures.
|
||||
if (path.endsWith("/cleanup-executions")) {
|
||||
return Response.json({
|
||||
workspace_id: workspace,
|
||||
runtime_id: "fixture-runtime",
|
||||
executed_at: "2026-01-01T00:00:00Z",
|
||||
diagnostics: [],
|
||||
plan_after: plan(workspace),
|
||||
results: [{
|
||||
target_id: "workdir:retry-dir",
|
||||
action: "workdir_clean_cleanup",
|
||||
status: "retained",
|
||||
message: "Provider cleanup failed",
|
||||
}],
|
||||
});
|
||||
}
|
||||
if (
|
||||
path.endsWith("/working-directories/retry-dir") &&
|
||||
request.method === "DELETE"
|
||||
) {
|
||||
if (workspace === "home-error") {
|
||||
return Response.json({
|
||||
message: "Synthetic private host path must not render",
|
||||
}, { status: 503 });
|
||||
}
|
||||
if (mountReleased) deleted.add(workspace);
|
||||
return Response.json({
|
||||
working_directory_id: "retry-dir",
|
||||
disposition: mountReleased ? "removed" : "attention_required",
|
||||
retryable: !mountReleased,
|
||||
failure_category: mountReleased ? null : "mount_present",
|
||||
});
|
||||
}
|
||||
return Response.json({ error: "Unexpected fixture Workdir request" }, {
|
||||
status: 404,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
/// <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);
|
||||
assert(
|
||||
await deletion.evaluate((button) => {
|
||||
const r = button.getBoundingClientRect();
|
||||
return r.left >= 0 && r.right <= innerWidth;
|
||||
}),
|
||||
"Delete is outside initial viewport",
|
||||
);
|
||||
await page.screenshot({
|
||||
path: join(evidence, `${colorScheme}-${width}-initial.png`),
|
||||
});
|
||||
// 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.getByRole("button", {
|
||||
name: "Delete retry-dir",
|
||||
exact: true,
|
||||
}).filter({ hasText: "Deleting…" }).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(".workdir-identity .removal-guard")?.textContent
|
||||
?.includes("Changes are protected")
|
||||
);
|
||||
assert(
|
||||
await changed.getByRole("button", {
|
||||
name: "Retry Delete retry-dir",
|
||||
exact: true,
|
||||
}).isDisabled(),
|
||||
);
|
||||
assertStringIncludes(
|
||||
await changed.locator("main").innerText(),
|
||||
"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), []);
|
||||
}
|
||||
});
|
||||
@@ -2,6 +2,8 @@ import { extname, join, normalize } from "jsr:@std/path@1.1.4";
|
||||
import { dashboardFixture } from "../../../web/workspace/src/lib/workspace/home/dashboard.test-fixtures.ts";
|
||||
|
||||
import { settingsFixtureHandler } from "../../../web/workspace/src/lib/workspace/settings/identity.test-fixtures.ts";
|
||||
import { workdirFixture } from "./workdir_fixture.ts";
|
||||
const workdirsFixture = workdirFixture();
|
||||
const settingsFixture = settingsFixtureHandler();
|
||||
|
||||
const port = Number(Deno.args[0]);
|
||||
@@ -30,6 +32,8 @@ const mime: Record<string, string> = {
|
||||
Deno.serve({ hostname: "127.0.0.1", port }, async (request) => {
|
||||
const url = new URL(request.url);
|
||||
if (url.pathname === "/health") return new Response("ok");
|
||||
const workdirsResponse = await workdirsFixture(request);
|
||||
if (workdirsResponse) return workdirsResponse;
|
||||
const settingsResponse = await settingsFixture(request);
|
||||
if (settingsResponse) return settingsResponse;
|
||||
if (url.pathname === "/api/workspaces") {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"web-ux": "deno run --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys cli.ts",
|
||||
"check": "deno check cli.ts src/*.ts tests/*.ts browser-tests/*.ts",
|
||||
"test": "deno test --allow-env --allow-read --allow-write --allow-run --allow-sys tests",
|
||||
"test:browser": "deno test --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys browser-tests/ticket_shell_test.ts browser-tests/capture_smoke_test.ts browser-tests/memory_document_shell_test.ts browser-tests/console_history_shell_test.ts browser-tests/workspace_home_shell_test.ts browser-tests/workspace_settings_shell_test.ts browser-tests/console_pending_shell_test.ts browser-tests/console_live_shell_test.ts browser-tests/console_live_admission_shell_test.ts"
|
||||
"test:browser": "deno test --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys browser-tests/ticket_shell_test.ts browser-tests/capture_smoke_test.ts browser-tests/memory_document_shell_test.ts browser-tests/console_history_shell_test.ts browser-tests/workspace_home_shell_test.ts browser-tests/workspace_settings_shell_test.ts browser-tests/workdir_removal_shell_test.ts browser-tests/console_pending_shell_test.ts browser-tests/console_live_shell_test.ts browser-tests/console_live_admission_shell_test.ts"
|
||||
},
|
||||
"fmt": {
|
||||
"lineWidth": 100,
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "workdir-removal-dark",
|
||||
"title": "Ordinary Workdir removal and guarded retry",
|
||||
"baseUrl": "http://127.0.0.1:15290",
|
||||
"colorScheme": "dark",
|
||||
"reducedMotion": "reduce",
|
||||
"personas": [
|
||||
{
|
||||
"id": "fixture",
|
||||
"label": "Synthetic permission projections",
|
||||
"auth": {
|
||||
"kind": "anonymous"
|
||||
}
|
||||
}
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"label": "desktop",
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"label": "narrow",
|
||||
"width": 768,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"label": "mobile",
|
||||
"width": 390,
|
||||
"height": 844
|
||||
}
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"id": "owner",
|
||||
"label": "Owner removal",
|
||||
"path": "/w/home-owner/settings/runtimes/fixture-runtime/workdirs",
|
||||
"goal": "Remove a failed-cleanup Workdir using ordinary retry without enabling dirty or occupied removal.",
|
||||
"dataState": "Pending/unknown retry plus dirty and occupied guards. First attempt finds a mount.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "main table tbody tr"
|
||||
},
|
||||
"capturePoints": [
|
||||
{
|
||||
"id": "initial",
|
||||
"label": "Current inventory"
|
||||
},
|
||||
{
|
||||
"id": "failed-attempt",
|
||||
"label": "Removal blocked by mount",
|
||||
"interaction": [
|
||||
{
|
||||
"action": "click",
|
||||
"selector": "button[aria-label='Delete retry-dir']"
|
||||
}
|
||||
],
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "[role='alert']"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "member",
|
||||
"label": "No Runtime management permission",
|
||||
"path": "/w/home-member/settings/runtimes/fixture-runtime/workdirs",
|
||||
"goal": "Read inventory without owner-only removal controls.",
|
||||
"dataState": "Management permission false.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "main table tbody tr"
|
||||
},
|
||||
"capturePoints": [
|
||||
{
|
||||
"id": "initial",
|
||||
"label": "Read only inventory"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "empty",
|
||||
"label": "Empty inventory",
|
||||
"path": "/w/home-empty/settings/runtimes/fixture-runtime/workdirs",
|
||||
"goal": "Recognize no remaining Workdirs.",
|
||||
"dataState": "Empty list.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "main:has-text('No workdirs')"
|
||||
},
|
||||
"capturePoints": [
|
||||
{
|
||||
"id": "initial",
|
||||
"label": "Empty inventory"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"processes": [
|
||||
{
|
||||
"id": "workdir-fixture",
|
||||
"command": "deno",
|
||||
"cwd": "..",
|
||||
"args": [
|
||||
"run",
|
||||
"--allow-net",
|
||||
"--allow-read",
|
||||
"browser-tests/workspace_home_fixture_server.ts",
|
||||
"15290",
|
||||
"../../web/workspace/build"
|
||||
],
|
||||
"readyUrl": "http://127.0.0.1:15290/health"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "workdir-removal-light",
|
||||
"title": "Ordinary Workdir removal and guarded retry",
|
||||
"baseUrl": "http://127.0.0.1:15290",
|
||||
"colorScheme": "light",
|
||||
"reducedMotion": "reduce",
|
||||
"personas": [
|
||||
{
|
||||
"id": "fixture",
|
||||
"label": "Synthetic permission projections",
|
||||
"auth": { "kind": "anonymous" }
|
||||
}
|
||||
],
|
||||
"viewports": [
|
||||
{ "label": "desktop", "width": 1440, "height": 900 },
|
||||
{ "label": "narrow", "width": 768, "height": 900 },
|
||||
{ "label": "mobile", "width": 390, "height": 844 }
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"id": "owner",
|
||||
"label": "Owner removal",
|
||||
"path": "/w/home-owner/settings/runtimes/fixture-runtime/workdirs",
|
||||
"goal": "Remove a failed-cleanup Workdir using ordinary retry without enabling dirty or occupied removal.",
|
||||
"dataState": "Pending/unknown retry plus dirty and occupied guards. First attempt finds a mount.",
|
||||
"ready": { "kind": "selector", "selector": "main table tbody tr" },
|
||||
"capturePoints": [
|
||||
{ "id": "initial", "label": "Current inventory" },
|
||||
{
|
||||
"id": "failed-attempt",
|
||||
"label": "Removal blocked by mount",
|
||||
"interaction": [
|
||||
{
|
||||
"action": "click",
|
||||
"selector": "button[aria-label='Delete retry-dir']"
|
||||
}
|
||||
],
|
||||
"ready": { "kind": "selector", "selector": "[role='alert']" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "member",
|
||||
"label": "No Runtime management permission",
|
||||
"path": "/w/home-member/settings/runtimes/fixture-runtime/workdirs",
|
||||
"goal": "Read inventory without owner-only removal controls.",
|
||||
"dataState": "Management permission false.",
|
||||
"ready": { "kind": "selector", "selector": "main table tbody tr" },
|
||||
"capturePoints": [{ "id": "initial", "label": "Read only inventory" }]
|
||||
},
|
||||
{
|
||||
"id": "empty",
|
||||
"label": "Empty inventory",
|
||||
"path": "/w/home-empty/settings/runtimes/fixture-runtime/workdirs",
|
||||
"goal": "Recognize no remaining Workdirs.",
|
||||
"dataState": "Empty list.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "main:has-text('No workdirs')"
|
||||
},
|
||||
"capturePoints": [{ "id": "initial", "label": "Empty inventory" }]
|
||||
}
|
||||
],
|
||||
"processes": [
|
||||
{
|
||||
"id": "workdir-fixture",
|
||||
"command": "deno",
|
||||
"cwd": "..",
|
||||
"args": [
|
||||
"run",
|
||||
"--allow-net",
|
||||
"--allow-read",
|
||||
"browser-tests/workspace_home_fixture_server.ts",
|
||||
"15290",
|
||||
"../../web/workspace/build"
|
||||
],
|
||||
"readyUrl": "http://127.0.0.1:15290/health"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user