From 12f927321fdd0a945f3bdcfb3a618ef16b54ff7c Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Tue, 20 Oct 2026 09:37:04 -0400 Subject: [PATCH] fix: stop orphaned URL mirror continuation (client-reliability-5) --- ...026-10-20_13-36-00_client-reliability-5.md | 3 + ...026-10-20_13-36-00_client-reliability-5.md | 6 + docs/TODO.md | 2 + js/ui/urlHealthController.js | 6 + .../TEST_INTEGRITY_client-reliability-5.md | 50 +++++++ ...026-10-20_13-36-00_client-reliability-5.md | 10 ++ tests/url-health-lifecycle.test.mjs | 141 ++++++++++++++++++ ...026-10-20_13-36-00_client-reliability-5.md | 7 + 8 files changed, 225 insertions(+) create mode 100644 context/CONTEXT_2026-10-20_13-36-00_client-reliability-5.md create mode 100644 decisions/DECISIONS_2026-10-20_13-36-00_client-reliability-5.md create mode 100644 test_logs/TEST_INTEGRITY_client-reliability-5.md create mode 100644 test_logs/TEST_LOG_2026-10-20_13-36-00_client-reliability-5.md create mode 100644 todo/TODO_2026-10-20_13-36-00_client-reliability-5.md diff --git a/context/CONTEXT_2026-10-20_13-36-00_client-reliability-5.md b/context/CONTEXT_2026-10-20_13-36-00_client-reliability-5.md new file mode 100644 index 00000000..894b1ca5 --- /dev/null +++ b/context/CONTEXT_2026-10-20_13-36-00_client-reliability-5.md @@ -0,0 +1,3 @@ +# Item5 implementation + +Controller-only6-line pre-candidate ownership guard. Six new lifecycle scenarios reproduce2 baseline failures and pass18/18 fixed;6 reverted mutants rejected. Unique integrity note test_logs/TEST_INTEGRITY_client-reliability-5.md. Focused/lint/build running; awaiting explicit dispatch full-suite/browser slots and verified unstable merge before final checks/landing. No active abort/observer/cache/limit changes. diff --git a/decisions/DECISIONS_2026-10-20_13-36-00_client-reliability-5.md b/decisions/DECISIONS_2026-10-20_13-36-00_client-reliability-5.md new file mode 100644 index 00000000..20e9e04b --- /dev/null +++ b/decisions/DECISIONS_2026-10-20_13-36-00_client-reliability-5.md @@ -0,0 +1,6 @@ +# Item5 continuation boundary + +- Keep bounded already-active success/failure caching. Check existing lifecycle owners only BEFORE starting each candidate in probeUrlList; zero owners marks existing record cancelled and returns explicit cancellation. No probe abort or observer/cache/TTL/concurrency change. +- Shared and rejoining owners remain eligible because record owners are checked at continuation time. Completed final-source failure keeps original cache behavior; cancelled mirror chains never cache offline. +- Six focused regressions exercise actual observer/controller/cache plus external IO twins; unique integrity file during parallel trial. +- Dispatch resource coordination: local full suite is next after administrative drain but requires explicit grant; browser belongs initially to playback QA. All CLI/gate launches clear inherited NODE_TEST_CONTEXT operationally; no kit/runner change. diff --git a/docs/TODO.md b/docs/TODO.md index 85c77bdd..e9fcaab5 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -22,6 +22,8 @@ ## client-reliability +- 2026-10-20: item5 routed controller-only pre-candidate guard implemented; baseline16pass/2fail→fixed18/18 lifecycle,6 rejected/reverted mutants,focused28/4files,lint11/build+artifact pass. No full/browser launch while dispatch administrative drain/QA reservation holds; slot requested13:32:54UTC,await explicit grant and verified unstable merge. No active abort/observer/cache/limits change; actual fallback/CPU unverified. Unique integrity/evidence in test_logs/*client-reliability-5*. + - 2026-10-20: item5 investigation at0c1dd7e8:active URL success/real4000ms controlled timeout release media/slot correctly;disposed active primary media error starts1 obsolete mirror HEAD(expected0),strict assertion naturally exits1 after cleanup/retry. Shared/rejoin owner and later same-source controls pass. Proposed controller-only pre-candidate ownership guard + focused tests; await dispatch routing. No product/tests changed; active abort/browser/fallback/CPU unverified. See test_logs/TEST_LOG_2026-10-20_13-30-00_client-reliability-5.md. - 2026-10-20: item4 done/closed after normal landing89ccb907(submission7055496b,gate exit0,unstable ancestry exit0). Full427/427 files,focused22,lifecycle12,13 reverted mutants,lint11,build/artifact/native Chromium+Firefox controlled IO pass;obsolete starts1→0. Dispatch landing event sent; preview refresh owned by dispatch. No further item without routing. Actual media fallback/live peers/active abort/CPU and whole Stage3 unverified. Closure metadata/final event follow. diff --git a/js/ui/urlHealthController.js b/js/ui/urlHealthController.js index afae005a..d99f9e84 100644 --- a/js/ui/urlHealthController.js +++ b/js/ui/urlHealthController.js @@ -463,6 +463,12 @@ export default class UrlHealthController { } let sawTimeout = false; for (const url of list) { + // Completed active work may still cache its result, but starting another + // source requires a live owner (including a card that rejoined meanwhile). + if (options.lifecycle && !options.lifecycle.owners.size) { + options.lifecycle.cancelled = true; + return { outcome: "cancelled" }; + } let result; try { result = await this.probeUrl(url, options); diff --git a/test_logs/TEST_INTEGRITY_client-reliability-5.md b/test_logs/TEST_INTEGRITY_client-reliability-5.md new file mode 100644 index 00000000..b4c32ad0 --- /dev/null +++ b/test_logs/TEST_INTEGRITY_client-reliability-5.md @@ -0,0 +1,50 @@ +# Item5 integrity note + +```yaml +test_integrity_note: + change_type: ["new_tests"] + scenarios: + - id: SCN-active-primary-disposed-mirror + given: "primary media is active, with an alternate mirror" + when: "the last owner disposes then primary media errors" + then: "no mirror HEAD starts; chain settles null without cache or stale UI, later fresh request succeeds" + - id: SCN-active-shared-mirror + given: "two registrations share active media" + when: "one owner disposes and primary errors" + then: "ordered mirror serves live owner, healthy cache avoids later requests, disposed badge stays unchanged" + - id: SCN-active-rejoin-mirror + given: "primary active media survives last-owner disposal" + when: "same-source live owner rejoins before primary error" + then: "dedup survives and ordered mirror serves new owner without duplicate primary IO" + - id: SCN-active-later-mirror-disposal + given: "first mirror media is active and another mirror exists" + when: "last owner disposes then first mirror errors" + then: "next mirror HEAD does not start, cancellation settles without cache or stale UI" + - id: SCN-active-timeout-cleanup-retry + given: "media is active with real configured initial and longer retry deadlines" + when: "last owner disposes and controlled clock reaches initial timeout" + then: "slot/source clear, no new retry resources or cache, later fresh request is healthy" + - id: SCN-completed-active-final-error-cache + given: "active primary has no further candidate" + when: "last owner disposes then media errors" + then: "completed active failure keeps existing offline cache result without stale UI" + observable_outcomes: + - "fetch/media starts, source cleanup, slot release, promise settlement and actual cache/inflight records" + - "live/disposed connected badge state and ordered mirror requests" + determinism_controls: + - "Real observer/controller/cache with controlled IntersectionObserver, jsdom native media event APIs and stubbed external fetch/media IO" + - "node:test mock clock advances configured4000ms timeout; no sleeps, retries, public network or forced exit" + anti_cheat_rationale: + prevents: + - "returning cancelled for live shared/rejoining owners" + - "guarding only first mirror or discarding completed active results" + - "caching cancellation as offline or starting obsolete retry resources" + - "over-mocking queue/controller instead of service boundaries" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +Existing assertions unchanged; six new scenarios (18 total lifecycle scenarios). Baseline before guard naturally exits1 with new primary/later-mirror failures (16pass/2fail/0cancelled). Fixed18pass/0fail/0cancelled. Unique note avoids shared integrity-file writes during parallel trial. + +`python3 /tmp/item5-mutants.py`: six deliberately broken variants each naturally exit1 with named assertion failures,0 cancelled, restored in finally before gates. Missing candidate guard kills primary/later-mirror scenarios; cancelling live mirrors kills shared+rejoin controls; checking only first mirror kills later-mirror case; caching cancellation kills null/cache expectations; discarding final active errors kills existing-offline-cache scenario; removing both media no-owner checks kills timeout cleanup/retry. Results `/tmp/item5-mutants.json` and `/tmp/item5-mutant-*.log`. No gate/test-registration changes or timed-out mutants counted. diff --git a/test_logs/TEST_LOG_2026-10-20_13-36-00_client-reliability-5.md b/test_logs/TEST_LOG_2026-10-20_13-36-00_client-reliability-5.md new file mode 100644 index 00000000..8873ea4e --- /dev/null +++ b/test_logs/TEST_LOG_2026-10-20_13-36-00_client-reliability-5.md @@ -0,0 +1,10 @@ +# Item5 precommit evidence + +- `env -u NODE_TEST_CONTEXT node tests/url-health-lifecycle.test.mjs` before guard: natural exit1,16pass/2fail/0cancelled, disposed active primary and later-mirror cases fail. After guard and formatting: natural exit0,18/18,0 failed/cancelled/skipped (`/tmp/item5-lifecycle-final.log`). +- `python3 /tmp/item5-mutants.py`: six/six reverted variants naturally exit1 with named assertion failures,0 cancelled; no timeouts or survivors. Unique integrity mapping test_logs/TEST_INTEGRITY_client-reliability-5.md. +- `env -u NODE_TEST_CONTEXT node scripts/run-unit-tests.mjs url-health health-disposal`: natural exit0,4 files/28 scenarios (lifecycle18,controller4,multisource4,view lifecycle2). `/tmp/item5-focused.log`. +- `env -u NODE_TEST_CONTEXT npm run lint` with approved git subprocess access: exit0,11 stages (`/tmp/item5-lint.log`). +- `env -u NODE_TEST_CONTEXT npm run build`: exit0 including artifact verification (`/tmp/item5-build.log`). No dependency/runner/gate changes. +- First mailbox_read tool review hit its deadline; accumulated visible tool waits at least57s, exact approval wall duration not separately instrumented. It was a tool/approval stall, not product failure or rejection. One permitted retry succeeded; pickup mailbox sent13:27:49 UTC. +- Full-suite/browser slots requested13:32:54 UTC. Dispatch queued local full slot behind administrative drain and reserves initial browser to QA; no full/browser launches made. Full-slot wait duration will be measured through grant, not inferred from silence. +- Current code/tests based on0c1dd7e8 plus notes-only140618f8; final verified unstable merge, full/browser grants/checks, unchanged normal landing/durable ancestry remain. Active abort,real decoding/playback/live peers/CPU and whole Stage3 unverified. diff --git a/tests/url-health-lifecycle.test.mjs b/tests/url-health-lifecycle.test.mjs index 1ea32a56..909310b5 100644 --- a/tests/url-health-lifecycle.test.mjs +++ b/tests/url-health-lifecycle.test.mjs @@ -436,3 +436,144 @@ test("shared inflight ownership also survives a second controller using the same assert.equal(badge.dataset.urlHealthState, "healthy"); assert.equal(f.heads.length, 4); }); + +test("active primary error after last-owner disposal starts no mirror HEAD and permits a fresh request", async (t) => { + const f = fixture(t), + v = f.video("disposed-active-mirror"); + v.sources = [{ url: "https://mirror.invalid/obsolete.mp4" }]; + const old = f.feed([v]); + old.observer.show(); + await flush(); + assert.equal(f.heads.length, 1); + assert.equal(f.media.length, 1); + old.dispose(); + f.done(f.media[0], "error"); + await flush(); + assert.equal( + f.heads.length, + 1, + "disposed chain must not start a mirror HEAD" + ); + assert.equal(await f.promises[0], null); + assert.equal(cache.getCachedUrlHealth(v.id, v.url), null); + assert.equal(f.controller.activeVideoProbes, 0); + assert.equal(f.media[0].getAttribute("src"), null); + assert.equal(old.badges[0].dataset.urlHealthState, "checking"); + const fresh = f.feed([v]); + fresh.observer.show(); + await flush(); + assert.equal(f.heads.length, 2); + assert.equal(f.media.length, 2); + f.done(f.media[1]); + await flush(); + assert.equal(fresh.badges[0].dataset.urlHealthState, "healthy"); + assert.equal(cache.getCachedUrlHealth(v.id, v.url).status, "healthy"); +}); + +for (const timing of ["shared", "rejoin"]) { + test(`active primary error preserves ordered mirror for a ${timing} live owner`, async (t) => { + const f = fixture(t), + v = f.video(`active-${timing}`); + v.sources = [ + { url: "https://mirror.invalid/live-first.mp4" }, + { url: "https://mirror.invalid/unused-second.mp4" } + ]; + const old = f.feed([v]); + old.observer.show(); + await flush(); + if (timing === "rejoin") old.dispose(); + const live = f.feed([v]); + live.observer.show(); + await flush(); + if (timing === "shared") old.dispose(); + assert.equal(f.heads.length, 1); + assert.equal(f.promises.length, 1); + f.done(f.media[0], "error"); + await flush(); + assert.equal(f.heads.length, 2); + assert.equal(f.heads[1].source, v.sources[0].url); + assert.equal(f.media.length, 2); + f.done(f.media[1]); + await flush(); + assert.equal(f.heads.length, 2, "healthy first mirror must stop fallback"); + assert.equal(live.badges[0].dataset.urlHealthState, "healthy"); + assert.equal(old.badges[0].dataset.urlHealthState, "checking"); + assert.equal(f.controller.activeVideoProbes, 0); + const cached = f.feed([v]); + cached.observer.show(); + await flush(); + assert.equal(f.heads.length, 2); + assert.equal(cached.badges[0].dataset.urlHealthState, "healthy"); + }); +} + +test("active first mirror error after disposal does not start the next mirror", async (t) => { + const f = fixture(t), + v = f.video("later-mirror"); + v.sources = [ + { url: "https://mirror.invalid/first.mp4" }, + { url: "https://mirror.invalid/obsolete-second.mp4" } + ]; + const old = f.feed([v]); + old.observer.show(); + await flush(); + f.done(f.media[0], "error"); + await flush(); + assert.equal(f.heads.length, 2); + old.dispose(); + f.done(f.media[1], "error"); + await flush(); + assert.equal(f.heads.length, 2); + assert.equal(f.media.length, 2); + assert.equal(await f.promises[0], null); + assert.equal(cache.getCachedUrlHealth(v.id, v.url), null); + assert.equal(f.controller.activeVideoProbes, 0); + assert.equal(old.badges[0].dataset.urlHealthState, "checking"); +}); + +test("disposed active media timeout releases its slot without retry resources and later requests remain playable", async (t) => { + const f = fixture(t), + v = f.video("active-timeout"); + f.controller.constants = { + URL_PROBE_TIMEOUT_MS: cache.URL_PROBE_TIMEOUT_MS, + urlHealthConstants: cache.urlHealthConstants + }; + t.mock.timers.enable({ apis: ["setTimeout"] }); + const old = f.feed([v]); + old.observer.show(); + await flush(); + old.dispose(); + t.mock.timers.tick(cache.URL_PROBE_TIMEOUT_MS); + await flush(); + assert.equal(f.heads.length, 1); + assert.equal(f.media.length, 1); + assert.equal(f.controller.activeVideoProbes, 0); + assert.equal(f.media[0].getAttribute("src"), null); + assert.equal(await f.promises[0], null); + assert.equal(cache.getCachedUrlHealth(v.id, v.url), null); + assert.equal(old.badges[0].dataset.urlHealthState, "checking"); + const fresh = f.feed([v]); + fresh.observer.show(); + await flush(); + assert.equal(f.heads.length, 2); + assert.equal(f.media.length, 2); + f.done(f.media[1]); + await flush(); + assert.equal(fresh.badges[0].dataset.urlHealthState, "healthy"); + assert.equal(f.controller.activeVideoProbes, 0); +}); + +test("completed active failure without another candidate preserves its existing offline cache result", async (t) => { + const f = fixture(t), + v = f.video("final-active-failure"), + old = f.feed([v]); + old.observer.show(); + await flush(); + old.dispose(); + f.done(f.media[0], "error"); + await flush(); + assert.equal(f.heads.length, 1); + assert.equal(f.controller.activeVideoProbes, 0); + assert.equal(cache.getCachedUrlHealth(v.id, v.url).status, "offline"); + assert.equal(old.badges[0].dataset.urlHealthState, "checking"); +}); diff --git a/todo/TODO_2026-10-20_13-36-00_client-reliability-5.md b/todo/TODO_2026-10-20_13-36-00_client-reliability-5.md new file mode 100644 index 00000000..1c279a80 --- /dev/null +++ b/todo/TODO_2026-10-20_13-36-00_client-reliability-5.md @@ -0,0 +1,7 @@ +# Item5 + +- [x] Reproduce/independent route. +- [x] Controller-only guard/six scenarios and six rejected mutants. +- [ ] Focused/lint/build results and committed merged-tree checks. +- [ ] Explicit full-suite slot and browser slot/QA coverage. +- [ ] Normal clean-env queue,durable status/ancestry/close/dispatch events.