From 145c09f7a602507bdeee4dad95e1a9cba5f213bf Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Thu, 22 Oct 2026 10:17:10 -0400 Subject: [PATCH 1/3] [client-reliability] client-reliability-9 record HEAD retention reproduction --- ...TEXT_2026-10-22_14-10-00_client-reliability-9.md | 1 + ...IONS_2026-10-22_14-12-00_client-reliability-9.md | 3 +++ docs/agents/backlog/client-reliability-9.json | 5 +++-- ...2026-10-22_14-12-00_client-reliability-9-head.md | 13 +++++++++++++ ...TODO_2026-10-22_14-10-00_client-reliability-9.md | 3 +++ ...TODO_2026-10-22_14-12-00_client-reliability-9.md | 6 ++++++ 6 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 context/CONTEXT_2026-10-22_14-10-00_client-reliability-9.md create mode 100644 decisions/DECISIONS_2026-10-22_14-12-00_client-reliability-9.md create mode 100644 test_logs/TEST_LOG_2026-10-22_14-12-00_client-reliability-9-head.md create mode 100644 todo/TODO_2026-10-22_14-10-00_client-reliability-9.md create mode 100644 todo/TODO_2026-10-22_14-12-00_client-reliability-9.md diff --git a/context/CONTEXT_2026-10-22_14-10-00_client-reliability-9.md b/context/CONTEXT_2026-10-22_14-10-00_client-reliability-9.md new file mode 100644 index 00000000..2339cc57 --- /dev/null +++ b/context/CONTEXT_2026-10-22_14-10-00_client-reliability-9.md @@ -0,0 +1 @@ +Client9 at c5b678f7; investigate HEAD owner cancellation only via actual observer/controller and controlled fetch. No product/retained tests/full/build/browser. HLS/torrent remain separate. Success: counts for disposal/shared/rejoin/cache/timers/media and scoped proposal to dispatch. diff --git a/decisions/DECISIONS_2026-10-22_14-12-00_client-reliability-9.md b/decisions/DECISIONS_2026-10-22_14-12-00_client-reliability-9.md new file mode 100644 index 00000000..00ae39b5 --- /dev/null +++ b/decisions/DECISIONS_2026-10-22_14-12-00_client-reliability-9.md @@ -0,0 +1,3 @@ +- HEAD slice only. Current media cancellation hook does not cover fetch; reproduce real ownership flow before proposing hook reuse. +- Do not change product/tests without dispatch contract routing. Keep HLS/torrent and wide Stage3 completion open. +- No redundant full/lint/build/browser for scratch investigation; QA slot untouched. diff --git a/docs/agents/backlog/client-reliability-9.json b/docs/agents/backlog/client-reliability-9.json index fcbc3233..cb4ed56c 100644 --- a/docs/agents/backlog/client-reliability-9.json +++ b/docs/agents/backlog/client-reliability-9.json @@ -1,10 +1,11 @@ { "id": "client-reliability-9", "area": "client-reliability", - "status": "open", + "status": "in-progress", "title": "Measure remaining active HEAD HLS torrent cancellation at current HEAD", "blockedOn": null, "addedOn": "2026-10-22", "filedAt": "fb54cea3a2ba99b4860421724a5050af6b97c293", - "brief": "docs/agents/CAMPAIGN-2026-10-22.md" + "brief": "docs/agents/CAMPAIGN-2026-10-22.md", + "evidence": "c5b678f7 controlled actual observer/controller HEAD: disposed/same-turn/later3 natural failures (abort0,timer1,unsettled,one obsolete fetch reused),8 shared/cross/legacy/live success-error-timeout/late completion controls pass. No product/test changes; waiting independent confirmation+HEAD-only implementation/spec routing. See test_logs/TEST_LOG_2026-10-22_14-12-00_client-reliability-9-head.md and /tmp/client9-head-repro.mjs." } diff --git a/test_logs/TEST_LOG_2026-10-22_14-12-00_client-reliability-9-head.md b/test_logs/TEST_LOG_2026-10-22_14-12-00_client-reliability-9-head.md new file mode 100644 index 00000000..90e52fdd --- /dev/null +++ b/test_logs/TEST_LOG_2026-10-22_14-12-00_client-reliability-9-head.md @@ -0,0 +1,13 @@ +# client-reliability-9 HEAD investigation + +- HEAD: c5b678f7c9d0b92fbf786b16ae831f1be1d0b899; filedAt fb54cea3. Controller/observer/cache diff between filedAt and HEAD is empty. Item still applies; claimed. +- Scratch: /tmp/client9-head-repro.mjs, derived from real lifecycle fixture; imports actual observer/controller/cache. Controlled fetch and AbortSignal; manually driven positive-delay timeout callbacks; actual DOM/media events. Zero-delay JSDOM tasks use native timers. No public IO, sleep, retry, forced exit or internal probe override. +- Command per mode: `env -u NODE_TEST_CONTEXT node /tmp/client9-head-repro.mjs `; /tmp/client9-.tap; structured /tmp/client9-results.json. +- Three natural failures (1 scenario each, exit1, cancelled0): disposed, same-turn, later. Last owner leaves HEAD1/abort0/timer1(5000ms)/unsettled/cache0/media0/video slots0/queued0/original inflight retained. Rejoin starts1 HEAD total, expected2. This is retained network work, not media-slot retention: HEAD has no shared slot cap in this controller. +- Eight natural passes (exit0, 1 scenario each, cancelled0): shared, cross, legacy, live-success, live-error, live-timeout, late-success, late-error. Shared survivor remains un-aborted and receives healthy result; disposed badge checking. Live HEAD success/error/timeout proceed to successful controlled media and healthy cache; timeout abort1. Completed probe timers0, video slots0. Late HEAD success/error after disposal settles null/cache0/media0/mirror0; existing guards work eventually, but not promptly. +- Cleanup resolves all controlled outstanding HEAD requests, completes created media, disposes registrations, restores globals and closes JSDOM before natural exit. Initial diagnostic intercepted a JSDOM0ms timer; corrected capture to delegate0ms tasks, repeated all11 modes above. Final pass timer counts exclude no real probe timer. +- Inspection: retainUrlProbe calls lifecycle.cancelActive on final owner loss, but probeUrl HEAD phase installs none. Existing hook only covers active media. Proposed smallest product boundary: js/ui/urlHealthController.js probeUrl HEAD phase only; register identity-checked lifecycle cancellation hook, mark cancelled synchronously, abort/race-settle cancelled, clear timer and hook on every completion; explicitly skip fallback/mirrors/cache on cancel. Existing shared-owner release logic/cache identity guards stay unchanged. +- Proposed retained test boundary: new focused HEAD lifecycle suite or tests/url-health-lifecycle.test.mjs plus unique integrity/spec notes. Preserve current29 media scenarios and live HEAD error/timeout fallback. Add both rejoin timings, shared/cross/legacy controls, cancellation with abort-unresponsive transport, late callbacks/replacement identity and transition-to-media/FIFO checks before readiness. Need explicit routing/spec contract before writes. +- Unverified: HLS/torrent active cancellation, browser/network cancellation bytes, FIFO competition/replacement late callbacks (not yet reproduced in this HEAD scratch), no-AbortController behavior, whole Stage3, real playback/CPU, full suite/lint/build/browser. QA expensive slot untouched. +- Open PR check attempted prescribed GitHub API via curl: exit6 DNS could not resolve api.github.com. No product changes/PR creation; overlap not remotely qualified. +- No product/retained test changes. Investigation evidence only; waiting for independent reproduction and minimal implementation routing. diff --git a/todo/TODO_2026-10-22_14-10-00_client-reliability-9.md b/todo/TODO_2026-10-22_14-10-00_client-reliability-9.md new file mode 100644 index 00000000..8a2207e6 --- /dev/null +++ b/todo/TODO_2026-10-22_14-10-00_client-reliability-9.md @@ -0,0 +1,3 @@ +- [x] Read rules/campaign/mail; clean fast-forward; claim9; ack dispatch. +- [ ] Reproduce active HEAD final-owner disposal and controls. +- [ ] Report evidence, proposed files and contract; await routing before implementation. diff --git a/todo/TODO_2026-10-22_14-12-00_client-reliability-9.md b/todo/TODO_2026-10-22_14-12-00_client-reliability-9.md new file mode 100644 index 00000000..1f6d4afd --- /dev/null +++ b/todo/TODO_2026-10-22_14-12-00_client-reliability-9.md @@ -0,0 +1,6 @@ +- [x] Safe sync/claim/ack and source review. +- [x] HEAD disposal/rejoin3 natural failures;8 controls pass. +- [x] Record exact commands/counts/limits and scoped proposal. +- [ ] Dispatch independent reproduction + explicit implementation/spec routing. +- [ ] Later: retained HEAD regression/mutants/focused/lint and allocated normal gates. +- [ ] HLS/torrent separately measured/routed. From e9c960edcf9f4ffd820f17dd6cf9dfe081e6bd29 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Thu, 22 Oct 2026 10:26:08 -0400 Subject: [PATCH 2/3] [client-reliability] client-reliability-9 cancel ownerless active HEAD probes --- ...026-10-22_14-25-00_client-reliability-9.md | 1 + ...-10-22-client-reliability-9-active-head.md | 11 + js/ui/urlHealthController.js | 48 +- ...TEGRITY_2026-10-22_client-reliability-9.md | 73 +++ ...-22_14-25-00_client-reliability-9-ready.md | 12 + tests/url-health-head-lifecycle.test.mjs | 513 ++++++++++++++++++ ...026-10-22_14-25-00_client-reliability-9.md | 6 + 7 files changed, 654 insertions(+), 10 deletions(-) create mode 100644 context/CONTEXT_2026-10-22_14-25-00_client-reliability-9.md create mode 100644 docs/spec_changes/2026-10-22-client-reliability-9-active-head.md create mode 100644 test_logs/TEST_INTEGRITY_2026-10-22_client-reliability-9.md create mode 100644 test_logs/TEST_LOG_2026-10-22_14-25-00_client-reliability-9-ready.md create mode 100644 tests/url-health-head-lifecycle.test.mjs create mode 100644 todo/TODO_2026-10-22_14-25-00_client-reliability-9.md diff --git a/context/CONTEXT_2026-10-22_14-25-00_client-reliability-9.md b/context/CONTEXT_2026-10-22_14-25-00_client-reliability-9.md new file mode 100644 index 00000000..60ef49a3 --- /dev/null +++ b/context/CONTEXT_2026-10-22_14-25-00_client-reliability-9.md @@ -0,0 +1 @@ +Client9 HEAD-only candidate ready: controller cancel hook/abort+raced prompt settlement/timer identity cleanup;23new HEAD+37existing scenarios pass,16failing isolated mutants/cancelled0,lint11 pass. No existing test expectation edits. Explicit strict contract/integrity notes. Next commit candidate, merge current unstable, request full/browser slot; do not submit or run expensive jobs without grant. HLS/torrent separately open. diff --git a/docs/spec_changes/2026-10-22-client-reliability-9-active-head.md b/docs/spec_changes/2026-10-22-client-reliability-9-active-head.md new file mode 100644 index 00000000..2d7e2793 --- /dev/null +++ b/docs/spec_changes/2026-10-22-client-reliability-9-active-head.md @@ -0,0 +1,11 @@ +# Active HEAD owner cancellation + +Source: docs/performance-efficiency-plan.md Stage3, campaign2026-10-22 priority2, fleet/RULE-CHANGES.md2026-10-22 and explicit dispatch routing56417d11 durably landed e04549f8. + +Old scoped behavior: queued work and active media cancel on final-owner disposal, but active HEAD fetch remains until response/error/timeout. A fresh observer may join that obsolete inflight record. Controlled11-mode reproduction independently confirmed3 failures/8 controls. + +New contract: final registered owner loss invalidates active HEAD synchronously, requests transport abort when supported and promptly settles the observer chain null independently of transport cooperation. HEAD timer and cancellation hook are released. Same-turn/later consumers start fresh work; obsolete response/error/timer/finalizer cannot affect replacement, cache, badges, media or mirrors. Unsupported AbortController still permits logical settlement and cleanup, but cannot promise transport abort. + +Preserve shared and cross-controller ownership, legacy consumers, live HEAD success/error/timeout/opaque/bad-status media fallback and cache, media-hook handoff and FIFO/cap. Existing29 media lifecycle scenarios and4+4 adjacent controller/multisource scenarios remain unchanged. No retained assertion was relaxed or removed;23 new HEAD scenarios tighten previously uncovered behavior. This is a scope extension, not retroactive proof of HEAD cancellation by item8. + +Only js/ui/urlHealthController.js HEAD phase changes. Observer, cache, app, HLS, torrent, RangeGET and limits/TTLs unchanged. HLS/torrent remain separately open. No browser/real network bytes/fullStage3 qualification. diff --git a/js/ui/urlHealthController.js b/js/ui/urlHealthController.js index e2f66a9f..6af13135 100644 --- a/js/ui/urlHealthController.js +++ b/js/ui/urlHealthController.js @@ -562,16 +562,44 @@ export default class UrlHealthController { return result; }; + const lifecycle = options.lifecycle; + if (lifecycle && (lifecycle.cancelled || !lifecycle.owners.size)) { + lifecycle.cancelled = true; + return { outcome: "cancelled" }; + } const supportsAbort = typeof AbortController !== "undefined"; const controller = supportsAbort ? new AbortController() : null; let timeoutId = null; + let resolveCancellation; + const cancellation = new Promise((resolve) => { + resolveCancellation = resolve; + }); + const cancelActive = () => { + // Invalidate before resolving: same-turn rejoin must start fresh work. + lifecycle.cancelled = true; + resolveCancellation({ outcome: "cancelled" }); + // Settlement must not depend on the transport observing this signal. + controller?.abort(); + }; + if (lifecycle) lifecycle.cancelActive = cancelActive; + const cleanupHead = () => { + if (timeoutId) clearTimeout(timeoutId); + if (lifecycle?.cancelActive === cancelActive) { + lifecycle.cancelActive = null; + } + }; const racers = [ - fetch(trimmed, { - method: "HEAD", - mode: "no-cors", - cache: "no-store", - signal: controller ? controller.signal : undefined + cancellation, + new Promise((resolve) => { + resolve( + fetch(trimmed, { + method: "HEAD", + mode: "no-cors", + cache: "no-store", + signal: controller ? controller.signal : undefined + }) + ); }) ]; @@ -596,9 +624,8 @@ export default class UrlHealthController { try { responseOrTimeout = await Promise.race(racers); } catch (err) { - if (timeoutId) { - clearTimeout(timeoutId); - } + cleanupHead(); + if (lifecycle?.cancelled) return { outcome: "cancelled" }; this.logger.warn(`[probeUrl] HEAD request failed for ${trimmed}:`, err); const fallback = await confirmWithVideoElement(); if (fallback) { @@ -609,8 +636,9 @@ export default class UrlHealthController { : { outcome: "unknown", error: err }; } - if (timeoutId) { - clearTimeout(timeoutId); + cleanupHead(); + if (lifecycle?.cancelled || responseOrTimeout?.outcome === "cancelled") { + return { outcome: "cancelled" }; } if (responseOrTimeout && responseOrTimeout.outcome === "timeout") { diff --git a/test_logs/TEST_INTEGRITY_2026-10-22_client-reliability-9.md b/test_logs/TEST_INTEGRITY_2026-10-22_client-reliability-9.md new file mode 100644 index 00000000..bd24989f --- /dev/null +++ b/test_logs/TEST_INTEGRITY_2026-10-22_client-reliability-9.md @@ -0,0 +1,73 @@ +# HEAD lifecycle integrity + +```yaml +test_integrity_note: + change_type: ["new_tests", "spec_correction"] + scenarios: + - id: SCN-head-final-owner + given: "actual observer/controller HEAD with abortable, ignoring-abort or missing AbortController transport" + when: "final owner disposes, including duplicate disposal and already-completed HEAD boundary" + then: "prompt null settlement, timer cleanup, no cache/media/mirror/stale badge; abort if supported" + - id: SCN-head-replacement + given: "same-turn or later rejoin after disposal" + when: "obsolete success, error or timeout callback runs" + then: "fresh HEAD and inflight identity preserved, fresh healthy result only reaches live badge" + - id: SCN-head-sharing + given: "same-controller, cross-controller or legacy live survivor" + when: "other observer disposes then HEAD/media completes" + then: "one un-aborted HEAD, survivor gets healthy cache/badge, disposed badge unchanged" + - id: SCN-head-live-fallback + given: "live HEAD success/error/timeout/opaque/405 or failing media and mirror" + when: "controlled transport/media settles" + then: "healthy fallback or honest offline cache, timers/media slots released" + - id: SCN-head-handoff-fifo + given: "HEAD transitioning to media or saturated real media queue" + when: "owner leaves or live media releases slots" + then: "new media cancellation hook works, live FIFO and cap3 remain intact" + - id: SCN-head-before-start + given: "lifecycle already cancelled or without owners" + when: "real probeUrl entry is called" + then: "cancelled result, no fetch/media/timer" + observable_outcomes: + - "fetch count/AbortSignal, settled value, timer/source/listener cleanup" + - "real cache/inflight identity, live/disposed badges and media queue/FIFO" + determinism_controls: + - "controlled fetch with native AbortSignal and optional ignored abort" + - "manually driven positive-delay timers, native zero-delay JSDOM tasks" + - "actual observer/controller/cache/DOM media events; external network never used" + - "cleanup completes controlled transports/media and restores globals before natural exit" + anti_cheat_rationale: + prevents: + - "abort signal without prompt settlement" + - "null-only fake fix retaining transport or timer" + - "reuse of obsolete request or cache-on-cancel" + - "dropping shared/legacy owners or fallback/FIFO behavior" + - "stale inflight finalizer deleting fresh work" + - "retry/sleep/assertion relaxation" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +Spec scope extension: docs/spec_changes/2026-10-22-client-reliability-9-active-head.md. No existing test was changed. Original controller fails12/23 assertions and passes11 controls, cancelled0. Fixed controller23/23. All16 isolated mutants exit1 with actual failed assertions/cancelled0; production source never contains a mutant. Isolated cache-finalizer mutant changes a scratch copy only. + +| Mutant | Failed scenarios | Pass | Exit | +|---|---:|---:|---:| +| baseline | 12 | 11 | 1 | +| no-head-hook | 10 | 13 | 1 | +| no-head-abort | 2 | 21 | 1 | +| no-prompt-settlement | 8 | 15 | 1 | +| no-sync-invalidation | 3 | 20 | 1 | +| retained-head-timer | 21 | 2 | 1 | +| no-prestart-guard | 2 | 21 | 1 | +| lost-shared-owner | 3 | 20 | 1 | +| cache-on-cancel | 12 | 11 | 1 | +| no-live-error-fallback | 1 | 22 | 1 | +| lost-media-hook | 1 | 22 | 1 | +| healthy-cache-corrupted | 14 | 9 | 1 | +| no-live-timeout-abort | 1 | 22 | 1 | +| no-mirror-fallback | 1 | 22 | 1 | +| fifo-reversed | 1 | 22 | 1 | +| stale-inflight-finalizer | 3 | 20 | 1 | + +Mutant generation/reproduction: /tmp/client9-run-mutants.py; per-mutant source/test/TAP under /tmp/client9-mutant-*.mjs/.tap; structured /tmp/client9-mutants.json. Changes disable HEAD hook/abort/prompt settlement/synchronous marker/timer cleanup/prestart guard, lose shared owner, cache on cancellation, lose live error fallback/media hook/timeout abort/mirror fallback, corrupt healthy cache, reverse FIFO and remove cache finalizer identity guard. Baseline uses pre-change git controller. Every scenario is rejected by at least one mutant (grouped coverage); none survived. No forced termination, retries or waits for real timeout. diff --git a/test_logs/TEST_LOG_2026-10-22_14-25-00_client-reliability-9-ready.md b/test_logs/TEST_LOG_2026-10-22_14-25-00_client-reliability-9-ready.md new file mode 100644 index 00000000..e0fd96ec --- /dev/null +++ b/test_logs/TEST_LOG_2026-10-22_14-25-00_client-reliability-9-ready.md @@ -0,0 +1,12 @@ +# HEAD-only candidate verification + +- Test parent e21be27f34402bb274fa698b721dc01a0fa12623 (merge of notes145c09f7 and routing unstablee04549f8). Candidate source/tests subsequently committed byte-for-byte; no full/build/browser/product submission yet. +- `node_modules/.bin/prettier --write tests/url-health-head-lifecycle.test.mjs js/ui/urlHealthController.js`: formatting complete; product38+/10-, focused new suite23 scenarios. `git diff --check` exit0. +- Initial `env -u NODE_TEST_CONTEXT node --test tests/url-health-head-lifecycle.test.mjs` baseline/fixed produced file-level1 summary only (exit1/0); not used for scenario counts. Switched to direct Node per repo runner. Initial before-start baseline awaited blocked transport and cancelled3 tests; changed test to first assert no transport after draining continuations so broken variants fail assertions and clean naturally. No expectation relaxed. +- Final original-controller reproduction: /tmp/client9-baseline-tests.mjs imports isolated pre-change controller with absolute dependency imports. `env -u NODE_TEST_CONTEXT node /tmp/client9-baseline-tests.mjs`: exit1,23 total/11pass/12fail/0cancelled. /tmp/client9-direct-baseline.tap. Fixed direct23/23 exit0, /tmp/client9-direct-fixed.tap (before last test-only no-start assertion order adjustment); authoritative final focused run below covers final bytes. +- `env -u NODE_TEST_CONTEXT node scripts/run-targeted-tests.mjs tests/url-health-head-lifecycle.test.mjs tests/url-health-lifecycle.test.mjs tests/url-health-multisource.test.mjs tests/ui/url-health-controller.test.mjs`: /tmp/client9-focused.tap shows23+29+4+4 pass but initial concurrent tool completion metadata exit2 was inconsistent with all child results and runner success. Retained that observation; authoritative host rerun clears shard context too. +- `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node scripts/run-targeted-tests.mjs tests/url-health-head-lifecycle.test.mjs tests/url-health-lifecycle.test.mjs tests/url-health-multisource.test.mjs tests/ui/url-health-controller.test.mjs`: approved host exit0,4files/60scenarios pass,0failed/0cancelled. /tmp/client9-focused-host.tap. Existing29+4+4 unchanged. +- `env -u NODE_TEST_CONTEXT npm run lint`: initial sandbox /tmp/client9-lint.log failed at git spawnEPERM. Approved identical host run /tmp/client9-lint-host.log exit0,11lint checks completed. No suppression/config/baseline edit. +- `python3 /tmp/client9-run-mutants.py`: final16 isolated mutants, each exit1/assertionfail>0/cancelled0, all natural exits. /tmp/client9-mutants.json and per-mutant TAP. Initial no-head-hook mutant accidentally targeted media due substring indentation; corrected to HEAD suffix and reran all16. Coverage and counts in integrity note. No product file mutated for experiments. +- Measured new contract: cancelled HEAD prompt null despite uncooperative/noAbort transport; timer0; fresh same-turn and later request; late success/error/timer/replacement identity; shared/cross/legacy live continuation; live fallback/cache; handoff cancellation; media cap3/FIFO preserved. Controller HEAD-only; no observer/cache/app/HLS/torrent edits. +- Unverified: unchanged full normal gate, build/artifact/browser/native abort/network bytes, real playback/CPU, HLS/torrent active cancellation/wholeStage3, remote open PR overlap (prior DNS exit6). No product queue submit or expensive-slot acquisition. Dispatch routing required for next gates. diff --git a/tests/url-health-head-lifecycle.test.mjs b/tests/url-health-head-lifecycle.test.mjs new file mode 100644 index 00000000..bbb6eb6e --- /dev/null +++ b/tests/url-health-head-lifecycle.test.mjs @@ -0,0 +1,513 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { JSDOM } from "jsdom"; +// Resolve real cache dependencies with installed Node packages before DOM setup. +import * as cache from "../js/state/cache.js"; +import UrlHealthController from "../js/ui/urlHealthController.js"; +import { attachUrlHealthBadges } from "../js/urlHealthObserver.js"; +import { + setCardLivenessPolicy, + getCardLivenessPolicy +} from "../js/constants.js"; +import { updateVideoCardSourceVisibility } from "../js/utils/cardSourceVisibility.js"; + +const flush = () => new Promise((resolve) => setImmediate(resolve)); +let sequence = 0; +function fixture(t, { ignoreAbort = false, missingAbort = false } = {}) { + const dom = new JSDOM("", { + url: "https://example.invalid" + }); + const names = [ + "window", + "document", + "HTMLElement", + "localStorage", + "fetch", + "IntersectionObserver", + "requestAnimationFrame", + "setTimeout", + "clearTimeout", + "AbortController" + ]; + const saved = names.map((key) => [ + key, + Object.getOwnPropertyDescriptor(globalThis, key) + ]); + Object.assign(globalThis, { + window: dom.window, + document: dom.window.document, + HTMLElement: dom.window.HTMLElement, + localStorage: dom.window.localStorage + }); + if (missingAbort) globalThis.AbortController = undefined; + const policy = getCardLivenessPolicy(); + setCardLivenessPolicy("show-pending"); + const resources = new Set(), + listeners = new Map(), + handlers = new Map(), + pauses = []; + const observers = [], + media = [], + heads = [], + promises = [], + warnings = [], + registrations = [], + visibility = [], + checks = []; + const id = (name) => `url-lifecycle-${++sequence}-${name}`; + const url = (name) => `https://media.invalid/${name}.mp4`; + globalThis.IntersectionObserver = class { + constructor(callback) { + this.callback = callback; + this.cards = []; + observers.push(this); + } + observe(card) { + this.cards.push(card); + } + takeRecords() { + return []; + } + disconnect() { + this.disconnected = true; + } + show() { + this.callback( + this.cards.map((target) => ({ + target, + isIntersecting: true, + intersectionRatio: 1 + })) + ); + } + }; + const timers = new Map(), + nativeSetTimeout = globalThis.setTimeout, + nativeClearTimeout = globalThis.clearTimeout; + globalThis.setTimeout = (fn, ms) => { + if (!ms) return nativeSetTimeout(fn, ms); + const handle = {}; + timers.set(handle, { fn, ms }); + return handle; + }; + globalThis.clearTimeout = (handle) => { + if (!timers.delete(handle)) nativeClearTimeout(handle); + }; + globalThis.fetch = (source, options) => + new Promise((resolve, reject) => { + const head = { source, options, resolve, reject, aborted: 0 }; + heads.push(head); + options.signal?.addEventListener( + "abort", + () => { + head.aborted++; + if (!ignoreAbort) reject(new DOMException("Aborted", "AbortError")); + }, + { once: true } + ); + }); + const proto = dom.window.HTMLMediaElement.prototype; + const addListener = proto.addEventListener, + removeListener = proto.removeEventListener; + proto.addEventListener = function (type, fn, options) { + if (["loadeddata", "canplay", "error"].includes(type)) { + if (!listeners.has(this)) listeners.set(this, new Map()); + if (!handlers.has(this)) handlers.set(this, new Map()); + listeners.get(this).set(type, fn); + handlers.get(this).set(type, fn); + } + return addListener.call(this, type, fn, options); + }; + proto.removeEventListener = function (type, fn, options) { + listeners.get(this)?.delete(type); + return removeListener.call(this, type, fn, options); + }; + proto.load = function () { + if (this.getAttribute("src")) { + if (!media.includes(this)) media.push(this); + resources.add(this); + } else resources.delete(this); + }; + proto.pause = function () { + pauses.push(this); + }; + const controller = new UrlHealthController({ + state: { + ...cache, + setInFlightUrlProbe(...args) { + promises.push(args[2]); + return cache.setInFlightUrlProbe(...args); + } + }, + logger: { + warn(...args) { + warnings.push(args); + } + }, + utils: { + updateVideoCardSourceVisibility(card) { + visibility.push(card); + updateVideoCardSourceVisibility(card); + } + } + }); + function feed( + videos, + { + container = document.createElement("section"), + consumer = controller + } = {} + ) { + container.replaceChildren(); + for (const video of videos) { + const card = document.createElement("article"); + card.className = "card"; + card.dataset.videoId = video.id; + card.dataset.urlHealthEventId = video.id; + card.dataset.urlHealthUrl = encodeURIComponent(video.url); + card.dataset.urlHealthState = "checking"; + card.dataset.streamHealthState = "unsupported"; + const badge = document.createElement("span"); + badge.dataset.urlHealthState = "checking"; + card.append(badge); + container.append(card); + updateVideoCardSourceVisibility(card); + } + document.body.append(container); + const registration = { container, videos }; + registration.dispose = attachUrlHealthBadges( + container, + ({ badgeEl, url, eventId }) => { + checks.push(eventId); + consumer.handleUrlHealthBadge({ + video: videos.find((v) => v.id === eventId), + url, + badgeEl + }); + } + ); + registration.observer = observers.at(-1); + registration.cards = [...container.children]; + registration.badges = registration.cards.map( + (card) => card.firstElementChild + ); + registrations.push(registration); + return registration; + } + const video = (name) => { + const key = id(name); + return { id: key, url: url(key) }; + }; + const done = (element, event = "loadeddata") => + element.dispatchEvent(new dom.window.Event(event)); + async function finishAll() { + // Event-driven drain: each iteration completes the currently created media. + // No wall-clock delay or retry of a failed assertion. + for (const head of heads) + head.resolve({ ok: true, status: 200, type: "basic" }); + await flush(); + let handled = 0; + while (handled < media.length) { + for (const element of media.slice(handled)) { + done(element); + handled++; + } + await flush(); + } + await Promise.all(promises); + await flush(); + } + t.after(async () => { + for (const registration of registrations) registration.dispose(); + await finishAll(); + dom.window.close(); + setCardLivenessPolicy(policy); + for (const [key, descriptor] of saved) { + if (descriptor) Object.defineProperty(globalThis, key, descriptor); + else delete globalThis[key]; + } + }); + return { + dom, + resources, + listeners, + handlers, + pauses, + controller, + media, + heads, + timers, + promises, + warnings, + visibility, + checks, + feed, + video, + done, + finishAll + }; +} +// All transport and time boundaries are controlled; ownership, cache, observer, +// controller, DOM events and media queue are real. Flush drains continuations, +// never a wall-clock wait or assertion retry. +const inFlight = (v) => + cache.getInFlightUrlProbe(v.id, v.url, { confirmPlayable: true }); +const cached = (v) => cache.getCachedUrlHealth(v.id, v.url); +function tracking(promise) { + const result = { settled: false }; + promise.then((value) => Object.assign(result, { settled: true, value })); + return result; +} +async function start(f, name = "head") { + const v = f.video(name); + v.sources = [{ url: `${v.url}?mirror` }]; + const old = f.feed([v]); + old.observer.show(); + await flush(); + assert.equal(f.heads.length, 1); + assert.equal(f.media.length, 0); + return { v, old, promise: f.promises[0], result: tracking(f.promises[0]) }; +} +function cleanCancelled(f, v, old, result) { + assert.equal(result.settled, true, "cancelled HEAD must settle promptly"); + assert.equal(result.value, null); + assert.equal(cached(v), null); + assert.equal(inFlight(v), null); + assert.equal(f.timers.size, 0, "no HEAD or media timer retained"); + assert.equal(f.heads.length, 1, "no obsolete mirror"); + assert.equal(f.media.length, 0, "no obsolete media fallback"); + assert.equal(f.controller.activeVideoProbes, 0); + assert.equal(old.badges[0].dataset.urlHealthState, "checking"); +} +for (const transport of ["abortable", "ignores-abort", "missing-abort"]) { + test(`last HEAD owner disposal releases promise/timer with ${transport} transport`, async (t) => { + const f = fixture(t, { + ignoreAbort: transport === "ignores-abort", + missingAbort: transport === "missing-abort" + }); + const { v, old, result } = await start(f); + assert.equal(f.timers.size, 1); + old.dispose(); + old.dispose(); + await flush(); + assert.equal(f.heads[0].aborted, transport === "missing-abort" ? 0 : 1); + cleanCancelled(f, v, old, result); + // Even a transport that never settles its own promise cannot block release. + }); +} +for (const timing of ["same-turn", "after-flush"]) { + for (const late of ["success", "error", "timeout"]) { + test(`HEAD ${timing} rejoin has fresh identity despite late ${late}`, async (t) => { + const f = fixture(t, { ignoreAbort: true }); + const { v, old, promise, result } = await start(f); + const oldTimer = [...f.timers.values()][0].fn; + old.dispose(); + if (timing === "after-flush") await flush(); + const live = f.feed([v]); + live.observer.show(); + const replacement = inFlight(v); + assert.notEqual(replacement, promise, "same-turn owner must start fresh"); + await flush(); + assert.equal(f.heads.length, 2); + assert.equal(result.settled, true); + assert.equal(result.value, null); + assert.equal( + inFlight(v), + replacement, + "old finalizer preserves replacement" + ); + if (late === "success") + f.heads[0].resolve({ ok: true, status: 200, type: "basic" }); + if (late === "error") f.heads[0].reject(new Error("obsolete HEAD error")); + if (late === "timeout") oldTimer(); + await flush(); + assert.equal(inFlight(v), replacement); + assert.equal(cached(v), null); + assert.equal(f.media.length, 0); + assert.equal(f.timers.size, 1, "only replacement timer remains"); + assert.equal(old.badges[0].dataset.urlHealthState, "checking"); + f.heads[1].resolve({ ok: true, status: 200, type: "basic" }); + await flush(); + assert.equal(f.media.length, 1); + f.done(f.media[0]); + await flush(); + assert.equal(cached(v).status, "healthy"); + assert.equal(live.badges[0].dataset.urlHealthState, "healthy"); + assert.equal(old.badges[0].dataset.urlHealthState, "checking"); + assert.equal(f.timers.size, 0); + assert.equal(f.controller.activeVideoProbes, 0); + }); + } +} +for (const kind of ["shared", "cross-controller", "legacy"]) { + test(`HEAD disposal preserves ${kind} survivor and live media handoff`, async (t) => { + const f = fixture(t); + const { v, old, result } = await start(f); + const consumer = + kind === "cross-controller" + ? new UrlHealthController({ state: cache, logger: { warn() {} } }) + : f.controller; + let live; + if (kind === "legacy") { + const badge = document.createElement("span"); + document.body.append(badge); + consumer.handleUrlHealthBadge({ video: v, url: v.url, badgeEl: badge }); + live = { badges: [badge] }; + } else { + live = f.feed([v], { consumer }); + live.observer.show(); + } + await flush(); + old.dispose(); + await flush(); + assert.equal(f.heads.length, 1); + assert.equal(f.heads[0].aborted, 0); + assert.equal(result.settled, false); + assert.equal(f.timers.size, 1); + f.heads[0].resolve({ ok: true, status: 200, type: "basic" }); + await flush(); + assert.equal(f.media.length, 1); + f.done(f.media[0]); + await flush(); + assert.equal(cached(v).status, "healthy"); + assert.equal(live.badges[0].dataset.urlHealthState, "healthy"); + assert.equal(old.badges[0].dataset.urlHealthState, "checking"); + assert.equal(f.timers.size, 0); + }); +} +for (const response of [ + "success", + "error", + "timeout", + "opaque", + "bad-status" +]) { + test(`live HEAD ${response} retains playable media fallback/cache`, async (t) => { + const f = fixture(t); + const { v, old, result } = await start(f); + if (response === "error") + f.heads[0].reject(new Error("controlled HEAD error")); + else if (response === "timeout") [...f.timers.values()][0].fn(); + else + f.heads[0].resolve({ + ok: response !== "bad-status", + status: response === "bad-status" ? 405 : 200, + type: response === "opaque" ? "opaque" : "basic" + }); + await flush(); + assert.equal(f.media.length, 1); + assert.equal(f.timers.size, 1); + f.done(f.media[0]); + await flush(); + assert.equal(result.settled, true); + assert.equal(cached(v).status, "healthy"); + assert.equal(old.badges[0].dataset.urlHealthState, "healthy"); + assert.equal(f.heads[0].aborted, response === "timeout" ? 1 : 0); + assert.equal(f.timers.size, 0); + assert.equal(f.controller.activeVideoProbes, 0); + }); +} +test("HEAD completion immediately before disposal never starts media or mirrors", async (t) => { + const f = fixture(t); + const { v, old, result } = await start(f); + f.heads[0].resolve({ ok: true, status: 200, type: "basic" }); + old.dispose(); + await flush(); + cleanCancelled(f, v, old, result); +}); +test("HEAD to media handoff retains the new cancellation hook", async (t) => { + const f = fixture(t); + const { v, old, result } = await start(f); + f.heads[0].resolve({ ok: true, status: 200, type: "basic" }); + await flush(); + assert.equal(f.media.length, 1); + assert.equal(f.controller.activeVideoProbes, 1); + old.dispose(); + await flush(); + assert.equal(result.settled, true); + assert.equal(result.value, null); + assert.equal(f.resources.size, 0); + assert.equal(f.listeners.get(f.media[0]).size, 0); + assert.equal(f.timers.size, 0); + assert.equal(f.controller.activeVideoProbes, 0); + assert.equal(cached(v), null); + assert.equal(f.heads.length, 1); +}); +test("HEAD cancellation during saturated media queue preserves live FIFO and cap", async (t) => { + const f = fixture(t), + videos = ["a", "b", "c", "queued1", "queued2"].map(f.video); + const live = f.feed(videos); + live.observer.show(); + await flush(); + for (const head of f.heads) + head.resolve({ ok: true, status: 200, type: "basic" }); + await flush(); + assert.equal(f.media.length, 3); + assert.equal(f.controller.activeVideoProbes, 3); + assert.equal(f.controller.videoProbeQueue.length, 2); + const obsolete = f.video("obsolete"), + old = f.feed([obsolete]); + old.observer.show(); + await flush(); + const cancelled = tracking(f.promises.at(-1)); + old.dispose(); + await flush(); + assert.equal(cancelled.settled, true); + assert.equal(cancelled.value, null); + assert.equal(f.controller.activeVideoProbes, 3); + assert.equal(f.controller.videoProbeQueue.length, 2); + f.done(f.media[0]); + await flush(); + assert.equal(f.media.length, 4); + assert.equal(f.media[3].getAttribute("src"), videos[3].url); + assert.equal(f.controller.activeVideoProbes, 3); + f.done(f.media[1]); + await flush(); + assert.equal(f.media.length, 5); + assert.equal(f.media[4].getAttribute("src"), videos[4].url); + await f.finishAll(); + assert.equal(f.controller.activeVideoProbes, 0); + assert.equal(cached(obsolete), null); + assert.equal(f.timers.size, 0); +}); +for (const state of ["no-owners", "already-cancelled"]) { + test(`HEAD ${state} lifecycle starts no fetch or timer`, async (t) => { + const f = fixture(t), + lifecycle = { + owners: new Set(), + cancelled: state === "already-cancelled" + }; + if (state === "already-cancelled") lifecycle.owners.add({ active: true }); + const promise = f.controller.probeUrl("https://media.invalid/before.mp4", { + confirmPlayable: true, + lifecycle + }); + f.promises.push(promise); + await flush(); + assert.equal(f.heads.length, 0); + assert.equal(f.timers.size, 0); + assert.equal(f.media.length, 0); + assert.deepEqual(await promise, { outcome: "cancelled" }); + }); +} +test("live HEAD and failed media still try one mirror then cache offline", async (t) => { + const f = fixture(t); + const { v, old, result } = await start(f); + f.heads[0].resolve({ ok: false, status: 405, type: "basic" }); + await flush(); + assert.equal(f.media.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); + f.heads[1].resolve({ ok: false, status: 503, type: "basic" }); + await flush(); + assert.equal(f.media.length, 2); + f.done(f.media[1], "error"); + await flush(); + assert.equal(result.settled, true); + assert.equal(cached(v).status, "offline"); + assert.equal(old.badges[0].dataset.urlHealthState, "offline"); + assert.equal(f.timers.size, 0); + assert.equal(f.controller.activeVideoProbes, 0); +}); diff --git a/todo/TODO_2026-10-22_14-25-00_client-reliability-9.md b/todo/TODO_2026-10-22_14-25-00_client-reliability-9.md new file mode 100644 index 00000000..af16fcb2 --- /dev/null +++ b/todo/TODO_2026-10-22_14-25-00_client-reliability-9.md @@ -0,0 +1,6 @@ +- [x] Independent evidence and explicit HEAD implementation routing verified landed e04549f8. +- [x] Minimal controller HEAD fix,23 real lifecycle scenarios and strict integrity/spec notes. +- [x] Focused60/4files,16naturally failing mutants,cancelled0,lint11. +- [ ] Commit/merge candidate and send READY + slot request. +- [ ] Allocated full/normal gate, durable landing/ancestry, final closure. +- [ ] HLS/torrent separately routed/measured; no wider Stage3 claim. From 4bea541e677fda9aa990fc3a1e23790789b847fe Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Thu, 22 Oct 2026 10:27:10 -0400 Subject: [PATCH 3/3] [client-reliability] client-reliability-9 record merged candidate readiness --- docs/agents/backlog/client-reliability-9.json | 2 +- .../TEST_LOG_2026-10-22_14-27-00_client-reliability-9-merged.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 test_logs/TEST_LOG_2026-10-22_14-27-00_client-reliability-9-merged.md diff --git a/docs/agents/backlog/client-reliability-9.json b/docs/agents/backlog/client-reliability-9.json index cb4ed56c..2194bd85 100644 --- a/docs/agents/backlog/client-reliability-9.json +++ b/docs/agents/backlog/client-reliability-9.json @@ -7,5 +7,5 @@ "addedOn": "2026-10-22", "filedAt": "fb54cea3a2ba99b4860421724a5050af6b97c293", "brief": "docs/agents/CAMPAIGN-2026-10-22.md", - "evidence": "c5b678f7 controlled actual observer/controller HEAD: disposed/same-turn/later3 natural failures (abort0,timer1,unsettled,one obsolete fetch reused),8 shared/cross/legacy/live success-error-timeout/late completion controls pass. No product/test changes; waiting independent confirmation+HEAD-only implementation/spec routing. See test_logs/TEST_LOG_2026-10-22_14-12-00_client-reliability-9-head.md and /tmp/client9-head-repro.mjs." + "evidence": "HEAD-only candidate e9c960ed merged e887ceaf;60focused/4files (new23+existing29+4+4),16isolated mutants naturally exit1/0cancelled,lint11 hostexit0. Strict scope extension note;no existing tests changed. Merge from current unstable docs/evidence-only,runtime inputs equal. READY awaiting full/browser/normal submit slot;HLS/torrent separately open. test_logs/TEST_LOG_2026-10-22_14-25-00_client-reliability-9-ready.md" } diff --git a/test_logs/TEST_LOG_2026-10-22_14-27-00_client-reliability-9-merged.md b/test_logs/TEST_LOG_2026-10-22_14-27-00_client-reliability-9-merged.md new file mode 100644 index 00000000..71af01bd --- /dev/null +++ b/test_logs/TEST_LOG_2026-10-22_14-27-00_client-reliability-9-merged.md @@ -0,0 +1 @@ +Product/test/integrity/spec commit e9c960edcf9f4ffd820f17dd6cf9dfe081e6bd29; merged current unstable4de357e4 as e887ceafdae9eb06a05f7cf6e8a2c50ab6f0c004. git diff --exit-code e9c960ed HEAD -- js tests package.json package-lock.json scripts config exit0; merge docs/evidence-only, prior60focused/16failingmutants/lint11 apply unchanged. git diff --check exit0;treeclean before new readiness metadata. Full/build/browser/productqueue not run;await slot.