fix(orchestrator): a view transition skipped by the next switch stays quiet

apply() starts a view transition on every hash change; a quick second switch skips the running one, whose ready promise then rejected with an unhandled AbortError, which the presence e2e counted as a page error in Chromium and Firefox. The skip is expected, so its AbortError is caught; every other ready failure is rethrown and still surfaces.

🎯 Fix: 92 %
💥 Break: 2 %
💸 Cost: 100 %
⏱️ Time: 100 %
🛡️ Security: 100 %
🏆 Score: 90
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-12 07:53:29 +00:00
1 parent 4ab13b6e65
commit 82edf43417
2 files changed
+43 -2

No files matched your search

+5 -1
View File
@@ -37,7 +37,11 @@
ui.views[route.screen] = route.view;
}
if (document.startViewTransition && drawn.screen && !calm.matches) {
document.startViewTransition(() => render(true));
document.startViewTransition(() => render(true)).ready.catch((error) => {
if (error.name !== "AbortError") {
throw error;
}
});
} else {
render(true);
}
+38 -1
View File
@@ -4,7 +4,38 @@ import json
import subprocess
import unittest
from hub_support import NAV
from hub_support import HUB, NAV
SCREENS = HUB / "templates" / "page-screens.js"
TRANSITION_PROBE = """
const fs = require("fs");
const vm = require("vm");
const code = fs.readFileSync(process.argv[1], "utf8");
const unhandled = [];
process.on("unhandledRejection", (error) => unhandled.push(error.name));
const settle = () => new Promise((resolve) => setImmediate(resolve));
function switched(name) {
const error = new Error(`transition ${name}`);
error.name = name;
const started = [];
vm.runInNewContext(`${code}\\napply();`, {
Nav: {TABS: [], route: () => ({screen: "agents", view: "chord"})}, ui: {views: {}}, drawn: {screen: true}, calm: {matches: false}, h: () => null,
window: {location: {hash: "#agents/chord"}, history: {replaceState: () => null}},
document: {getElementById: () => ({append: () => null}), startViewTransition: (update) => {
started.push(typeof update);
return {ready: Promise.reject(error), finished: Promise.resolve(), updateCallbackDone: Promise.resolve()};
}},
});
return started;
}
(async () => {
const started = [...switched("AbortError"), ...switched("InvalidStateError")];
for (let round = 0; round < 5; round += 1) {
await settle();
}
console.log(JSON.stringify({started, unhandled}));
})().catch((error) => console.log(JSON.stringify({crash: String(error)})));
"""
NAV_PROBE = """
const N = require(process.argv[1]);
@@ -90,5 +121,11 @@ class TestNavModule(unittest.TestCase):
self.assertEqual(self.data["tabOf"], ["stream", "stream", "stream", "agents", "artifacts", "tasks"])
class TestScreenTransition(unittest.TestCase):
def test_a_view_transition_skipped_by_the_next_switch_stays_quiet_and_other_failures_still_surface(self):
done = subprocess.run(["node", "-e", TRANSITION_PROBE, str(SCREENS)], capture_output=True, text=True, timeout=60, check=True)
self.assertEqual(json.loads(done.stdout), {"started": ["function", "function"], "unhandled": ["InvalidStateError"]})
if __name__ == "__main__":
unittest.main()