mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-02 19:02:28 +00:00
fix: cancel final-owner active torrent probes (client-reliability-9)
Preserve shared owners and standalone probe callers. Clear obsolete timers and defer temporary torrent destruction until event unwind, holding the queue slot through teardown. Guard cache and replacement inflight identity. Spec correction: previous queued-only active reuse/cache expectations are replaced by equally strict final-owner cleanup and fresh-request assertions, with independent live/shared/cache controls retained. See unique spec and Test Integrity Note. Saved focused checks: six files, 60 scenarios, zero failures/cancelled; 19 distinct mutants killed. Remaining full lint/full/build/browser and normal landing require a resource slot. No submission.
This commit is contained in:
1 parent
ab1ea6dee1
commit
1b2362d234
13 files changed
+644
-71
No files matched your search
@@ -0,0 +1 @@
|
||||
Approved dispatch routing read locally: actual baseline independently reproduced12=7pass5fail, no repeat needed. Scope gridHealth+optional probePeers lifecycle only. Goal final-owner null settlement/resource cleanup/fresh identity; preserve shared/standalone/live/cap4/FIFO/TTL/deferred destroy safety. Spec and integrity written first. Focused offline/mutants/targeted lint allowed; no full/build/browser/submission until resource grant.
|
||||
@@ -0,0 +1,6 @@
|
||||
- Grid publishes owner/release/inflight promise before synchronous client IO so disposal or same-source registration inside constructor/add is safe; cancelled identity removed synchronously and cache guarded.
|
||||
- Optional third probePeers lifecycle follows existing owner-record convention; standalone behavior unchanged. Cancel settlesnull after deferred temporary-torrent destroy call returns/throws; timers clear immediately, cancel hook/owned listeners removed at final teardown.
|
||||
- Retain inert error handler through deferred teardown, even cancellation insideadd; repeated/late callbacks are settled guards. Shared probeClient remains alive; queue slot only releases after destroy, preserving event unwind/cap4/FIFO.
|
||||
- Original tests367/417 contract corrected only after routed spec+integrity note; preserve separate shared-live/cache/TTL/timeout controls and strict noobsoletecache/freshrequest/resource assertions.
|
||||
- No live WebTorrent transport/byte teardown qualification from fake IO. Async low-level network shutdown not measured; existing teardown call ordering preserved.
|
||||
- Stop at completedfocused boundary for requested kit restart; no commit/merge/submission before restart, all snapshots saved.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Client9 active torrent ownership contract correction
|
||||
|
||||
Authority: dispatch docs/agents/ROUTING-2026-10-26-client9-torrent.md (17:38UTC), read locally by client after interpreter18:53 relay. Independent exact12cases=7pass/5fail/naturalexit1/network0/cleanup0 confirms client baseline. This note precedes any prior expectation edits.
|
||||
|
||||
Source: docs/performance-efficiency-plan.md Stage3 explicitly extends owner cancellation to active torrent probes and says removed cards do not retain probes. Earlier queued-only implementation intentionally left running probes eligible across final-owner disposal, a new same-source owner and cache completion.
|
||||
|
||||
Old tests/grid-health-lifecycle.test.mjs scenarios originally at367 and417 permit last-owner active work to be rejoined and ownerless success cached. New contract: final-owner disposal synchronously marks old work cancelled/unavailable; it cannot be rejoined, cache, or paint. Timer/cancel hooks are cleaned, cancelled promise settles null, and safe temporary-torrent destruction happens after event unwind before queue slot release exactly once. A fresh same-source owner requests fresh work; old callbacks/finalizer cannot delete/cache/paint over it. Cancellation does not depend on an AbortController or cooperative transport.
|
||||
|
||||
Retain strict independent controls: one leaving owner cannot cancel another live owner, active healthy/error/timeout caches retain existing TTL behavior, standalone helper callers preserve results, queue cap4/priority/FIFO and destroy-before-slot safety remain. The shared probe client and unrelated torrents must survive. Replace the two superseded scenarios with equally strict sharing-with-live-owner and obsolete-cache/fresh-request assertions, and add real grid+probe resource/settlement/identity tests. No silent deletion or relaxed assertions.
|
||||
|
||||
Scope only js/gridHealth.js ownership/inflight/cache and js/webtorrent.js probePeers optional lifecycle, plus focused tests/notes. No app, playback/service-worker engine, URL/HLS, magnet/tracker policy, concurrency/TTL/config changes. Current offline source defect does not qualify real peers/network byte release, browser decoding/playback, CPU, RangeGET or all Stage3. Full/build/browser/queue still need separate resource grant.
|
||||
+41
-34
@@ -195,20 +195,48 @@ function queueProbe(magnet, cacheKey, priority = 0, webSeeds = [], owner) {
|
||||
|
||||
if (probeInflight.has(cacheKey)) {
|
||||
const record = probeInflight.get(cacheKey);
|
||||
record.owners.add(owner);
|
||||
return record;
|
||||
if (!record.cancelled) {
|
||||
record.owners.add(owner);
|
||||
owner.release = () => releaseProbe(record, owner, cacheKey);
|
||||
return record;
|
||||
}
|
||||
}
|
||||
|
||||
const record = { owners: new Set([owner]), cancelled: false };
|
||||
owner.release = () => releaseProbe(record, owner, cacheKey);
|
||||
// Publish ownership and the result before starting IO: client.add() can
|
||||
// synchronously unregister a card or register another owner of this source.
|
||||
let resolveJob;
|
||||
let rejectJob;
|
||||
record.promise = new Promise((resolve, reject) => {
|
||||
resolveJob = resolve;
|
||||
rejectJob = reject;
|
||||
})
|
||||
.then((result) => {
|
||||
if (record.cancelled) return null;
|
||||
const normalized = normalizeResult(result);
|
||||
if (cacheKey) setCacheEntry(cacheKey, normalized);
|
||||
return normalized;
|
||||
})
|
||||
.finally(() => {
|
||||
if (cacheKey && probeInflight.get(cacheKey) === record) {
|
||||
probeInflight.delete(cacheKey);
|
||||
}
|
||||
});
|
||||
if (cacheKey) probeInflight.set(cacheKey, record);
|
||||
record.job = probeQueue.run(
|
||||
() =>
|
||||
torrentClient
|
||||
.probePeers(magnet, {
|
||||
timeoutMs: PROBE_TIMEOUT_MS,
|
||||
maxWebConns: 2,
|
||||
polls: PROBE_POLL_COUNT,
|
||||
urlList: webSeeds,
|
||||
})
|
||||
.probePeers(
|
||||
magnet,
|
||||
{
|
||||
timeoutMs: PROBE_TIMEOUT_MS,
|
||||
maxWebConns: 2,
|
||||
polls: PROBE_POLL_COUNT,
|
||||
urlList: webSeeds,
|
||||
},
|
||||
record
|
||||
)
|
||||
.catch((err) => ({
|
||||
healthy: false,
|
||||
peers: 0,
|
||||
@@ -219,26 +247,7 @@ function queueProbe(magnet, cacheKey, priority = 0, webSeeds = [], owner) {
|
||||
})),
|
||||
priority
|
||||
);
|
||||
record.promise = record.job.promise
|
||||
.then((result) => {
|
||||
if (record.cancelled) {
|
||||
return null;
|
||||
}
|
||||
const normalized = normalizeResult(result);
|
||||
if (cacheKey) {
|
||||
setCacheEntry(cacheKey, normalized);
|
||||
}
|
||||
return normalized;
|
||||
})
|
||||
.finally(() => {
|
||||
if (cacheKey && probeInflight.get(cacheKey) === record) {
|
||||
probeInflight.delete(cacheKey);
|
||||
}
|
||||
});
|
||||
|
||||
if (cacheKey) {
|
||||
probeInflight.set(cacheKey, record);
|
||||
}
|
||||
record.job.promise.then(resolveJob, rejectJob);
|
||||
|
||||
return record;
|
||||
}
|
||||
@@ -246,13 +255,13 @@ function queueProbe(magnet, cacheKey, priority = 0, webSeeds = [], owner) {
|
||||
function releaseProbe(record, owner, cacheKey) {
|
||||
owner.active = false;
|
||||
record.owners.delete(owner);
|
||||
// A running probe keeps its existing lifecycle and cache behavior. Only work
|
||||
// still waiting for a slot can be cancelled when its last card unregisters.
|
||||
if (!record.owners.size && record.job?.cancel()) {
|
||||
if (!record.owners.size && !record.cancelled) {
|
||||
record.cancelled = true;
|
||||
if (probeInflight.get(cacheKey) === record) {
|
||||
probeInflight.delete(cacheKey);
|
||||
}
|
||||
record.job?.cancel();
|
||||
record.cancelActive?.();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -524,10 +533,8 @@ function handleCardVisible({ card, pendingByCard, liveCards, priority = 0 }) {
|
||||
const webSeeds = webSeed ? [webSeed] : [];
|
||||
|
||||
const owner = { active: true };
|
||||
const record = queueProbe(magnet, infoHash, priority, webSeeds, owner);
|
||||
owner.release = () => releaseProbe(record, owner, infoHash);
|
||||
|
||||
pendingByCard.set(card, owner);
|
||||
const record = queueProbe(magnet, infoHash, priority, webSeeds, owner);
|
||||
|
||||
record.promise
|
||||
.then((result) => {
|
||||
|
||||
+55
-32
@@ -77,12 +77,15 @@ export class TorrentClient {
|
||||
|
||||
/**
|
||||
* Helper to check if a magnet link has active peers without starting a full
|
||||
* download. Used for "Stream Health" badges/indicators in the UI.
|
||||
* download. Optional lifecycle belongs to grid cards; standalone callers
|
||||
* retain their normal wire/error/timeout results.
|
||||
*/
|
||||
async probePeers(
|
||||
magnetURI,
|
||||
{ timeoutMs = 8000, maxWebConns = 2, polls = 3, urlList = [] } = {}
|
||||
{ timeoutMs = 8000, maxWebConns = 2, polls = 3, urlList = [] } = {},
|
||||
lifecycle
|
||||
) {
|
||||
if (lifecycle?.cancelled || lifecycle?.owners?.size === 0) return null;
|
||||
const magnet = typeof magnetURI === "string" ? magnetURI.trim() : "";
|
||||
if (!magnet) {
|
||||
return {
|
||||
@@ -137,6 +140,26 @@ export class TorrentClient {
|
||||
let torrent = null;
|
||||
let timeoutId = null;
|
||||
let pollId = null;
|
||||
let adding = false;
|
||||
let disposalStarted = false;
|
||||
let result = null;
|
||||
const cancelActive = () => {
|
||||
lifecycle.cancelled = true;
|
||||
finalize(null);
|
||||
};
|
||||
const settleHealthy = () => {
|
||||
const peers = Math.max(1, Math.floor(normalizeNumber(torrent?.numPeers, 1)));
|
||||
finalize({ healthy: true, peers, reason: "peer" });
|
||||
};
|
||||
const settleError = (err) => {
|
||||
const peers = Math.max(0, Math.floor(normalizeNumber(torrent?.numPeers, 0)));
|
||||
finalize({
|
||||
reason: "error",
|
||||
error: toError(err),
|
||||
peers,
|
||||
webseedOnly: peers === 0 && hasWebSeed,
|
||||
});
|
||||
};
|
||||
|
||||
// A successful probe commonly settles from WebTorrent's `wire` event.
|
||||
// Do not destroy the temporary torrent while that event is still being
|
||||
@@ -144,21 +167,23 @@ export class TorrentClient {
|
||||
// listeners return, and synchronous destruction can leave its bitfield
|
||||
// null (`recalculate(...).fill` in the bundled client). The next task
|
||||
// guarantees the callback has fully unwound before cleanup.
|
||||
const disposeProbeTorrent = (target, onDisposed = () => {}) => {
|
||||
if (!target) {
|
||||
onDisposed();
|
||||
return;
|
||||
}
|
||||
const disposeProbeTorrent = () => {
|
||||
if (adding || disposalStarted) return;
|
||||
disposalStarted = true;
|
||||
const dispose = () => {
|
||||
try {
|
||||
target.destroy({ destroyStore: true });
|
||||
torrent?.destroy({ destroyStore: true });
|
||||
} catch (err) {
|
||||
// A completed or already-destroyed probe is harmless.
|
||||
} finally {
|
||||
onDisposed();
|
||||
torrent?.removeListener("wire", settleHealthy);
|
||||
torrent?.removeListener("error", settleError);
|
||||
if (lifecycle?.cancelActive === cancelActive) lifecycle.cancelActive = null;
|
||||
resolve(lifecycle?.cancelled ? null : result);
|
||||
}
|
||||
};
|
||||
setTimeout(dispose, 0);
|
||||
if (torrent) setTimeout(dispose, 0);
|
||||
else dispose();
|
||||
};
|
||||
|
||||
const finalize = (overrides = {}) => {
|
||||
@@ -179,10 +204,10 @@ export class TorrentClient {
|
||||
? performance.now()
|
||||
: Date.now();
|
||||
|
||||
const isHealthy = Boolean(overrides.healthy);
|
||||
const peersCount = Number.isFinite(overrides.peers) ? overrides.peers : 0;
|
||||
const isHealthy = Boolean(overrides?.healthy);
|
||||
const peersCount = Number.isFinite(overrides?.peers) ? overrides.peers : 0;
|
||||
|
||||
const result = {
|
||||
result = overrides === null ? null : {
|
||||
healthy: isHealthy,
|
||||
peers: peersCount,
|
||||
reason: isHealthy ? "peer" : "timeout",
|
||||
@@ -192,13 +217,18 @@ export class TorrentClient {
|
||||
durationMs: Math.max(0, endedAt - startedAt),
|
||||
...overrides,
|
||||
};
|
||||
emit("torrent-probe-result", result);
|
||||
if (result) emit("torrent-probe-result", result);
|
||||
// Keep the grid-health queue slot occupied until the delayed teardown
|
||||
// completes. Otherwise the next probe can begin before this probe has
|
||||
// detached its tracker listeners, briefly exceeding listener limits.
|
||||
disposeProbeTorrent(torrent, () => resolve(result));
|
||||
disposeProbeTorrent();
|
||||
};
|
||||
|
||||
if (lifecycle) lifecycle.cancelActive = cancelActive;
|
||||
if (lifecycle?.cancelled || lifecycle?.owners?.size === 0) {
|
||||
cancelActive();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const addOptions = {
|
||||
announce: trackers,
|
||||
@@ -225,6 +255,7 @@ export class TorrentClient {
|
||||
if (hasExplicitWebSeed) {
|
||||
addOptions.urlList = urlList;
|
||||
}
|
||||
adding = true;
|
||||
torrent = client.add(augmentedMagnet, addOptions);
|
||||
} catch (err) {
|
||||
finalize({
|
||||
@@ -233,26 +264,18 @@ export class TorrentClient {
|
||||
peers: 0,
|
||||
webseedOnly: hasWebSeed,
|
||||
});
|
||||
} finally {
|
||||
adding = false;
|
||||
}
|
||||
// Even cancellation inside add() must absorb a late error until the
|
||||
// returned torrent can be safely destroyed on the next task.
|
||||
torrent?.on("error", settleError);
|
||||
if (settled) {
|
||||
disposeProbeTorrent();
|
||||
return;
|
||||
}
|
||||
|
||||
const settleHealthy = () => {
|
||||
const peers = Math.max(1, Math.floor(normalizeNumber(torrent?.numPeers, 1)));
|
||||
finalize({ healthy: true, peers, reason: "peer" });
|
||||
};
|
||||
|
||||
torrent.once("wire", settleHealthy);
|
||||
|
||||
torrent.once("error", (err) => {
|
||||
const peers = Math.max(0, Math.floor(normalizeNumber(torrent?.numPeers, 0)));
|
||||
finalize({
|
||||
healthy: false,
|
||||
reason: "error",
|
||||
error: toError(err),
|
||||
peers,
|
||||
webseedOnly: peers === 0 && hasWebSeed,
|
||||
});
|
||||
});
|
||||
if (settled) return;
|
||||
|
||||
if (safeTimeout > 0) {
|
||||
timeoutId = setTimeout(() => {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Client9 active torrent test integrity
|
||||
|
||||
Written before editing prior367/417 expectations. Exact authority and old/new contract: docs/spec_changes/2026-10-26-client-reliability-9-active-torrent.md. Prior failed baseline and original queued-only suite preserved in investigation evidence; production files and old assertions are in ab1ea6de parent history.
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: [new_tests, spec_correction]
|
||||
scenarios:
|
||||
- id: SCN-active-torrent-last-owner
|
||||
given: real grid observer and active probe with one card owner
|
||||
when: the final owner unregisters
|
||||
then: cancelled work settles null, clears timers/hooks and destroys temporary torrent after event unwind before slot release, without cache or stale badge writes
|
||||
- id: SCN-active-torrent-shared-and-replacement
|
||||
given: shared live owners or a same-source replacement after final-owner loss
|
||||
when: one owner leaves or old callbacks and finalizer run
|
||||
then: live shared work is preserved and cancelled identity cannot affect fresh work
|
||||
- id: SCN-active-torrent-live-standalone
|
||||
given: a live owner or standalone probe caller
|
||||
when: wire, error or normal timeout occurs
|
||||
then: existing results, cache TTL, shared probe client and bounded queue ordering remain
|
||||
observable_outcomes:
|
||||
- temporary torrent lifetime, destroy ordering/count, timers/listeners/hook, returned results
|
||||
- actual card badge and persisted cache values, new work count, FIFO progress and resource cap
|
||||
determinism_controls:
|
||||
- fake WebTorrent IO, controlled clock and observer delivery, map-backed persistence IO
|
||||
- isolated mutant copies, no public fetch/WebSocket, no sleeps or retries
|
||||
anti_cheat_rationale:
|
||||
prevents:
|
||||
- cached obsolete results or stale identity mutation
|
||||
- cancelling a still shared live owner
|
||||
- immediate unsafe destroy or premature queue-slot release
|
||||
- mocked probe implementation bypassing real resource cleanup
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ''
|
||||
```
|
||||
|
||||
Contract correction changes outcomes, not assertion strength: original last-owner-gap reuse/cache expectations become fresh-work/no-obsolete-cache/resource assertions, while separate live-owner/shared/cache/timeout controls remain. Mutant mappings and exact validation results will be recorded before handoff; no passing or mutant-kill counts claimed yet.
|
||||
|
||||
Final focused27new+11corrected+22adjacent=60/60;19unique mutants all killed,20variant executions with added extended run; all27newscenarios covered. Full per-scenario mappings and failures: state/evidence/client9-torrent-impl-20261026/new-scenario-mutant-map.json and mutants/coverage.json. No mutants survived; all mutated suite runs cancelled0, natural failure exits. Restored state is automatic because only isolated copies were changed. Full/build/browser/landing pending separately.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Client9 focused implementation validation (in progress)
|
||||
|
||||
- Explicit dispatch17:38 routing read before implementation; baseline independently reproduced, no repeat. Spec change + integrity note written before old367/417 expectation edits.
|
||||
- `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node tests/grid-health-active-torrent.test.mjs > /tmp/client9-torrent-focused-20261026-1.log 2>&1`: naturalexit0,24/24,0fail/0cancelled (~355ms).
|
||||
- `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node tests/grid-health-lifecycle.test.mjs > /tmp/client9-grid-corrected-20261026-1.log 2>&1`: naturalexit0,11/11,0fail/0cancelled (~263ms); explicit active sharing/cache spec correction only, live/cache controls retained.
|
||||
- `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node scripts/run-targeted-tests.mjs tests/grid-health-active-torrent.test.mjs tests/grid-health-lifecycle.test.mjs tests/grid-health.test.mjs tests/webtorrent-regression.test.mjs tests/webtorrent-handlers.test.mjs tests/minimal-webtorrent.test.mjs > /tmp/client9-torrent-targeted-20261026-2.log 2>&1`: naturalexit0,6files/58scenarios=25+11+6+3+12+1 pass,0fail/0cancelled. Includes added late error after cancellation inside add() and add-throw/shared-client reuse. No broad runner or browser involved.
|
||||
- Later small test additions strengthen queued prune helper invocation and bound reentrant add callback in fixture; final focused rerun pending. Mutants and targeted lint pending. No full/build/browser/submission/landing or actual peers/network/CPU/wholeStage3 qualification.
|
||||
|
||||
## Restart handoff after focused boundary
|
||||
|
||||
- Final targeted command (same6files as above) output `/tmp/client9-torrent-targeted-20261026-final.log`: naturalexit0,60/60=27+11+6+3+12+1,0fail/0cancelled. Added strict queued-helper prune assertion, missing AbortController cancellation and error+throwing teardown queued progress. No tests weakened. Product delta remains2routed files, old suite only2explicit spec corrections.
|
||||
- Mutants: `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD python3 /tmp/client9-torrent-mutants-20261026.py > /tmp/client9-torrent-mutants-20261026.log 2>&1`: naturalexit0 harness;19/19unique mutations killed by actual assertion failures, no survivors; each mutated new/old suite run exits naturally and reports cancelled0. Initial25new scenarios all covered. Additional isolated skip-destroy run with final27scenario file: naturalexit1,25assertion failures/0cancelled, including both added cases. Total19unique mutants/20variant executions; all27newscenarios have a recorded killing variant. No worktree mutation/restoration required: copies under/tmp only. Product formatting after initial mutation run changed no behavior; final focused60 covers current source.
|
||||
- Targeted lint: `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD npm run lint:file-size`, `lint:innerhtml`, `lint:pool-access` each exit0 (separate logs). Same `lint:hex` first exits2 because spawnSync git EPERM; unchanged host-escalated command approved and exits0. Both results retained. No full npm run lint/all11 claim; remaining checks and unchanged full/build/browser/normal gate require later resource grant.
|
||||
- Current upstream unstable04c8c7cf is4kit/setup commits ahead; read its newest2026-10-26 RULE-CHANGES via git show. Runtime js/tests/config/package inputs diff0 versus current committedHEAD; our workingtree changes remain local and uncommitted. No merge before this requested restart. Updated rules make swap occupancy advisory with ampleRAM/no swap-in; old blanket holds are superseded, but no heavy grant inferred.
|
||||
- Durable copy: `/home/user/.local/state/agent-fleet/bitvid/evidence/client9-torrent-impl-20261026/` contains final4candidate files, original+finalfocused outputs, all targeted lint outputs incl EPERM+host pass, mutant runner/19variant copies+logs/summary, extended variant, per-new-scenario mutant map and manifest.sha256. Existing failure baselines and earlier evidence commits retained. No new commit/submission/landing; HEAD remainsab1ea6dee1da7eebd2b5c08f3d5935dfbfb045d0.
|
||||
- STOP/SAFE TO RESTART per interpreter185753/190045 mails. Next resumed step: read live rules/mailbox/task and saved snapshots; inspect final2-file delta and tests, finish decision/integrity/READY notes, then commit named paths usingfleet tool and merge currentunstable (neverrebase). Focused60 need not repeat unless source changes; merged-tree/runtime equality determines remainingcheck need. Explicit resource grant still required before full/build/browser/submission. Real peers/browser/CPU/wholeStage3 unverified. No process stops, discard/stash/reset or kit work by client.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Client9 resumed candidate review
|
||||
|
||||
- Restart HEAD `ab1ea6dee1da7eebd2b5c08f3d5935dfbfb045d0`; unstable `04c8c7cf61eed29424f561297ef49c6cdfa91a0a`. Reclaimed existing item9; mailbox empty. Read local rules and newer unstable rules (2026-10-26) plus saved dispatch torrent routing. No repeated baseline or new scope.
|
||||
- Review: final-owner synchronous cancellation/inflight removal, optional probePeers hook, prompt timer cleanup and deferred temporary-torrent destruction before slot release; shared client/owners and identity guards preserved. Product files only gridHealth.js and webtorrent.js; two explicitly authorized old expectations corrected. No product edits this turn.
|
||||
- `sha256sum` of four candidate source/test files matches durable manifest exactly. Existing six-file focused evidence remains 60/60, fail0/cancelled0; 19 unique killed mutants, 20 variant executions/all27 new scenarios covered. This is preserved prior-run evidence, not a new test run.
|
||||
- `git diff --check`: exit0. `git diff ab1ea6de unstable -- js tests config package.json package-lock.json`: exit0, empty. Upstream kit changes do not change those validation inputs. No duplicate focused run justified.
|
||||
- Fresh MCP health19:07:29UTC: TCPU97.1C exceeds85C threshold; load11.58, availableRAM64.2%, swapInPages0. Heavy work held; no test/build/browser/submission launched. Hold owner dispatch; next allowed light work candidate commit/merge/runtime comparison. Need fresh resource clearance and explicit slot.
|
||||
- Remaining full lint, merged-tree normal full gate, build/browser qualification and durable landing/ancestry. No live peer/network teardown, playback, CPU improvement or whole Stage3 qualification.
|
||||
@@ -0,0 +1,435 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import './test-helpers/setup-localstorage.mjs';
|
||||
const root = new URL('../', import.meta.url).href;
|
||||
const { torrentClient } = await import(`${root}js/webtorrent.js`);
|
||||
const { attachHealthBadges } = await import(`${root}js/gridHealth.js`);
|
||||
const { readTorrentProbeFromStorage } = await import(`${root}js/utils/storage.js`);
|
||||
const { getCardLivenessPolicy, setCardLivenessPolicy } = await import(`${root}js/constants.js`);
|
||||
const turn = () => new Promise(resolve => setImmediate(resolve));
|
||||
let nextHash = 100000;
|
||||
const magnet = () => `magnet:?xt=urn:btih:${(nextHash++).toString(16).padStart(40, '0')}`;
|
||||
|
||||
class Clock {
|
||||
now = 0; next = 1; jobs = new Map();
|
||||
add(fn, ms, interval = false) {
|
||||
const id = this.next++;
|
||||
this.jobs.set(id, { fn, ms, at: this.now + ms, interval });
|
||||
return id;
|
||||
}
|
||||
tick(ms) {
|
||||
const end = this.now + ms;
|
||||
let steps = 0;
|
||||
while (true) {
|
||||
const entry = [...this.jobs].filter(([, job]) => job.at <= end)
|
||||
.sort((a, b) => a[1].at - b[1].at || a[0] - b[0])[0];
|
||||
if (!entry) break;
|
||||
assert.ok(++steps < 1000, 'fake clock must remain bounded');
|
||||
const [id, job] = entry;
|
||||
this.now = job.at;
|
||||
if (job.interval) job.at += job.ms;
|
||||
else this.jobs.delete(id);
|
||||
job.fn();
|
||||
}
|
||||
this.now = end;
|
||||
}
|
||||
counts() {
|
||||
const all = [...this.jobs.values()];
|
||||
return { timeout: all.filter(x => !x.interval && x.ms > 0).length,
|
||||
poll: all.filter(x => x.interval).length,
|
||||
teardown: all.filter(x => !x.interval && x.ms === 0).length };
|
||||
}
|
||||
}
|
||||
|
||||
function fixture(t) {
|
||||
const dom = new JSDOM('<!doctype html><body></body>', { url: 'https://example.invalid' });
|
||||
Object.defineProperty(dom.window, 'innerHeight', { value: 20 });
|
||||
const names = ['window', 'document', 'HTMLElement', 'localStorage', 'IntersectionObserver',
|
||||
'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'fetch', 'WebSocket', 'AbortController'];
|
||||
const saved = new Map(names.map(key => [key, Object.getOwnPropertyDescriptor(globalThis, key)]));
|
||||
const previousPolicy = getCardLivenessPolicy();
|
||||
const clock = new Clock();
|
||||
const config = {};
|
||||
// Isolate persistence IO: JSDOM StorageImpl schedules unrelated storage-event timers.
|
||||
// Production read/writeTorrentProbeToStorage still executes against this map.
|
||||
const store = new Map();
|
||||
const writes = [];
|
||||
const memoryStorage = { getItem: key => store.get(key) ?? null,
|
||||
setItem: (key, value) => { writes.push([key, value]); store.set(String(key), String(value)); },
|
||||
removeItem: key => store.delete(key), clear: () => store.clear() };
|
||||
const observers = [], torrents = [], calls = [], disposals = [], updates = [], timeline = [];
|
||||
let networkAttempts = 0, peak = 0;
|
||||
class Observer {
|
||||
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, index) => ({ target, isIntersecting: true,
|
||||
intersectionRatio: 1, intersectionRect: { top: index * 20, width: 100, height: 20 } })));
|
||||
}
|
||||
}
|
||||
class FakeTorrent extends EventEmitter {
|
||||
constructor(source, options) {
|
||||
super(); this.source = source; this.options = options; this.numPeers = 0;
|
||||
this.destroyed = false; this.destroyCount = 0; this.emitting = false;
|
||||
}
|
||||
on(event, listener) {
|
||||
super.on(event, listener);
|
||||
if (config.syncEvent === event && !this.syncEmitted) {
|
||||
this.syncEmitted = true;
|
||||
if (event === 'wire') this.wire(); else this.error();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
destroy(options) {
|
||||
assert.deepEqual(options, { destroyStore: true });
|
||||
this.destroyedDuringEvent = this.emitting;
|
||||
this.destroyedDuringAdd = config.adding === true;
|
||||
this.destroyed = true; this.destroyCount++;
|
||||
if (config.throwDestroy) throw new Error('controlled destroy exception');
|
||||
this.removeAllListeners();
|
||||
timeline.push(['destroy', this.source, clock.now]);
|
||||
}
|
||||
wire() { this.numPeers = 1; this.emitting = true;
|
||||
try { this.emit('wire', {}); } finally { this.emitting = false; } }
|
||||
error() { this.emit('error', new Error('controlled fake torrent error')); }
|
||||
}
|
||||
class FakeClient {
|
||||
constructor() { config.onConstruct?.(); }
|
||||
setMaxListeners() {}
|
||||
destroy() { assert.fail('shared probe client must never be destroyed'); }
|
||||
add(source, options) {
|
||||
if (config.throwAdd) throw new Error('controlled add exception');
|
||||
const torrent = new FakeTorrent(source, options);
|
||||
torrents.push(torrent);
|
||||
peak = Math.max(peak, torrents.filter(x => !x.destroyed).length);
|
||||
timeline.push(['add', source, clock.now]);
|
||||
config.adding = true;
|
||||
try { config.onAdd?.(torrent); } finally { config.adding = false; }
|
||||
return torrent;
|
||||
}
|
||||
}
|
||||
Object.assign(globalThis, { window: dom.window, document: dom.window.document,
|
||||
HTMLElement: dom.window.HTMLElement, localStorage: memoryStorage,
|
||||
IntersectionObserver: Observer,
|
||||
setTimeout: (fn, ms = 0) => clock.add(fn, ms), clearTimeout: id => clock.jobs.delete(id),
|
||||
setInterval: (fn, ms) => clock.add(fn, ms, true), clearInterval: id => clock.jobs.delete(id),
|
||||
fetch: () => { networkAttempts++; throw new Error('network forbidden'); },
|
||||
WebSocket: class { constructor() { networkAttempts++; throw new Error('network forbidden'); } }
|
||||
});
|
||||
const savedClient = { probeClient: torrentClient.probeClient, WebTorrentClass: torrentClient.WebTorrentClass,
|
||||
probePeers: torrentClient.probePeers };
|
||||
torrentClient.probeClient = null;
|
||||
torrentClient.WebTorrentClass = FakeClient;
|
||||
// Transparent measurement wrapper: execute the actual production method, do not stub its logic.
|
||||
torrentClient.probePeers = function(source, options, lifecycle) {
|
||||
const call = { source, options, lifecycle, settled: false, result: undefined };
|
||||
calls.push(call);
|
||||
return savedClient.probePeers.call(this, source, options, lifecycle).then(result => {
|
||||
call.settled = true; call.result = result; return result;
|
||||
});
|
||||
};
|
||||
setCardLivenessPolicy('show-pending');
|
||||
function feed(sources) {
|
||||
const container = document.createElement('section');
|
||||
const cards = sources.map(source => {
|
||||
const card = document.createElement('article');
|
||||
card.className = 'card'; card.dataset.videoId = source; card.dataset.magnet = source;
|
||||
const badge = document.createElement('span'); badge.className = 'torrent-health-badge';
|
||||
card.append(badge); container.append(card); return card;
|
||||
});
|
||||
document.body.append(container);
|
||||
const events = [];
|
||||
const dispose = attachHealthBadges(container, { onUpdate: event => { events.push(event); updates.push(event); } });
|
||||
disposals.push(dispose);
|
||||
return { container, cards, dispose, events, observer: observers.at(-1),
|
||||
show() { this.observer.show(); } };
|
||||
}
|
||||
const cache = source => readTorrentProbeFromStorage(source.match(/btih:([a-f0-9]{40})/)[1]);
|
||||
const snapshot = source => ({ started: torrents.length, activeTorrents: torrents.filter(x => !x.destroyed).length,
|
||||
destroyed: torrents.filter(x => x.destroyed).length,
|
||||
listeners: torrents.reduce((n, t) => n + t.eventNames().reduce((m, e) => m + t.listenerCount(e), 0), 0),
|
||||
unsettled: calls.filter(x => !x.settled).length, timers: clock.counts(),
|
||||
cache: source ? cache(source) : undefined, peak, networkAttempts });
|
||||
async function drain() { await turn(); clock.tick(0); await turn(); }
|
||||
async function cleanup() {
|
||||
disposals.forEach(dispose => dispose());
|
||||
for (let i = 0; i < 10 && (calls.some(x => !x.settled) || clock.jobs.size); i++) {
|
||||
torrents.filter(x => !x.destroyed).forEach(t => t.wire());
|
||||
await drain();
|
||||
}
|
||||
const final = snapshot();
|
||||
|
||||
try {
|
||||
assert.equal(final.activeTorrents, 0); assert.equal(final.unsettled, 0);
|
||||
assert.equal(final.listeners, 0); assert.equal(clock.jobs.size, 0);
|
||||
assert.equal(networkAttempts, 0);
|
||||
assert.ok(torrents.every(x => x.destroyCount === 1), 'destroy exactly once');
|
||||
assert.ok(torrents.every(x => !x.destroyedDuringEvent && !x.destroyedDuringAdd), 'destroy after event/add unwind');
|
||||
assert.ok(calls.every(x => !x.lifecycle?.cancelActive), 'cancel hook removed');
|
||||
} finally {
|
||||
Object.assign(torrentClient, savedClient);
|
||||
setCardLivenessPolicy(previousPolicy);
|
||||
dom.window.close();
|
||||
for (const [key, desc] of saved) {
|
||||
if (desc) Object.defineProperty(globalThis, key, desc); else delete globalThis[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
t.after(cleanup);
|
||||
return { feed, cache, snapshot, drain, clock, torrents, calls, timeline, config, writes,
|
||||
probe: (source, lifecycle) => torrentClient.probePeers(source, { timeoutMs: 20000 }, lifecycle),
|
||||
get peak() { return peak; } };
|
||||
}
|
||||
|
||||
const cases = [
|
||||
['final-owner-cleanup', async f => {
|
||||
const source = magnet(), feed = f.feed([source]); feed.show();
|
||||
assert.equal(f.torrents.length, 1);
|
||||
feed.dispose(); await f.drain();
|
||||
assert.equal(f.snapshot().activeTorrents, 0, 'last owner must not retain an active torrent');
|
||||
assert.equal(f.snapshot().unsettled, 0, 'last owner must settle without original timeout');
|
||||
assert.equal(f.calls[0].result, null);
|
||||
assert.equal(f.calls[0].lifecycle.cancelActive, null);
|
||||
assert.equal(f.clock.jobs.size, 0);
|
||||
assert.equal(f.cache(source), null);
|
||||
}],
|
||||
['disposed-late-success-no-cache', async f => {
|
||||
const source = magnet(), feed = f.feed([source]); feed.show(); feed.dispose();
|
||||
f.torrents[0].wire(); await f.drain();
|
||||
assert.equal(feed.cards[0].dataset.streamHealthState, 'checking', 'stale badge guard retained');
|
||||
assert.equal(f.cache(source), null, 'obsolete active completion must not populate persistent cache');
|
||||
}],
|
||||
['same-turn-fresh-rejoin', async f => {
|
||||
const source = magnet(), old = f.feed([source]); old.show(); old.dispose();
|
||||
const replacement = f.feed([source]); replacement.show();
|
||||
await f.drain();
|
||||
assert.equal(f.calls.length, 2, 'new same-turn owner must start a fresh probe');
|
||||
}],
|
||||
['later-fresh-rejoin', async f => {
|
||||
const source = magnet(), old = f.feed([source]); old.show(); old.dispose(); await f.drain();
|
||||
const replacement = f.feed([source]); replacement.show(); await f.drain();
|
||||
assert.equal(f.calls.length, 2, 'new later owner must start a fresh probe');
|
||||
}],
|
||||
['disposal-releases-fifo-capacity', async f => {
|
||||
const active = Array.from({ length: 4 }, () => f.feed([magnet()])); active.forEach(x => x.show());
|
||||
const queued = f.feed([magnet()]); queued.show(); assert.equal(f.calls.length, 4);
|
||||
active[0].dispose(); await f.drain();
|
||||
assert.equal(f.calls.length, 5, 'queued live owner must gain released obsolete active capacity');
|
||||
assert.ok(f.peak <= 4);
|
||||
}],
|
||||
['shared-live-owner', async f => {
|
||||
const source = magnet(), old = f.feed([source]), live = f.feed([source]); old.show(); live.show();
|
||||
assert.equal(f.calls.length, 1); old.dispose(); await f.drain();
|
||||
assert.equal(f.snapshot().activeTorrents, 1); assert.equal(f.snapshot().timers.timeout, 1);
|
||||
f.torrents[0].wire(); await f.drain();
|
||||
assert.equal(live.cards[0].dataset.streamHealthState, 'healthy');
|
||||
assert.equal(old.cards[0].dataset.streamHealthState, 'checking'); assert.ok(f.cache(source));
|
||||
}],
|
||||
['live-wire-cache', async f => {
|
||||
const source = magnet(), live = f.feed([source]); live.show(); f.torrents[0].wire(); await f.drain();
|
||||
assert.equal(live.cards[0].dataset.streamHealthState, 'healthy'); assert.ok(f.cache(source));
|
||||
const cached = f.feed([source]); cached.show(); assert.equal(f.calls.length, 1);
|
||||
assert.equal(cached.cards[0].dataset.streamHealthState, 'healthy');
|
||||
}],
|
||||
['live-error-cleanup-cache', async f => {
|
||||
const source = magnet(), live = f.feed([source]); live.show(); f.torrents[0].error(); await f.drain();
|
||||
assert.equal(live.cards[0].dataset.streamHealthState, 'unhealthy');
|
||||
assert.equal(f.calls[0].result.reason, 'error'); assert.ok(f.cache(source));
|
||||
assert.equal(f.snapshot().activeTorrents, 0); assert.equal(f.clock.jobs.size, 0);
|
||||
}],
|
||||
['live-timeout-cleanup-cache', async f => {
|
||||
const source = magnet(), live = f.feed([source]); live.show();
|
||||
f.clock.tick(19999); await turn(); assert.equal(f.calls[0].settled, false);
|
||||
f.clock.tick(1); await turn();
|
||||
assert.equal(live.cards[0].dataset.streamHealthState, 'unhealthy');
|
||||
assert.equal(f.calls[0].result.reason, 'timeout'); assert.ok(f.cache(source));
|
||||
assert.equal(f.snapshot().activeTorrents, 0); assert.equal(f.clock.jobs.size, 0);
|
||||
}],
|
||||
['queued-cancel-priority-fifo', async f => {
|
||||
const active = f.feed(Array.from({ length: 4 }, magnet)); active.show();
|
||||
const firstSource = magnet(), lowSource = magnet(), equalSource = magnet(), cancelledSource = magnet();
|
||||
const batch = f.feed([firstSource, lowSource]); batch.show();
|
||||
const equal = f.feed([equalSource]); equal.show();
|
||||
const cancelled = f.feed([cancelledSource]); cancelled.show(); cancelled.dispose();
|
||||
for (const [slot, expected] of [[0, firstSource], [4, equalSource], [5, lowSource]]) {
|
||||
f.torrents[slot].wire(); await f.drain();
|
||||
assert.ok(f.torrents.at(-1).source.includes(expected.slice(-40)));
|
||||
}
|
||||
assert.equal(f.torrents.some(x => x.source.includes(cancelledSource.slice(-40))), false);
|
||||
assert.equal(f.calls.some(x => x.source === cancelledSource), false, 'pruned queued job never invokes helper');
|
||||
assert.ok(f.peak <= 4); assert.equal(f.cache(cancelledSource), null);
|
||||
}],
|
||||
['deferred-teardown-precedes-next-slot', async f => {
|
||||
const active = f.feed(Array.from({ length: 4 }, magnet)); active.show();
|
||||
const queued = f.feed([magnet()]); queued.show();
|
||||
f.torrents[0].wire(); await turn();
|
||||
assert.equal(f.calls.length, 4, 'slot retained until delayed destroy');
|
||||
assert.equal(f.snapshot().timers.teardown, 1);
|
||||
f.clock.tick(0); await turn(); assert.equal(f.calls.length, 5); assert.equal(f.peak, 4);
|
||||
const destroyIndex = f.timeline.findIndex(x => x[0] === 'destroy');
|
||||
assert.equal(f.timeline[destroyIndex + 1][0], 'add');
|
||||
}],
|
||||
['late-observer-disposed-guard', async f => {
|
||||
const old = f.feed([magnet()]); old.show(); old.dispose();
|
||||
const before = old.events.length; old.observer.show(); f.torrents[0].wire(); await f.drain();
|
||||
assert.equal(old.events.length, before); assert.equal(f.calls.length, 1);
|
||||
assert.equal(old.cards[0].dataset.streamHealthState, 'checking');
|
||||
}],
|
||||
['old-callbacks-cannot-affect-replacement', async f => {
|
||||
const source = magnet(), old = f.feed([source]); old.show();
|
||||
const wire = f.torrents[0].listeners('wire')[0];
|
||||
const error = f.torrents[0].listeners('error')[0];
|
||||
const timers = [...f.clock.jobs.values()].map(job => job.fn);
|
||||
old.dispose();
|
||||
assert.equal(f.calls[0].lifecycle.cancelled, true, 'cancelled identity unavailable synchronously');
|
||||
const replacement = f.feed([source]); replacement.show();
|
||||
await f.drain();
|
||||
wire(); error(new Error('late')); timers.forEach(fn => fn()); await f.drain();
|
||||
const shared = f.feed([source]); shared.show();
|
||||
assert.equal(f.calls.length, 2, 'old finalizer must not delete replacement identity');
|
||||
assert.equal(f.cache(source), null);
|
||||
assert.equal(replacement.cards[0].dataset.streamHealthState, 'checking');
|
||||
f.torrents[1].wire(); await f.drain();
|
||||
assert.equal(f.writes.length, 1, 'only the new live result is cached');
|
||||
assert.equal(replacement.cards[0].dataset.streamHealthState, 'healthy');
|
||||
assert.equal(shared.cards[0].dataset.streamHealthState, 'healthy');
|
||||
assert.equal(old.cards[0].dataset.streamHealthState, 'checking');
|
||||
}],
|
||||
['repeated-release-and-noncooperative-io', async f => {
|
||||
const live = f.feed([magnet()]); live.show();
|
||||
const cancel = f.calls[0].lifecycle.cancelActive;
|
||||
const lateWire = f.torrents[0].listeners('wire')[0];
|
||||
live.dispose(); live.dispose(); cancel(); cancel();
|
||||
// No fake transport completion or abort event is required to settle.
|
||||
await f.drain();
|
||||
assert.equal(f.calls[0].settled, true); assert.equal(f.calls[0].result, null);
|
||||
assert.equal(f.torrents[0].destroyCount, 1);
|
||||
lateWire(); cancel(); await f.drain();
|
||||
assert.equal(f.calls[0].result, null); assert.equal(f.torrents[0].destroyCount, 1);
|
||||
}],
|
||||
['cancel-before-probe-start', async f => {
|
||||
const lifecycle = { cancelled: true, owners: new Set() };
|
||||
const promise = f.probe(magnet(), lifecycle); await f.drain();
|
||||
assert.equal(f.calls[0].settled, true); assert.equal(await promise, null);
|
||||
assert.equal(f.torrents.length, 0); assert.equal(torrentClient.probeClient, null);
|
||||
assert.equal(f.clock.jobs.size, 0);
|
||||
}],
|
||||
['cancel-during-client-construction', async f => {
|
||||
const live = f.feed([magnet()]);
|
||||
f.config.onConstruct = () => live.dispose(); live.show(); await f.drain();
|
||||
assert.equal(f.calls[0].settled, true); assert.equal(f.calls[0].result, null);
|
||||
assert.equal(f.torrents.length, 0); assert.equal(f.clock.jobs.size, 0);
|
||||
assert.equal(f.calls[0].lifecycle.cancelActive, null);
|
||||
}],
|
||||
['cancel-during-add-defers-destroy', async f => {
|
||||
const source = magnet(), live = f.feed([source]);
|
||||
f.config.onAdd = () => live.dispose(); live.show();
|
||||
assert.equal(f.calls[0].lifecycle.cancelled, true);
|
||||
assert.equal(f.calls[0].settled, false);
|
||||
assert.equal(f.torrents[0].destroyCount, 0);
|
||||
assert.equal(f.clock.counts().timeout, 0); assert.equal(f.clock.counts().poll, 0);
|
||||
f.torrents[0].error(); // Noncooperative IO can report an error before deferred destruction.
|
||||
await f.drain();
|
||||
assert.equal(f.calls[0].settled, true); assert.equal(f.calls[0].result, null);
|
||||
assert.equal(f.torrents[0].destroyCount, 1);
|
||||
assert.equal(f.torrents[0].destroyedDuringAdd, false); assert.equal(f.cache(source), null);
|
||||
}],
|
||||
['same-source-owner-registers-during-add', async f => {
|
||||
const source = magnet(), first = f.feed([source]);
|
||||
let shared;
|
||||
f.config.onAdd = () => {
|
||||
f.config.onAdd = null;
|
||||
shared = f.feed([source]); shared.show(); first.dispose();
|
||||
};
|
||||
first.show(); assert.equal(f.calls.length, 1);
|
||||
assert.equal(f.calls[0].lifecycle.cancelled, false);
|
||||
f.torrents[0].wire(); await f.drain();
|
||||
assert.equal(shared.cards[0].dataset.streamHealthState, 'healthy');
|
||||
assert.equal(first.cards[0].dataset.streamHealthState, 'checking');
|
||||
}],
|
||||
['cancel-on-wire-unwind-before-destroy', async f => {
|
||||
const live = f.feed([magnet()]); live.show();
|
||||
f.torrents[0].on('wire', () => live.dispose());
|
||||
f.torrents[0].wire();
|
||||
assert.equal(f.torrents[0].destroyCount, 0);
|
||||
await f.drain();
|
||||
assert.equal(f.calls[0].settled, true); assert.equal(f.calls[0].result, null);
|
||||
assert.equal(f.torrents[0].destroyedDuringEvent, false); assert.equal(f.writes.length, 0);
|
||||
}],
|
||||
['cancel-on-error-unwind-before-destroy', async f => {
|
||||
const live = f.feed([magnet()]); live.show();
|
||||
f.torrents[0].on('error', () => live.dispose());
|
||||
f.torrents[0].error();
|
||||
assert.equal(f.torrents[0].destroyCount, 0);
|
||||
await f.drain();
|
||||
assert.equal(f.calls[0].result, null); assert.equal(f.writes.length, 0);
|
||||
assert.equal(f.torrents[0].destroyedDuringEvent, false);
|
||||
}],
|
||||
['throwing-destroy-still-settles-and-cleans-own-listeners', async f => {
|
||||
const live = f.feed([magnet()]); live.show();
|
||||
f.config.throwDestroy = true; live.dispose(); await f.drain();
|
||||
assert.equal(f.calls[0].settled, true); assert.equal(f.calls[0].result, null);
|
||||
assert.equal(f.torrents[0].destroyCount, 1);
|
||||
assert.equal(f.torrents[0].listenerCount('wire'), 0);
|
||||
assert.equal(f.torrents[0].listenerCount('error'), 0);
|
||||
assert.equal(f.calls[0].lifecycle.cancelActive, null);
|
||||
}],
|
||||
['standalone-ownerless-caller-stays-live', async f => {
|
||||
const source = magnet(), promise = f.probe(source);
|
||||
assert.equal(f.calls[0].lifecycle, undefined);
|
||||
f.torrents[0].wire(); await f.drain(); assert.equal(f.calls[0].settled, true);
|
||||
const result = await promise;
|
||||
assert.equal(result.healthy, true); assert.equal(result.reason, 'peer');
|
||||
assert.equal(f.torrents[0].destroyCount, 1);
|
||||
}],
|
||||
['synchronous-wire-listener-registration', async f => {
|
||||
f.config.syncEvent = 'wire'; const promise = f.probe(magnet());
|
||||
assert.equal(f.torrents[0].destroyCount, 0);
|
||||
await f.drain(); assert.equal(f.calls[0].settled, true);
|
||||
assert.equal((await promise).healthy, true); assert.equal(f.clock.jobs.size, 0);
|
||||
}],
|
||||
['synchronous-error-listener-registration', async f => {
|
||||
f.config.syncEvent = 'error'; const promise = f.probe(magnet());
|
||||
await f.drain(); assert.equal(f.calls[0].settled, true);
|
||||
assert.equal((await promise).reason, 'error'); assert.equal(f.clock.jobs.size, 0);
|
||||
}],
|
||||
['add-error-cleans-hook-and-keeps-shared-client', async f => {
|
||||
const lifecycle = { cancelled: false, owners: new Set([{}]) };
|
||||
f.config.throwAdd = true; const promise = f.probe(magnet(), lifecycle);
|
||||
await f.drain(); assert.equal(f.calls[0].settled, true);
|
||||
assert.equal((await promise).reason, 'error'); assert.equal(lifecycle.cancelActive, null);
|
||||
assert.equal(f.torrents.length, 0); assert.equal(f.clock.jobs.size, 0);
|
||||
const client = torrentClient.probeClient;
|
||||
f.config.throwAdd = false; const next = f.probe(magnet());
|
||||
f.torrents[0].wire(); await f.drain(); assert.equal(f.calls[1].settled, true);
|
||||
assert.equal((await next).healthy, true); assert.equal(torrentClient.probeClient, client);
|
||||
}],
|
||||
['missing-AbortController-still-cancels', async f => {
|
||||
globalThis.AbortController = undefined;
|
||||
const source = magnet(), live = f.feed([source]); live.show(); live.dispose();
|
||||
await f.drain(); assert.equal(f.calls[0].settled, true);
|
||||
assert.equal(f.calls[0].result, null); assert.equal(f.cache(source), null);
|
||||
assert.equal(f.clock.jobs.size, 0); assert.equal(f.torrents[0].destroyCount, 1);
|
||||
}],
|
||||
['error-and-throwing-teardown-release-queued-capacity-once', async f => {
|
||||
const active = f.feed(Array.from({ length: 4 }, magnet)); active.show();
|
||||
const source = magnet(), queued = f.feed([source]); queued.show();
|
||||
f.config.throwDestroy = true;
|
||||
f.torrents[0].error(); await turn();
|
||||
assert.equal(f.calls.length, 4, 'error result still holds slot until teardown');
|
||||
f.clock.tick(0); await turn();
|
||||
assert.equal(f.calls.length, 5); assert.equal(f.torrents[0].destroyCount, 1);
|
||||
assert.equal(f.torrents[0].listenerCount('error'), 0);
|
||||
f.config.throwDestroy = false;
|
||||
f.torrents[4].wire(); await f.drain();
|
||||
assert.equal(queued.cards[0].dataset.streamHealthState, 'healthy'); assert.equal(f.peak, 4);
|
||||
}]
|
||||
];
|
||||
|
||||
for (const [name, scenario] of cases) {
|
||||
test(name, async t => { await scenario(fixture(t)); });
|
||||
}
|
||||
@@ -369,13 +369,13 @@ test("active probe remains shared while disposed owners and late observer entrie
|
||||
const source = magnet();
|
||||
const first = f.feed([source]);
|
||||
first.observer.show();
|
||||
first.dispose();
|
||||
const liveUpdates = [];
|
||||
const live = f.feed([source], {
|
||||
onUpdate: (event) => liveUpdates.push(event)
|
||||
});
|
||||
live.observer.show(); // Retain a live owner BEFORE releasing the first.
|
||||
first.dispose();
|
||||
first.observer.show(); // An already-delivered entry may arrive after disconnect.
|
||||
live.observer.show();
|
||||
assert.equal(
|
||||
liveUpdates.some((event) => event.card === first.cards[0]),
|
||||
false,
|
||||
@@ -414,7 +414,7 @@ test("repeated old disposal cannot cancel a reattached live registration", async
|
||||
assert.equal(renewed.cards[0].dataset.streamHealthState, "healthy");
|
||||
});
|
||||
|
||||
test("disposed active completion remains cached for a later live owner", async (t) => {
|
||||
test("disposed active completion cannot cache and a later owner requests fresh work", async (t) => {
|
||||
const f = fixture(t);
|
||||
const source = magnet();
|
||||
const old = f.feed([source]);
|
||||
@@ -427,10 +427,15 @@ test("disposed active completion remains cached for a later live owner", async (
|
||||
live.observer.show();
|
||||
assert.equal(
|
||||
f.calls.length,
|
||||
1,
|
||||
"ownerless active completion must retain its healthy cache result"
|
||||
2,
|
||||
"ownerless active completion must not replace a fresh probe"
|
||||
);
|
||||
assert.equal(readTorrentProbeFromStorage(source.slice(-40)), null);
|
||||
assert.equal(live.cards[0].dataset.streamHealthState, "checking");
|
||||
f.calls[1].finish();
|
||||
await flush();
|
||||
assert.equal(live.cards[0].dataset.streamHealthState, "healthy");
|
||||
assert.equal(readTorrentProbeFromStorage(source.slice(-40)).value.healthy, true);
|
||||
});
|
||||
|
||||
test("completed healthy and negative probes retain existing cache TTL behavior", async (t) => {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
- [x] Read exact dispatch routing and newest rules/current tree.
|
||||
- [x] Write explicit spec correction and integrity note before superseded expectations.
|
||||
- [ ] Implement narrow grid ownership and optional helper lifecycle.
|
||||
- [ ] Focused real-chain/helper tests, edge callbacks, shared/live/legacy/queue controls.
|
||||
- [ ] Every new scenario rejects a deliberate restored mutant.
|
||||
- [ ] Targeted lint and evidence/READY handoff.
|
||||
- [ ] Later full/build/browser/normal landing under separate resource grant.
|
||||
@@ -0,0 +1,6 @@
|
||||
- [x] Resume rules/mailbox/item9 claim and saved candidate review.
|
||||
- [x] Four candidate hashes exactly match saved focused/mutant evidence; no repeat baseline/tests.
|
||||
- [x] Record thermal hold and READY scope/validation limits.
|
||||
- [ ] Commit named files via fleet; merge unstable without rebase; inspect runtime equality.
|
||||
- [ ] Dispatch fresh resource grant for remaining lint/full/build/browser/submission.
|
||||
- [ ] Normal queue durable landing plus unstable ancestry before closing item9.
|
||||
@@ -0,0 +1,8 @@
|
||||
- [x] Narrow implementation and explicit spec/integrity correction preserved.
|
||||
- [x] Final focused6files60/60/0cancelled.
|
||||
- [x] 19unique mutants killed; all27newcases covered, no surviving variants.
|
||||
- [x] Targetedlint4pass; sandboxhexEPERM plus unchangedhostpass retained.
|
||||
- [x] Durable candidate/log/mutant snapshots+manifest saved; SAFE TO RESTART.
|
||||
- [ ] Resume: review final delta/notes; finish READY and commit named paths.
|
||||
- [ ] Merge currentunstable (never rebase), verify runtime equality/checks.
|
||||
- [ ] Explicit grant for remaining lint/full/build/browser/submission; durable landing/ancestry before closure.
|
||||
Reference in new issue
Block a user