feat(browser): a boot failure is a state with a reason, not silence
improvise-1wft and the five beads it blocked were one failure: nothing
carried a boot failure's REASON from the worker to the page. The channel
is one rule -- a protocol frame crosses the port as a STRING, a shell
notice as an OBJECT -- so it needs no protocol change and the
discriminator is total.
A boot failure is a STATE, not a rejected promise. A document another
context holds the exclusive handle to now OPENS read-only: measured first
(harness probe, Chromium + Firefox), getFile() reads the bytes while the
handle is held, with the second-handle refusal asserted so the probe
cannot pass vacuously.
- improvise-1wft the channel: Notice/NoticeKind, boot_notice, take_notice
- improvise-i0v9 read-only boot over vfs::ReadOnly + OpfsFs::read_unattached
- improvise-86nw start() rejecting for any reason now posts BootFailed
- improvise-9cma boot::document_name -- and a/b/c.improv was ACCEPTED as
c.improv, not rejected, which is worse than the report
- improvise-iobc vfs::Origin: a seed error is a notice only when the
document really had to be created without it
- improvise-vep8 the watchdog arms BEFORE transport.ready; its message
stopped claiming the connection was accepted
- improvise-7gip DedicatedWorkerGlobalScope (needs a web-sys feature)
- improvise-zre1 a persist that fails raises a notice; the trigger is the
dirty flag surviving, observed rather than predicted
- improvise-7gf8 the render breaker trips once instead of retrying forever
- improvise-ociy non-OPEN sends are loud; a viewport announcement is not
- improvise-y01k the three ws-server drops are logged
- improvise-r4nf serde failures throw instead of reading as "nothing to send"
Every notice routes through shell.fail, so gotcha 48's disarm half is not
re-broken. Two comments in shell.spec.js stated the opposite of the new
arming order and were corrected rather than left.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
@@ -5,12 +5,15 @@ import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
classifyWorkerMessage,
|
||||
composeStatus,
|
||||
createShell,
|
||||
documentTitle,
|
||||
extentOf,
|
||||
frameCostOf,
|
||||
indicator,
|
||||
isFatal,
|
||||
noticeText,
|
||||
socketUrl,
|
||||
} from "../shell.js";
|
||||
|
||||
@@ -175,3 +178,76 @@ test("the title bar spells the document the way the TUI's own title does", () =>
|
||||
// arrives as `null`, and an empty pair of parens would be a lie about storage.
|
||||
assert.equal(documentTitle("Untitled", true, null), "Untitled [+]");
|
||||
});
|
||||
|
||||
test("a protocol frame is a string and a shell notice is an object", () => {
|
||||
assert.deepEqual(classifyWorkerMessage('{"Commands":[]}'), {
|
||||
kind: "frame",
|
||||
text: '{"Commands":[]}',
|
||||
});
|
||||
const notice = { kind: "ReadOnly", document: "a.improv", detail: "held" };
|
||||
assert.deepEqual(classifyWorkerMessage(notice), { kind: "notice", notice });
|
||||
// The discriminating case: a notice serialized as a STRING would be fed to
|
||||
// the fold, which is exactly the confusion the two shapes exist to prevent.
|
||||
assert.equal(classifyWorkerMessage(JSON.stringify(notice)).kind, "frame");
|
||||
});
|
||||
|
||||
test("a message that is neither is named rather than silently dropped", () => {
|
||||
for (const junk of [null, undefined, 42, {}, { detail: "no kind" }]) {
|
||||
assert.equal(
|
||||
classifyWorkerMessage(junk).kind,
|
||||
"unknown",
|
||||
`classified ${JSON.stringify(junk)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("every notice reads as a reason, and names its document", () => {
|
||||
const of = (kind) => noticeText({ kind, document: "books.improv", detail: "404" });
|
||||
// improvise-i0v9: the page must say what is wrong, not blame the browser.
|
||||
assert.match(of("ReadOnly"), /read-only/);
|
||||
assert.match(of("ReadOnly"), /another tab/);
|
||||
// improvise-iobc: an empty document must not pass for an intentional one.
|
||||
assert.match(of("SeedMissing"), /EMPTY/);
|
||||
assert.match(of("SeedMissing"), /404/);
|
||||
// improvise-zre1: "not saved" must not read like "saving".
|
||||
assert.match(of("SaveFailed"), /NOT SAVED/);
|
||||
assert.match(of("BootFailed"), /could not start/);
|
||||
for (const kind of ["ReadOnly", "SeedMissing", "SaveFailed", "BootFailed"]) {
|
||||
assert.match(of(kind), /books\.improv/, `${kind} did not name the document`);
|
||||
}
|
||||
});
|
||||
|
||||
test("a notice kind this page has never heard of still carries its reason", () => {
|
||||
const text = noticeText({ kind: "SomethingNew", document: "a.improv", detail: "because" });
|
||||
assert.match(text, /SomethingNew/);
|
||||
assert.match(text, /because/);
|
||||
});
|
||||
|
||||
test("only a failed boot is fatal — read-only, empty and unsaved all still paint", () => {
|
||||
assert.equal(isFatal({ kind: "BootFailed" }), true);
|
||||
for (const kind of ["ReadOnly", "SeedMissing", "SaveFailed"]) {
|
||||
assert.equal(isFatal({ kind }), false, `${kind} was treated as fatal`);
|
||||
}
|
||||
});
|
||||
|
||||
test("a notice DISARMS the watchdog rather than being replaced by it", () => {
|
||||
const clock = fakeClock();
|
||||
const seen = [];
|
||||
const shell = createShell({
|
||||
onStatus: (t) => seen.push(t),
|
||||
timeoutMs: 8000,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
});
|
||||
// The order improvise-vep8 puts them in: armed first, then the reason.
|
||||
shell.arm("the in-page worker did not answer (see console)");
|
||||
assert.equal(shell.timesArmed(), 1, "the denial below is vacuous if it was never armed");
|
||||
shell.fail(noticeText({ kind: "ReadOnly", document: "a.improv", detail: "" }));
|
||||
clock.fire();
|
||||
assert.match(seen.at(-1), /read-only/);
|
||||
assert.equal(
|
||||
seen.some((t) => t.includes("did not answer")),
|
||||
false,
|
||||
`the watchdog spoke over a precise reason: ${JSON.stringify(seen)}`,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user