Files: never an empty pane -- Loading…, a slow answer and a refusal said in the tree
ci / ui (push) Waiting to run
ci / check (push) Failing after 3s
ci / start (push) Failing after 3s

Files › Server drew nothing while GET /files was on its way: no word that it was being read, and a
refusal or a broken answer only after Rust's 60 s read timeout. To the administrator it looked as
if the server had no files, until the listing arrived. Every folder of both trees now says
Loading… while it is read, says after 8 s that the server has not answered (with Try again, the
late answer still drawn), and says a refusal, an answer that is not a listing, or a failure to
draw it in its place with Try again. An answer that comes after Read again, or after the folder
was closed, is dropped instead of appended beside the new one.

Tests (tests/ui N8): the administrator's / as the server's test client answers it (folders, a
mount, files at the root), held to show Loading…; the 8 s note under a fake clock; a 403 and Try
again; three answers that are not a listing; Read again while an answer is pending; 390 px. The
mock can hold an answer (`hold`, `__mock.release`) and change one (`__mock.answer`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-11-10 16:06:44 +00:00
1 parent 189f706c65
commit 6874dd46cd
7 files changed
+284 -44

No files matched your search

+22
View File
@@ -8,6 +8,8 @@
// /api/v2 as the page reaches it through `api`. A path is looked up with its query,
// then without it. A value `{ __error: { status, detail } }` is a refusal. Anything
// not in the table is a 404, as an older server would answer.
// hold ["GET /files?path=%2F", ...] -- answers that wait (a slow server) until the test
// lets them go with `__mock.release(key)`; the table is read when they are let go.
// streams { "/chats/c1/messages/m1/stream": [[event, data], ...] } -- the frames a followed
// reply sends as soon as it is opened. More can be sent later with `__mock.frame()`.
// terminal [[kind, data], ...] -- what every terminal's socket sends as soon as it is opened
@@ -29,7 +31,16 @@
for (const cb of listeners.get(name) || []) cb({ event: name, payload })
}
const held = [] // [{ key, go }] answers waiting for `release`
function route(method, path) {
if ((config.hold || []).includes(`${method} ${path}`)) {
return new Promise((resolve) => held.push({ key: `${method} ${path}`, go: resolve })).then(() => answer(method, path))
}
return answer(method, path)
}
function answer(method, path) {
const routes = config.routes || {}
const bare = path.split("?")[0]
for (const key of [`${method} ${path}`, `${method} ${bare}`]) {
@@ -130,6 +141,17 @@
window.__mock = {
calls,
/** Let the held answers for `key` go, in the order they were asked; stop holding new ones. */
release(key) {
config.hold = (config.hold || []).filter((k) => k !== key)
const going = held.filter((x) => x.key === key)
for (const x of going) held.splice(held.indexOf(x), 1)
for (const x of going) x.go()
},
/** Change the server's answer for a route from now on. */
answer(key, value) {
config.routes[key] = value
},
frame,
emit: dispatch,
/** The calls of one command, or of `api` with one method and path prefix. */