fix: show Workdir removal causes and refresh ordinary retries

This commit is contained in:
2026-10-09 16:35:26 +09:00
parent 4337f415f9
commit c889c9a68d
11 changed files with 1381 additions and 177 deletions
+248
View File
@@ -0,0 +1,248 @@
declare const Deno: {
test(name: string, fn: () => void | Promise<void>): void;
};
import { parseWorkingDirectoryRemovalResponse } from "../src/lib/workspace/api/workdirs.ts";
import {
canRemoveWorkdir,
removalCause,
removeWorkdir,
} from "../src/lib/workspace/settings/workdir-removal.ts";
import type {
CleanupWorkdirCandidate,
WorkingDirectorySummary,
} from "../src/lib/workspace/sidebar/types.ts";
function assert(value: unknown, message = "assertion failed"): asserts value {
if (!value) throw new Error(message);
}
Deno.test("removal wire boundary rejects malformed results and mismatched identities", async () => {
for (
const extra of [
{ retryable: "true" },
{ disposition: "forgotten" },
{ failure_category: {} },
{ failure_category: "x".repeat(129) },
{ working_directory_id: "x".repeat(129) },
{ unexpected: true },
]
) {
let rejected = false;
try {
parseWorkingDirectoryRemovalResponse({
working_directory_id: "dir",
disposition: "retained",
retryable: true,
...extra,
});
} catch {
rejected = true;
}
assert(
rejected,
`accepted malformed removal result: ${JSON.stringify(extra)}`,
);
}
let rejected = false;
try {
await removeWorkdir(
(() =>
Promise.resolve(
Response.json({
working_directory_id: "other",
disposition: "removed",
retryable: false,
}),
)) as typeof fetch,
"workspace",
"runtime",
"dir",
);
} catch {
rejected = true;
}
assert(rejected, "accepted removal evidence for a different Workdir");
});
Deno.test("removal response size is bounded before decoding provider content", async () => {
let rejected = false;
try {
await removeWorkdir(
(() => Promise.resolve(new Response("x".repeat(4097)))) as typeof fetch,
"workspace",
"runtime",
"dir",
);
} catch {
rejected = true;
}
assert(rejected, "accepted an oversized removal response");
});
Deno.test("removal causes give bounded actions without echoing untrusted provider details", () => {
const categories = [
"mount_present",
"mount_check_unavailable",
"permission_denied",
"resource_busy",
"storage_unavailable",
"ownership_unknown",
"changes_present",
"changes_unknown",
"provider_cleanup_failed",
"provider_observation_unknown",
"provider_cleanup_outcome_unknown",
"authority_invalid",
"provider_identity_changed",
"runtime_unavailable",
"provider_unavailable",
"unsupported_target",
"blocked_by_live_authority",
"dirty_or_unknown",
"authority_changed",
];
for (const category of categories) {
const text = removalCause(category);
assert(text !== removalCause(null), `missing category ${category}`);
assert(
text.length < 400 && !text.includes("/"),
`unbounded or pathful category ${category}`,
);
}
for (
const untrusted of [
"/private/host/path",
"token=synthetic-secret",
"toString",
"__proto__",
"future_category",
"x".repeat(10000),
]
) {
const text = removalCause(untrusted);
assert(
text === removalCause(null) && !text.includes(untrusted),
"echoed unknown category",
);
}
});
Deno.test("pending unknown cleanup permits ordinary removal while current dirty and live guards remain protected", () => {
const workdir = {
working_directory_id: "dir",
status: "cleanup_pending",
cleanliness: "unknown",
source: { kind: "repository", repository_key: "main" },
materializer_kind: "runtime_git_clone",
} as WorkingDirectorySummary;
const candidate = {
action: "workdir_clean_cleanup",
blocking_reason: null,
running_linked: false,
pinned_linked: false,
} as CleanupWorkdirCandidate;
assert(canRemoveWorkdir(workdir, candidate));
for (
const guard of [
{ action: "workdir_dirty_discard" },
{ blocking_reason: "protected" },
{ running_linked: true },
{ pinned_linked: true },
]
) {
assert(
!canRemoveWorkdir(
workdir,
{ ...candidate, ...guard } as CleanupWorkdirCandidate,
),
);
}
assert(!canRemoveWorkdir({ ...workdir, cleanliness: "dirty" }, candidate));
assert(
canRemoveWorkdir(workdir, {
...candidate,
action: "workdir_record_delete",
}),
);
assert(!canRemoveWorkdir(workdir, { ...candidate, action: "worker_delete" }));
assert(!canRemoveWorkdir(workdir, undefined));
assert(
!canRemoveWorkdir({
...workdir,
occupied_by: {
worker_id: "worker",
runtime_id: "runtime",
display_name: "Worker",
linked_at: "now",
},
}, candidate),
);
});
Deno.test("ordinary retries send the same scoped DELETE body and expose only allowlisted HTTP causes", async () => {
const requests: { path: string; body: string; method: string }[] = [];
const fetchFn = (async (path: RequestInfo | URL, init?: RequestInit) => {
requests.push({
path: String(path),
body: String(init?.body),
method: String(init?.method),
});
return Response.json({
working_directory_id: "dir/x",
disposition: "attention_required",
retryable: true,
failure_category: "mount_present",
});
}) as typeof fetch;
for (let i = 0; i < 2; i++) {
const result = await removeWorkdir(
fetchFn,
"workspace/x",
"runtime/x",
"dir/x",
);
assert(result.retryable && result.failure_category === "mount_present");
}
assert(
JSON.stringify(requests[0]) === JSON.stringify(requests[1]),
"retry changed ordinary deletion request",
);
assert(
requests[0].path ===
"/api/w/workspace%2Fx/runtimes/runtime%2Fx/working-directories/dir%2Fx",
);
assert(
requests[0].method === "DELETE" &&
requests[0].body ===
JSON.stringify({ reason: "User requested Workdir deletion" }),
);
for (
const [status, category] of [
[403, "permission_denied"],
[409, "authority_changed"],
[503, "provider_unavailable"],
[500, null],
] as const
) {
let message = "";
try {
await removeWorkdir(
(() =>
Promise.resolve(
Response.json({ message: "/private/path token=synthetic-secret" }, {
status,
}),
)) as typeof fetch,
"workspace",
"runtime",
"dir",
);
} catch (error) {
message = error instanceof Error ? error.message : String(error);
}
assert(
message === removalCause(category),
`HTTP ${status} leaked provider text`,
);
}
});