diff --git a/context/CONTEXT_2026-10-20_13-30-00_client-reliability-5.md b/context/CONTEXT_2026-10-20_13-30-00_client-reliability-5.md new file mode 100644 index 00000000..4cf5fa83 --- /dev/null +++ b/context/CONTEXT_2026-10-20_13-30-00_client-reliability-5.md @@ -0,0 +1,3 @@ +# client-reliability-5 + +Investigation at0c1dd7e8:bounded active URL success/timeout cleanly settle; disposed active primary media error starts1 new mirror HEAD(expected0). Shared/rejoin and later same-source controls pass. Evidence test_logs/TEST_LOG_2026-10-20_13-30-00_client-reliability-5.md; scratch /tmp/bitvid-client-reliability-5-active-url.mjs. Await dispatch routing; no product/regression changes. diff --git a/context/CONTEXT_2026-10-20_13-36-00_client-reliability-5.md b/context/CONTEXT_2026-10-20_13-36-00_client-reliability-5.md new file mode 100644 index 00000000..894b1ca5 --- /dev/null +++ b/context/CONTEXT_2026-10-20_13-36-00_client-reliability-5.md @@ -0,0 +1,3 @@ +# Item5 implementation + +Controller-only6-line pre-candidate ownership guard. Six new lifecycle scenarios reproduce2 baseline failures and pass18/18 fixed;6 reverted mutants rejected. Unique integrity note test_logs/TEST_INTEGRITY_client-reliability-5.md. Focused/lint/build running; awaiting explicit dispatch full-suite/browser slots and verified unstable merge before final checks/landing. No active abort/observer/cache/limit changes. diff --git a/context/CONTEXT_2026-10-20_13-39-30_storage-cors.md b/context/CONTEXT_2026-10-20_13-39-30_storage-cors.md new file mode 100644 index 00000000..bd2cf829 --- /dev/null +++ b/context/CONTEXT_2026-10-20_13-39-30_storage-cors.md @@ -0,0 +1,6 @@ +# storage-cors-1 +- Start UTC: 2026-10-20T13:38:00Z; launch reported by dispatch ~13:37:59Z; acknowledgement 13:38:24Z. +- Goal: validate current-HEAD existing-origin loss via real helper and offline SDK/transport; propose preservation semantics before edits. +- Scope: investigation/evidence first; product/test changes require dispatch routing. No browser/cloud credentials/network bucket mutations; preserve preview3000. +- Done: claimed; traced callers; 5 characterization cases reproduced. See docs/testing/storage-cors-integrity.md. +- Pending: dispatch preservation semantics/fix routing, regression/mutants/focused checks, coordinated normal gates/build/landing. diff --git a/context/CONTEXT_2026-10-20_13-58-00_client-reliability-5-landed.md b/context/CONTEXT_2026-10-20_13-58-00_client-reliability-5-landed.md new file mode 100644 index 00000000..7b6c4f06 --- /dev/null +++ b/context/CONTEXT_2026-10-20_13-58-00_client-reliability-5-landed.md @@ -0,0 +1,3 @@ +# Item5 complete + +Product landed0bd0b1c3 via4bc52f9d,normal gate exit0/unstable ancestry verified. Backlog closed;expensive slot and browser released to dispatch13:57UTC. Local427files/28focused/18lifecycle/6mutants/lint11/build/native browser smoke pass. Finish docs-only closure queue/final event then idle. Evidence test_logs/TEST_LOG_2026-10-20_13-58-00_client-reliability-5-landed.md. No active abort/playback/CPU claim. diff --git a/decisions/DECISIONS_2026-10-20_13-30-00_client-reliability-5.md b/decisions/DECISIONS_2026-10-20_13-30-00_client-reliability-5.md new file mode 100644 index 00000000..7cbc7512 --- /dev/null +++ b/decisions/DECISIONS_2026-10-20_13-30-00_client-reliability-5.md @@ -0,0 +1,3 @@ +# client-reliability-5 + +Do not equate retained active work with a leak: explicit success and controlled real timeout release resources/slots and cache correctly. Route only demonstrated new mirror HEAD after disposal; guard candidate continuation in controller before considering active abort or broader resource policy. Existing observer token is sufficient. diff --git a/decisions/DECISIONS_2026-10-20_13-36-00_client-reliability-5.md b/decisions/DECISIONS_2026-10-20_13-36-00_client-reliability-5.md new file mode 100644 index 00000000..20e9e04b --- /dev/null +++ b/decisions/DECISIONS_2026-10-20_13-36-00_client-reliability-5.md @@ -0,0 +1,6 @@ +# Item5 continuation boundary + +- Keep bounded already-active success/failure caching. Check existing lifecycle owners only BEFORE starting each candidate in probeUrlList; zero owners marks existing record cancelled and returns explicit cancellation. No probe abort or observer/cache/TTL/concurrency change. +- Shared and rejoining owners remain eligible because record owners are checked at continuation time. Completed final-source failure keeps original cache behavior; cancelled mirror chains never cache offline. +- Six focused regressions exercise actual observer/controller/cache plus external IO twins; unique integrity file during parallel trial. +- Dispatch resource coordination: local full suite is next after administrative drain but requires explicit grant; browser belongs initially to playback QA. All CLI/gate launches clear inherited NODE_TEST_CONTEXT operationally; no kit/runner change. diff --git a/decisions/DECISIONS_2026-10-20_13-39-30_storage-cors.md b/decisions/DECISIONS_2026-10-20_13-39-30_storage-cors.md new file mode 100644 index 00000000..5803feac --- /dev/null +++ b/decisions/DECISIONS_2026-10-20_13-39-30_storage-cors.md @@ -0,0 +1,6 @@ +# storage-cors-1 proposed semantics (awaiting dispatch) +- Preserve every existing rule and field; do not union new origins into arbitrary rules (would grant their permissions to new origins). +- Read existing policy before writing; only explicit NoSuchCORSConfiguration indicates absent policy. Other failures propagate without PUT. +- Add the existing default rule for newly requested origins separately. Skip PUT when equivalent existing default rules already cover every requested origin. +- Preserve wildcard rules verbatim; never infer extra methods/headers or edit unknown fields. Exact equivalence/coverage details require routing before implementation. +- No dependencies/SDK version change. Inspect SDK export read-only until routing. diff --git a/docs/TODO.md b/docs/TODO.md index 2be8a299..12e6d660 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -2,6 +2,9 @@ ## dispatch +- 2026-10-20 trial first product landed: item5 product0bd0b1c3,closuredc852597;dispatch lifecycle18/18/build-artifact/previewHTTP200+dist match. Storage slot grant13:58:27 after949s wait,full/normal landing underway;QA next,ready95a77a7f. Remaining: storage/QA durable landings,preview refresh after product changes,final trial evidence;real cloud/peers/activeabort/CPU unverified. + + - 2026-10-20 trial current: all3 pickups verified; admin notes durableb922c186. Client item5 owns exclusive full/browser/product landing from13:43:51 (~10m57s wait); storage CORS ready9/9+8mutants,next slot; QA retained4/4+6mutants,third slot,browser released. Remaining: full merged-tree validation,durable product/test landings,preview refresh and final trial outcome. No real cloud/peers/CPU qualification. @@ -28,6 +31,14 @@ ## client-reliability +- 2026-10-20: item5 done/closed after normal product landing0bd0b1c3(submission4bc52f9d,gate exit0,unstable ancestry exit0). Full427files/focused28/lifecycle18/six reverted mutants/lint11/build-artifact/Chromium+Firefox controlled smoke pass;obsolete mirror HEAD1→0. Browser+expensive slot released13:57UTC after10m57s initial wait. Dispatch landing/release event sent;preview refresh dispatch-owned. Closure metadata/final follow;no next item. Active abort/actual fallback/livepeers/CPU/fullStage3 unverified. + +- 2026-10-20: item5 ready atmergedf2a392fa(product12f92732):full427/427 files60s-bound exit0,focused28/lifecycle18,six reverted mutants,lint11/build-artifact/native Chromium+Firefox smoke pass;obsolete mirror HEAD1→0,shared/rejoin/retry/hidden reveal pass,0 external/pageerrors. Full-slot wait10m57s recorded;browser released,expensive slot held through normal landing. Remaining queue/durable ancestry/close;active abort/real playback/CPU unverified. + +- 2026-10-20: item5 routed controller-only pre-candidate guard implemented; baseline16pass/2fail→fixed18/18 lifecycle,6 rejected/reverted mutants,focused28/4files,lint11/build+artifact pass. No full/browser launch while dispatch administrative drain/QA reservation holds; slot requested13:32:54UTC,await explicit grant and verified unstable merge. No active abort/observer/cache/limits change; actual fallback/CPU unverified. Unique integrity/evidence in test_logs/*client-reliability-5*. + +- 2026-10-20: item5 investigation at0c1dd7e8:active URL success/real4000ms controlled timeout release media/slot correctly;disposed active primary media error starts1 obsolete mirror HEAD(expected0),strict assertion naturally exits1 after cleanup/retry. Shared/rejoin owner and later same-source controls pass. Proposed controller-only pre-candidate ownership guard + focused tests; await dispatch routing. No product/tests changed; active abort/browser/fallback/CPU unverified. See test_logs/TEST_LOG_2026-10-20_13-30-00_client-reliability-5.md. + - 2026-10-20: item4 done/closed after normal landing89ccb907(submission7055496b,gate exit0,unstable ancestry exit0). Full427/427 files,focused22,lifecycle12,13 reverted mutants,lint11,build/artifact/native Chromium+Firefox controlled IO pass;obsolete starts1→0. Dispatch landing event sent; preview refresh owned by dispatch. No further item without routing. Actual media fallback/live peers/active abort/CPU and whole Stage3 unverified. Closure metadata/final event follow. - 2026-10-20: item4 ready atce1381f5; full427/427 file exits0 with60s/file bound,focused22/22,lifecycle12/12,13 rejected/reverted mutants,lint11/11,build/artifact and Chromium+Firefox controlled native-observer smoke pass (obsolete starts0). Remaining normal queue/durable landing/ancestry then close/report. Actual fallback/live IO/active abort/CPU remain unverified. diff --git a/docs/agents/backlog/client-reliability-5.json b/docs/agents/backlog/client-reliability-5.json index b036202a..06b07d0d 100644 --- a/docs/agents/backlog/client-reliability-5.json +++ b/docs/agents/backlog/client-reliability-5.json @@ -1,9 +1,11 @@ { "id": "client-reliability-5", "area": "client-reliability", - "status": "open", + "status": "done", "title": "Reproduce remaining active URL or torrent probe lifecycle defect before scoping a fix", "blockedOn": null, "addedOn": "2026-10-20", - "filedAt": "3f627f5df3da7bcd28d910d9f8161844fa77ff74" + "filedAt": "3f627f5df3da7bcd28d910d9f8161844fa77ff74", + "evidence": "Landed0bd0b1c3 via4bc52f9d,normal gate exit0/unstable ancestry exit0. Full427files,focused28/lifecycle18,six reverted mutants,lint11/build/native browser smoke pass;obsoleteHEAD1\u21920.10m57s initial slot wait;expensive/browser released13:57UTC. See test_logs/TEST_LOG_2026-10-20_13-58-00_client-reliability-5-landed.md;activeabort/playback/CPU unverified.", + "closedOn": "2026-10-20" } diff --git a/docs/agents/backlog/storage-cors-1.json b/docs/agents/backlog/storage-cors-1.json index fa59181f..6c30a1ab 100644 --- a/docs/agents/backlog/storage-cors-1.json +++ b/docs/agents/backlog/storage-cors-1.json @@ -1,9 +1,10 @@ { - "id": "storage-cors-1", - "area": "storage-cors", - "status": "open", - "title": "Reproduce existing-origin loss in bucket CORS provisioning using offline transport", - "blockedOn": null, - "addedOn": "2026-10-20", - "filedAt": "0c1dd7e86be3e28edc96aca47f7753a98990dde0" + "id": "storage-cors-1", + "area": "storage-cors", + "status": "done", + "title": "Reproduce existing-origin loss in bucket CORS provisioning using offline transport", + "blockedOn": null, + "addedOn": "2026-10-20", + "filedAt": "0c1dd7e86be3e28edc96aca47f7753a98990dde0", + "closedOn": "2026-10-20" } diff --git a/docs/testing/storage-cors-integrity.md b/docs/testing/storage-cors-integrity.md new file mode 100644 index 00000000..521ae41e --- /dev/null +++ b/docs/testing/storage-cors-integrity.md @@ -0,0 +1,59 @@ +# storage-cors-1 investigation + +Start UTC 2026-10-20T13:38:00Z. Investigation checkpoint 13:39:21Z. +HEAD bbc1be79e941add929c9c51f984778676822b5f2; filedAt0c1dd7e helper/client unchanged. + +Actual caller behavior: R2Service.getCorsOrigins returns current window origin (plus HTTPS localhost counterpart). ensureBucketConfigForNpub and verifyPublicAccess use this list; generic prepareS3Connection takes supplied origins, and its getCorsOrigins likewise returns current-site origins. r2-s3 reexports the helper. Thus existing policies are not passed through callers. + +Offline command `env -u NODE_TEST_CONTEXT node /tmp/storage-cors-reproduce.mjs` exit0, 5 characterization scenarios. Real s3-client and s3-multipart imported; registerHooks replaces only remote SDK with input-holding command classes; fake S3 send implements full-policy replacement on PutBucketCors. Unexpected HTTP imports throw. Fixtures use example.invalid and no credentials. + +- Existing GET-only rule: origin first.example.invalid, header Range, ETag exposure, maxAge75, ID existing-read-only. Request second.example.invalid. Result retains0/1 prior rules; origins only second.example.invalid; helper default methods/headers replace prior fields. +- Repeat same request: policy identical, PUT total2 (no skip). +- No existing CORS config: PUT1/GET0, default policy installed. +- AccessDenied on read: PUT1/GET0; configured read error cannot surface because helper never reads. +- Empty origins: zero transport calls, policy untouched. + +Proposed semantics sent dispatch: preserve all existing rules/fields verbatim; add default rule for new origins without widening arbitrary existing rules; skip PUT when exact equivalent default rules already satisfy all requested origins; only explicit NoSuchCORSConfiguration permits absent-policy creation; other read failures reject without PUT. Wildcards/unknown fields preserved; no speculative permission merging. Scope needs dispatch routing before implementation. + +No regression test changes yet, so no Test Integrity Note/mutants claimed. No browser/full-suite/lint/build/cloud/live-policy/concurrency qualification. PR overlap check unavailable (curl DNS exit6). Preview3000 untouched. Tool/browser/gate contention before reproduction0s; routing wait starts13:39:21Z. + +## Routed implementation and integrity (2026-10-20T13:43Z) + +Dispatch routed bounded product/test fix at13:40:50UTC after independent failure. Routing wait89s. Existing rules remain verbatim/in order; only explicit origins in rules with identical default permission/header/exposure/maxAge fields count as covered. ID is preserved metadata; unknown fields disqualify coverage. Wildcards remain unchanged and are not expanded/interpreted. Append current defaults for uncovered deduplicated origins. Only NoSuchCORSConfiguration permits creation after failed read; denied/404/transport failures propagate without PUT. Existing method defaults unchanged, no SDK upgrade. + +```yaml +test_integrity_note: + change_type: ["new_tests"] + scenarios: + - id: SCN-storage-cors-preservation + given: "existing custom or wildcard rules, or equivalent default coverage" + when: "real ensureBucketCors adds/repeats requested origins through actual SDK wrapper" + then: "all original rules and fields persist; only uncovered explicit origins get default rule; repeats issue no PUT" + - id: SCN-storage-cors-read-safety + given: "absent configuration, denied read, non-CORS 404, transport failure, or empty origins" + when: "real helper configures CORS" + then: "only explicit NoSuchCORSConfiguration creates policy; other read errors propagate identically with zero PUT; empty origins zero IO" + observable_outcomes: + - "full persisted CORS policy, unchanged rule fields/order, exact write count, original error identity" + determinism_controls: + - "actual helper and SDK wrapper; fake remote SDK command classes and S3 transport; no network, credentials, clock, retries, sleeps" + anti_cheat_rationale: + prevents: + - "hard-coded success/no-op" + - "over-mocking internal helper logic" + - "policy overwrite or arbitrary permission widening" + - "snapshot rubber-stamping and retry/sleep masking" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +Nine regression scenarios pass. Original helper fails8/9 with exit1; empty-origin control passes. Ten deliberately broken variants exit1 and were restored: overwrite existing rules; reverse rules; omit dedup; repeat PUT; include covered origins; reject absent policy; swallow read errors; empty-origins IO; ignore non-origin permissions (kills scenario5); remove SDK GetBucketCors export. Every scenario fails under at least one variant. Final focused runner passes3/3 files (13 scenarios total:9 CORS,3 cache-control,1 empty upload). + +`npm ci --offline --ignore-scripts`: exit0,441 installed packages in4s from local cache. Initial lint exit127 absent stylelint; after install lint exit2 sandbox EPERM read-only git subprocess. Unchanged lint with sandbox permission exits0,11 checks. `npm run build`: exit0 incl dist artifact verification. No dependency file changes. Normal full-suite/landing slot reserved after client; awaiting explicit grant. Cloud IAM permissions, actual provider response serialization, concurrent read-modify-write races, browser/live playback remain unverified. Read-modify-write has no provider CAS guarantee and concurrent writers can still race. + +Final merged gate atbfa36a3f350d6a9751a1a43c83fc890c855091fc: full normal runner428/428 files exit0 with UNIT_TEST_TIMEOUT_MS=60000/NODE_TEST_CONTEXT unset; focused3/3 files(13 scenarios),lint11 checks,build/artifact pass. Resource wait949s before13:58:27UTC grant; full completed14:03:49UTC. No product changes since restored mutants. Normal landing pending; no real-cloud/concurrent-writer claim. + +## Durable completion + +Submission699d72927bee46bdafb6a7775a12d05f75e3c7e1 landed2026-10-20T14:09:21.582Z asd3f4c2155fe38d1b73554a6163f732b4c0ba3308. Durable landq result: landed, unchanged normal install+`npm test` gate exit0. `git log -1 unstable` matched landed SHA; `git merge-base --is-ancestor 699d72927bee46bdafb6a7775a12d05f75e3c7e1 unstable` exit0. Backlog closed only after these checks. Slot release14:09:38UTC (dispatch notification14:09:47UTC). No product edits after checked tree. Actual cloud/provider permissions/serialization/concurrent writers remain unverified; no-CAS race limitation remains. No browser/cloud/preview changes. diff --git a/fleet/LOG.md b/fleet/LOG.md index 27911da7..4818516e 100644 --- a/fleet/LOG.md +++ b/fleet/LOG.md @@ -81,3 +81,9 @@ - Administrative drain completed: item5 routingf3c0ffd3->371e7902(normal gate exit0),storage routing4c7a897c+trial notes2a5ba463->b922c186 docs-only. Dispatch syncedb922c186. Client exclusive full/browser/normal product landing slot granted13:43:51,request13:32:54 (~10m57s resource wait),QA explicitly released browser. Storage next,QA third; normal queue unchanged. - Storage ready report9/9 new/original8fail/8mutants,offline lockfile install4s441packages,build pass; lint sandbox git-spawnEPERM requires approved unchanged rerun. QA retained4/4,6 restored failing mutants; same lint environment issue,temporary read-only deps linkage in own worktree. These are worker-derived,not independent reruns. No trial product/regression landing yet; preview preserved. + +## 2026-10-20 — Trial first product outcome and resource handoff + +- Client item5 submission4bc52f9d durably landed0bd0b1c3 at13:57:06,unchanged normal gate exit0;dispatch independently checked status/log/ancestry0. Worker full427/427,focused28/lifecycle18,6mutants,lint11/build,native Chromium+Firefox health lifecycle smoke0obsoleteHEAD/resources/external/pageerrors are worker-derived. Browser/server released. +- Dispatch synced product0bd0b1c3,independent direct URL lifecycle18/18 natural exit0(336.59ms),build/artifact exit0,preserved previewHTTP200 and downloaded index byte-equal to dist(cmp0). Actual media/playback/live peers/activeabort/CPU/fullStage3 unverified. +- Storage exclusive full/normal landing slot granted13:58:27,pickup13:58:42;request13:42:38→grant949s(15m49). QA ready95a77a7f focused4/6mutants/lint11,holding since13:43:52. No new client item while remaining trial landings validate prepared stable code;next action remains completing bounded3-worker trial,not waiting owner approval. diff --git a/js/storage/s3-client.js b/js/storage/s3-client.js index 1c929540..52da84ba 100644 --- a/js/storage/s3-client.js +++ b/js/storage/s3-client.js @@ -24,6 +24,7 @@ function assignAwsSdk(module) { AbortMultipartUploadCommand: module.AbortMultipartUploadCommand, ListBucketsCommand: module.ListBucketsCommand, HeadBucketCommand: module.HeadBucketCommand, + GetBucketCorsCommand: module.GetBucketCorsCommand, PutBucketCorsCommand: module.PutBucketCorsCommand, DeleteObjectCommand: module.DeleteObjectCommand, PutObjectCommand: module.PutObjectCommand, diff --git a/js/storage/s3-multipart.js b/js/storage/s3-multipart.js index fc23765f..9d9704f0 100644 --- a/js/storage/s3-multipart.js +++ b/js/storage/s3-multipart.js @@ -104,31 +104,58 @@ export async function ensureBucketCors({ s3, bucket, origins, region } = {}) { throw new Error("Bucket name is required to configure CORS"); } - const { PutBucketCorsCommand } = requireAwsSdk(); - - const allowedOrigins = (origins || []).filter(Boolean); + const allowedOrigins = [...new Set((origins || []).filter(Boolean))]; if (allowedOrigins.length === 0) { return; } + const { GetBucketCorsCommand, PutBucketCorsCommand } = requireAwsSdk(); await ensureBucketExists({ s3, bucket, region }); + let existingRules; + try { + const response = await s3.send(new GetBucketCorsCommand({ Bucket: bucket })); + existingRules = response.CORSRules || []; + } catch (error) { + // A denied or failed read must never turn into a destructive replacement. + if (error?.name !== "NoSuchCORSConfiguration") { + throw error; + } + existingRules = []; + } + + const defaultRule = { + AllowedHeaders: ["*"], + AllowedMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "OPTIONS"], + ExposeHeaders: ["ETag", "Content-Length", "Content-Range", "Accept-Ranges"], + MaxAgeSeconds: 3600, + }; + const equivalentRules = existingRules.filter((rule) => + // Only explicit origins in equivalent default rules count as coverage. + // Arbitrary rules (including wildcard rules) remain completely untouched. + Object.keys(rule).every((key) => + key === "AllowedOrigins" || key === "ID" || key in defaultRule + ) && + rule.MaxAgeSeconds === defaultRule.MaxAgeSeconds && + ["AllowedHeaders", "AllowedMethods", "ExposeHeaders"].every((key) => + Array.isArray(rule[key]) && + rule[key].length === defaultRule[key].length && + defaultRule[key].every((value) => rule[key].includes(value)) + ) + ); + const uncoveredOrigins = allowedOrigins.filter((origin) => + !equivalentRules.some((rule) => rule.AllowedOrigins?.includes(origin)) + ); + if (uncoveredOrigins.length === 0) { + return; + } + const command = new PutBucketCorsCommand({ Bucket: bucket, CORSConfiguration: { CORSRules: [ - { - AllowedHeaders: ["*"], - AllowedMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "OPTIONS"], - AllowedOrigins: allowedOrigins, - ExposeHeaders: [ - "ETag", - "Content-Length", - "Content-Range", - "Accept-Ranges", - ], - MaxAgeSeconds: 3600, - }, + ...existingRules, + { ...defaultRule, AllowedOrigins: uncoveredOrigins }, ], }, }); diff --git a/js/ui/urlHealthController.js b/js/ui/urlHealthController.js index afae005a..d99f9e84 100644 --- a/js/ui/urlHealthController.js +++ b/js/ui/urlHealthController.js @@ -463,6 +463,12 @@ export default class UrlHealthController { } let sawTimeout = false; for (const url of list) { + // Completed active work may still cache its result, but starting another + // source requires a live owner (including a card that rejoined meanwhile). + if (options.lifecycle && !options.lifecycle.owners.size) { + options.lifecycle.cancelled = true; + return { outcome: "cancelled" }; + } let result; try { result = await this.probeUrl(url, options); diff --git a/test_logs/TEST_INTEGRITY_client-reliability-5.md b/test_logs/TEST_INTEGRITY_client-reliability-5.md new file mode 100644 index 00000000..b4c32ad0 --- /dev/null +++ b/test_logs/TEST_INTEGRITY_client-reliability-5.md @@ -0,0 +1,50 @@ +# Item5 integrity note + +```yaml +test_integrity_note: + change_type: ["new_tests"] + scenarios: + - id: SCN-active-primary-disposed-mirror + given: "primary media is active, with an alternate mirror" + when: "the last owner disposes then primary media errors" + then: "no mirror HEAD starts; chain settles null without cache or stale UI, later fresh request succeeds" + - id: SCN-active-shared-mirror + given: "two registrations share active media" + when: "one owner disposes and primary errors" + then: "ordered mirror serves live owner, healthy cache avoids later requests, disposed badge stays unchanged" + - id: SCN-active-rejoin-mirror + given: "primary active media survives last-owner disposal" + when: "same-source live owner rejoins before primary error" + then: "dedup survives and ordered mirror serves new owner without duplicate primary IO" + - id: SCN-active-later-mirror-disposal + given: "first mirror media is active and another mirror exists" + when: "last owner disposes then first mirror errors" + then: "next mirror HEAD does not start, cancellation settles without cache or stale UI" + - id: SCN-active-timeout-cleanup-retry + given: "media is active with real configured initial and longer retry deadlines" + when: "last owner disposes and controlled clock reaches initial timeout" + then: "slot/source clear, no new retry resources or cache, later fresh request is healthy" + - id: SCN-completed-active-final-error-cache + given: "active primary has no further candidate" + when: "last owner disposes then media errors" + then: "completed active failure keeps existing offline cache result without stale UI" + observable_outcomes: + - "fetch/media starts, source cleanup, slot release, promise settlement and actual cache/inflight records" + - "live/disposed connected badge state and ordered mirror requests" + determinism_controls: + - "Real observer/controller/cache with controlled IntersectionObserver, jsdom native media event APIs and stubbed external fetch/media IO" + - "node:test mock clock advances configured4000ms timeout; no sleeps, retries, public network or forced exit" + anti_cheat_rationale: + prevents: + - "returning cancelled for live shared/rejoining owners" + - "guarding only first mirror or discarding completed active results" + - "caching cancellation as offline or starting obsolete retry resources" + - "over-mocking queue/controller instead of service boundaries" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +Existing assertions unchanged; six new scenarios (18 total lifecycle scenarios). Baseline before guard naturally exits1 with new primary/later-mirror failures (16pass/2fail/0cancelled). Fixed18pass/0fail/0cancelled. Unique note avoids shared integrity-file writes during parallel trial. + +`python3 /tmp/item5-mutants.py`: six deliberately broken variants each naturally exit1 with named assertion failures,0 cancelled, restored in finally before gates. Missing candidate guard kills primary/later-mirror scenarios; cancelling live mirrors kills shared+rejoin controls; checking only first mirror kills later-mirror case; caching cancellation kills null/cache expectations; discarding final active errors kills existing-offline-cache scenario; removing both media no-owner checks kills timeout cleanup/retry. Results `/tmp/item5-mutants.json` and `/tmp/item5-mutant-*.log`. No gate/test-registration changes or timed-out mutants counted. diff --git a/test_logs/TEST_LOG_2026-10-20_13-30-00_client-reliability-5.md b/test_logs/TEST_LOG_2026-10-20_13-30-00_client-reliability-5.md new file mode 100644 index 00000000..d00b6fc1 --- /dev/null +++ b/test_logs/TEST_LOG_2026-10-20_13-30-00_client-reliability-5.md @@ -0,0 +1,28 @@ +# client-reliability-5 active URL investigation + +## Scope and source + +- HEAD `0c1dd7e86be3e28edc96aca47f7753a98990dde0`; compared filedAt `3f627f5df3da7bcd28d910d9f8161844fa77ff74`: no observer/controller product differences. Claimed item5 after merge/read updated routing; no product or regression changes. +- Scratch `/tmp/bitvid-client-reliability-5-active-url.mjs`, SHA256 `31c7fa62c306b28008ee663fc69d25834904ee38cbf34711230ff0d3cdc66fd7`. +- Real attachUrlHealthBadges/cardObserver/controller/probeUrlList/probeUrl/media wait queue and actual state/cache functions. Callback shape matches VideoListView and feedCoordinator. jsdom/native DOM event APIs; only external fetch responses and media load/pause are controlled. Deferred fetch records and explicit loadeddata/error events; node:test mock clock advances the real configured4000ms deadline. No sleeps/retries/live network/forced exit. Mock timers reset and DOM closed before failing assertion. Real media decoding/resources are virtualized; counts describe IO boundary retention, not CPU/RSS. +- First mailbox_read hit automatic approval-review deadline, not rejection; one allowed retry succeeded. Pickup reported to dispatch; no bypass/control change. + +## Commands and observations + +All commands use `env -u NODE_TEST_CONTEXT node /tmp/bitvid-client-reliability-5-active-url.mjs `. Each uses one event/source, optional mirror and actual in-flight deduplication. Initial primary HEAD is explicitly resolved to create one active media probe before disposal. Disposed DOM stays connected to expose stale UI. Before/after disposal: HEAD starts1, media resource1, active slot1, queued0, settled0, no cache. Observer disconnect confirmed. + +- `success`: natural exit0. Explicit loadeddata after disposal releases resource/slot to0, settles1, stores healthy once; old connected badge remains checking. Later same-source registration uses healthy cache, adds0 HEAD requests and paints healthy. This is intentional bounded active caching, not a demonstrated leak. +- `timeout`: natural exit0. Advancing real4000ms media deadline with mock clock releases resource/slot to0, settles1, no health cache or additional media. Existing longer retry option is configured via actual cache constants, but no-owner lifecycle prevents another media start. Later same source starts1 fresh HEAD/media and paints healthy; final settled2, one healthy store from retry only. +- `mirror-disposed --expect-no-obsolete-head`: natural exit1 AFTER cleanup/retry, AssertionError `1 !== 0` at line50, message `disposed last-owner active failure must not start a fresh mirror HEAD`. Primary media error after disposal starts mirror HEAD2: obsolete new HEAD count1(expected0), unresolved HEAD1, media resource0, active slot0, queued0, unsettled original promise. Resolving that controlled mirror HEAD eventually settles original null with no cache and no new media/stale badge. Later retry starts1 fresh primary HEAD/media, healthy cache/badge, all media/slots0; final settled2. +- `mirror-shared`: natural exit0. A second live registration requests same event/source before original disposal: still1 initial HEAD and1 in-flight promise. Primary error starts one ordered mirror HEAD; resolving it starts one mirror media and loadeddata settles healthy for live badge; disposed badge stays checking. Final initial-chain media/slots0, settled1, healthy cache, later retry adds0 HEAD. +- `mirror-rejoin`: natural exit0. Same as shared control, but the second same-source registration arrives AFTER original disposal while its primary media remains active. It deduplicates onto original promise, restores live ownership, and mirror success paints healthy; no duplicated primary request, stale badge or retained media/slot after completion. + +## Smallest reproduced defect and proposed routing + +Active primary media failure after the LAST owner is disposed starts a NEW mirror HEAD request. This is additional obsolete work and promise retention, distinct from allowing an already-active bounded probe to settle/cache. `probeUrlList` moves from a completed error result to the next candidate without checking live ownership; the media-stage guard is later, after that mirror HEAD has started. + +Proposed scope: `js/ui/urlHealthController.js` lifecycle check BEFORE starting a subsequent candidate, focused URL lifecycle regression scenarios, Test Integrity Note and evidence. Existing observer token/unregister hook is sufficient. No active HEAD/media/HLS/torrent abort, shared observer/app/cache-module edits, TTL/limit changes or broad Stage3 work proposed. + +Acceptance:0 new mirror HEAD starts after last-owner active error; cancelled chain settles without negative cache/stale effects; active healthy caching remains; shared live/rejoining owner still gets ordered mirror success; timeout and later same-source requests retain slot/cache/inflight safety. Preserve existing mirror fallthrough for live owners. + +Unverified: active HEAD/HLS/torrent teardown, real media decoding/network/browser fallback/live peers, CPU/RSS and whole Stage3. No browser/full-suite/lint/build reruns for evidence-only investigation; prior item4 qualification not repeated. Preview3000 untouched. Await dispatch independent assessment and routing before product/tests. diff --git a/test_logs/TEST_LOG_2026-10-20_13-36-00_client-reliability-5.md b/test_logs/TEST_LOG_2026-10-20_13-36-00_client-reliability-5.md new file mode 100644 index 00000000..8873ea4e --- /dev/null +++ b/test_logs/TEST_LOG_2026-10-20_13-36-00_client-reliability-5.md @@ -0,0 +1,10 @@ +# Item5 precommit evidence + +- `env -u NODE_TEST_CONTEXT node tests/url-health-lifecycle.test.mjs` before guard: natural exit1,16pass/2fail/0cancelled, disposed active primary and later-mirror cases fail. After guard and formatting: natural exit0,18/18,0 failed/cancelled/skipped (`/tmp/item5-lifecycle-final.log`). +- `python3 /tmp/item5-mutants.py`: six/six reverted variants naturally exit1 with named assertion failures,0 cancelled; no timeouts or survivors. Unique integrity mapping test_logs/TEST_INTEGRITY_client-reliability-5.md. +- `env -u NODE_TEST_CONTEXT node scripts/run-unit-tests.mjs url-health health-disposal`: natural exit0,4 files/28 scenarios (lifecycle18,controller4,multisource4,view lifecycle2). `/tmp/item5-focused.log`. +- `env -u NODE_TEST_CONTEXT npm run lint` with approved git subprocess access: exit0,11 stages (`/tmp/item5-lint.log`). +- `env -u NODE_TEST_CONTEXT npm run build`: exit0 including artifact verification (`/tmp/item5-build.log`). No dependency/runner/gate changes. +- First mailbox_read tool review hit its deadline; accumulated visible tool waits at least57s, exact approval wall duration not separately instrumented. It was a tool/approval stall, not product failure or rejection. One permitted retry succeeded; pickup mailbox sent13:27:49 UTC. +- Full-suite/browser slots requested13:32:54 UTC. Dispatch queued local full slot behind administrative drain and reserves initial browser to QA; no full/browser launches made. Full-slot wait duration will be measured through grant, not inferred from silence. +- Current code/tests based on0c1dd7e8 plus notes-only140618f8; final verified unstable merge, full/browser grants/checks, unchanged normal landing/durable ancestry remain. Active abort,real decoding/playback/live peers/CPU and whole Stage3 unverified. diff --git a/test_logs/TEST_LOG_2026-10-20_13-39-30_storage-cors.md b/test_logs/TEST_LOG_2026-10-20_13-39-30_storage-cors.md new file mode 100644 index 00000000..ada21496 --- /dev/null +++ b/test_logs/TEST_LOG_2026-10-20_13-39-30_storage-cors.md @@ -0,0 +1,7 @@ +# storage-cors-1 offline investigation +- HEAD: bbc1be79e941add929c9c51f984778676822b5f2; branch area/storage-cors. +- `git diff 0c1dd7e86be3e28edc96aca47f7753a98990dde0 HEAD -- js/storage/s3-multipart.js js/storage/s3-client.js`: no changes; filed report still applies. +- `env -u NODE_TEST_CONTEXT node /tmp/storage-cors-reproduce.mjs`: exit0, 5 characterization cases: existing-rule loss0/1 preserved; repeated call identical policy/2 PUTs; absent-config1 PUT; read-denial fixture1 PUT/0 GET; empty origins0 IO. +- Uses production s3-client and s3-multipart modules, registerHooks fake SDK import, replacing policy transport. No internal helper replacement. All unexpected HTTP imports throw. +- `curl -s --max-time 10 'https://api.github.com/repos/PR0M3TH3AN/bitvid/pulls?state=open&per_page=100' -o /tmp/storage-cors-open-prs.json`: exit6 DNS; no overlap verification claimed. +- No new regression tests or product edits yet. No full unit/lint/build/browser/cloud checks run. No resource wait before reproduction; awaiting routing since13:39:21Z. diff --git a/test_logs/TEST_LOG_2026-10-20_13-43-30_storage-cors.md b/test_logs/TEST_LOG_2026-10-20_13-43-30_storage-cors.md new file mode 100644 index 00000000..55759170 --- /dev/null +++ b/test_logs/TEST_LOG_2026-10-20_13-43-30_storage-cors.md @@ -0,0 +1,10 @@ +# storage-cors-1 routed verification +- Base HEAD bbc1be79e941add929c9c51f984778676822b5f2; uncommitted implementation. +- `env -u NODE_TEST_CONTEXT node tests/storage-cors-preservation.test.mjs`:9/9 pass; original HEAD helper variant8/9 fail, exit1. +- `python3 /tmp/storage-cors-mutants.py`:8/8 restored variants rejected; extra ignore-permissions/missing-SDK-export variants rejected,10 total. +- `env -u NODE_TEST_CONTEXT node scripts/run-unit-tests.mjs storage-cors-preservation storage-cache-control multipart-empty-file-guard`: exit0,3/3 files,13 scenarios. +- `env -u NODE_TEST_CONTEXT npm ci --offline --ignore-scripts`: exit0,4s,441 packages installed, no dependency manifest change. +- `env -u NODE_TEST_CONTEXT npm run lint`: first exit127 missing stylelint; second exit2 sandbox git subprocess EPERM; unchanged permission retry exit0 (11 checks). +- `env -u NODE_TEST_CONTEXT npm run build`: exit0 incl artifact verifier. +- `git diff --check`: exit0. +- Not run: full normal gate, browser/cloud. Dispatch reserves full/submit slot after client; awaiting explicit grant. diff --git a/test_logs/TEST_LOG_2026-10-20_13-44-40_storage-cors.md b/test_logs/TEST_LOG_2026-10-20_13-44-40_storage-cors.md new file mode 100644 index 00000000..39a82b4a --- /dev/null +++ b/test_logs/TEST_LOG_2026-10-20_13-44-40_storage-cors.md @@ -0,0 +1,7 @@ +# storage-cors-1 merged-tree verification +- Product commit:2fb6a536568eeb5b4cc04257255f98cde34b7f66. +- Required merge:0919be930c1c8cc8e26685381b3763b660f741ed (git merge unstable; sandbox EROFS required authorized permission retry). Incoming changes administrative only. +- `env -u NODE_TEST_CONTEXT node scripts/run-unit-tests.mjs storage-cors-preservation storage-cache-control multipart-empty-file-guard`: exit0,3/3 files,13 scenarios. +- `env -u NODE_TEST_CONTEXT npm run lint`: exit0,11 checks; same sandbox permission for read-only git subprocess. +- `env -u NODE_TEST_CONTEXT npm run build`: exit0 incl deployment-artifact verifier. +- Full/submit reserved after client; no explicit grant as of13:44:33UTC. No browser/cloud or concurrent-writer qualification. No landing claim. diff --git a/test_logs/TEST_LOG_2026-10-20_13-51-00_client-reliability-5-ready.md b/test_logs/TEST_LOG_2026-10-20_13-51-00_client-reliability-5-ready.md new file mode 100644 index 00000000..b653c5a3 --- /dev/null +++ b/test_logs/TEST_LOG_2026-10-20_13-51-00_client-reliability-5-ready.md @@ -0,0 +1,11 @@ +# Item5 ready on merged f2a392fa3b969a66cdaca2ee5e8877c72658bcdf + +- Product12f927321fdd0a945f3bdcfb3a618ef16b54ff7c; non-destructive merge of verified unstableb922c186 printedf2a392fa. Tree clean through checks; product/controller diff remains6lines. +- Resource request13:32:54UTC; dispatch explicit exclusive full→browser→product-landing grant13:43:51.426UTC:10m57s waiting. Merge completed13:44:06,check-start event13:44:28. Full pass/browser-start event13:50:44,browser done by13:50:49. No full/browser launched before grant. Initial pickup13:27:49; approval-review deadline stall is tooling, not product failure/rejection (see precommit note). +- `env -u NODE_TEST_CONTEXT UNIT_TEST_TIMEOUT_MS=60000 npm test`: natural exit0,427/427 files,0 quarantined; log `/tmp/item5-full-merged.log`. No aggregate scenario count exposed by runner. +- `env -u NODE_TEST_CONTEXT npm run lint` with approved git subprocess access: exit0,11stages (`/tmp/item5-lint-merged.log`). +- `env -u NODE_TEST_CONTEXT npm run build`: exit0 including artifact verification (`/tmp/item5-build-merged.log`). +- Relevant focused4files/28scenarios pass,including18 lifecycle; six new scenarios baseline16pass/2fail→fixed18pass/0fail/0cancelled; six restored mutants naturally exit1 with named failures/0cancelled. Unique integrity note `test_logs/TEST_INTEGRITY_client-reliability-5.md`. +- `env -u NODE_TEST_CONTEXT node /tmp/item5-browser.mjs` with approved private loopback/browser access: exit0, Chromium+Firefox built-dist/native IntersectionObserver, external media/fetch IO virtualized. Each obsolete mirror HEAD0 vs baseline1; cancelled result null/no cache/old connected badge checking; later same-source retry healthy; shared/rejoin controls each one ordered mirror with live healthy/old checking and resource/slot0; hidden reveal works; external requests0/pageerrors0. Browser cache is a twin, unit uses actual cache module. Owned server/browser closed and preview3000 untouched. +- Browser released after smoke,exclusive expensive slot retained through normal product landing as granted. Next normal CLI submit clears NODE_TEST_CONTEXT without changing gate/runner/assertions; durable landing/unstable ancestry/close/final events remain. +- Out of scope/unverified: active HEAD/media/HLS/torrent abort, actual media decoding/playback fallback/live peers,CPU-RSS/full Stage3,production/signers/wallet/deploy. diff --git a/test_logs/TEST_LOG_2026-10-20_13-58-00_client-reliability-5-landed.md b/test_logs/TEST_LOG_2026-10-20_13-58-00_client-reliability-5-landed.md new file mode 100644 index 00000000..cb53db54 --- /dev/null +++ b/test_logs/TEST_LOG_2026-10-20_13-58-00_client-reliability-5-landed.md @@ -0,0 +1,9 @@ +# Item5 product landing and release proof + +- Product12f927321fdd0a945f3bdcfb3a618ef16b54ff7c; verified merged-tree checksf2a392fa; final notes6944d6f9 and documentation-only unstable merge→submission4bc52f9ded7484b93fdb2f6b082666e6aeafe44e. Product/tests identical to verified tree after final merge (git diff exit0); no blanket reruns. +- `env -u NODE_TEST_CONTEXT node tools/fleet/bin/landq.mjs submit`: exit0 normal queue/drain. MCP land_status4bc52f9d durable landed0bd0b1c3e36542b567cb58c6cd5de0a577cd003a at2026-10-20T13:57:06.683Z. Result detail confirms unchanged normal npm-ci/install+npm-test gate exit0. `git log -1 unstable` printed0bd0b1c3; `git merge-base --is-ancestor 4bc52f9d unstable` exit0. +- Local checks:427/427 unit files (60s/file diagnostic),focused28/4files,lifecycle18,six rejected/reverted mutants,lint11/build-artifact/native Chromium+Firefox controlled IO smoke all pass. Six new scenarios preserved existing assertions. Commands/mapping in ready/precommit/unique-integrity notes. +- Defect count obsolete mirror HEAD1→0 after disposed active primary/later-mirror failure. Shared/rejoin/retry/hidden reveal healthy;disposed connected badges unchanged,cancelled cache null,resource/slot0,external requests/pageerrors0. Unit actual cache;browser cache twin/external media IO controlled. +- Resource request13:32:54→grant13:43:51.426UTC wait10m57s. Browser closed/released before product submit13:51:49. Normal landing at13:57:06; product proof/release mailbox13:57:51UTC releases exclusive expensive slot to dispatch/storage. No full/browser before grant,no extra reruns. Preview3000 untouched;dispatch owns refresh. Pickup13:27:49/tool approval deadline separately documented as tooling stall. +- Backlog close executed only after durable product proof. Product landing+slot release event sent to dispatch. Closure metadata normal queue/final event remains; no next item started. +- Unverified/out of scope:active HEAD/media/HLS/torrent abort,actual decoding/playback fallback/live peers,CPU-RSS/full Stage3,production/signers/wallet/deploy. diff --git a/test_logs/TEST_LOG_2026-10-20_13-59-00_dispatch-item5.md b/test_logs/TEST_LOG_2026-10-20_13-59-00_dispatch-item5.md new file mode 100644 index 00000000..7886570b --- /dev/null +++ b/test_logs/TEST_LOG_2026-10-20_13-59-00_dispatch-item5.md @@ -0,0 +1,7 @@ +# Dispatch landed item5 verification + +- Submission4bc52f9d durable landed0bd0b1c3;git log+ancestry0. Closure25156812 durable lookup and unstable closuredc852597 checked separately. Normal gate unchanged,exit0 recorded. +- env -u NODE_TEST_CONTEXT node tests/url-health-lifecycle.test.mjs:18/18,0fail/cancelled,natural exit0,336.59ms. Includes last-owner mirror guard/shared/rejoin/timeout/offline cache/retry invariants. +- npm run build including artifact verification:exit0. Existing preview server preserved;host localhost curl --fail http://127.0.0.1:3000 exit0HTTP200;cmp dist/index.html downloaded preview exit0. +- Worker-derived full427/427files60s-bound,focused28,6restored mutants,lint11/native Chromium+Firefox controlled lifecycle smoke. Not independently rerun. +- No real decoding/playback fallback/live peers/active abort/CPU-RSS/fullStage3 qualification. Storage full/normal slot granted13:58:27,wait949s;QA holding. diff --git a/test_logs/TEST_LOG_2026-10-20_14-04-00_storage-cors.md b/test_logs/TEST_LOG_2026-10-20_14-04-00_storage-cors.md new file mode 100644 index 00000000..2db5c497 --- /dev/null +++ b/test_logs/TEST_LOG_2026-10-20_14-04-00_storage-cors.md @@ -0,0 +1,9 @@ +# storage-cors-1 final pre-submit checks +- Product2fb6a536568eeb5b4cc04257255f98cde34b7f66; newest unstable merge bfa36a3f350d6a9751a1a43c83fc890c855091fc. +- Slot granted13:58:27UTC; requested13:42:38UTC; resource wait949s. Start acknowledgement13:58:42UTC. Full completed14:03:49UTC. +- `env -u NODE_TEST_CONTEXT UNIT_TEST_TIMEOUT_MS=60000 npm test`: exit0,428/428 files, no failed/cancelled files (normal runner,60s/file diagnostic). +- `env -u NODE_TEST_CONTEXT node scripts/run-unit-tests.mjs storage-cors-preservation storage-cache-control multipart-empty-file-guard`:exit0,3/3 files,13 scenarios. +- `env -u NODE_TEST_CONTEXT npm run lint`:exit0,11 checks, read-only git subprocess permission as before. +- `env -u NODE_TEST_CONTEXT npm run build`:exit0 including deploy-artifact verifier. +- No new product changes since10 rejected/restored mutants/9 CORS scenarios. No weakened assertions/retries/sleeps. +- Pending normal queue/durable status+ancestry before closure. No cloud/browser/concurrent-writer qualification; preserve preview3000. IAM/read permission and lost updates from concurrent non-CAS policy writes remain limitations. diff --git a/test_logs/TEST_LOG_2026-10-20_14-10-00_storage-cors.md b/test_logs/TEST_LOG_2026-10-20_14-10-00_storage-cors.md new file mode 100644 index 00000000..95f69b3f --- /dev/null +++ b/test_logs/TEST_LOG_2026-10-20_14-10-00_storage-cors.md @@ -0,0 +1,9 @@ +# storage-cors-1 landing +- Product submission699d72927bee46bdafb6a7775a12d05f75e3c7e1 durably landed asd3f4c2155fe38d1b73554a6163f732b4c0ba3308 at14:09:21.582UTC. +- `env -u NODE_TEST_CONTEXT node tools/fleet/bin/landq.mjs status 699d72927bee46bdafb6a7775a12d05f75e3c7e1`: state landed; recorded unchanged normal install/npm-test gate exit0. +- `git log -1 unstable --format='%H %s'`: matched d3f4c215. +- `git merge-base --is-ancestor 699d72927bee46bdafb6a7775a12d05f75e3c7e1 unstable`:exit0. +- `backlog.mjs close storage-cors-1`: success after landing/ancestry; gate resource released to dispatch14:09:47UTC. +- Prior qualification:428/428 full files;13 focused scenarios/3files;9 new CORS scenarios;10 rejected restored mutants;11 lint checks/build artifact pass. +- Start13:38:00UTC; product completion14:09:38UTC; routing wait89s/resource wait949s; cached dependency install4s. No browser contention since no browser used. Queue gate14:04:20.323–14:09:21.582UTC(~301s). +- No cloud/live-service/credential use,provider serialization/IAM validation or concurrent-writer guarantee. Existing no-CAS read-modify-write race remains. Preview3000 untouched. diff --git a/tests/storage-cors-preservation.test.mjs b/tests/storage-cors-preservation.test.mjs new file mode 100644 index 00000000..49dd1a9e --- /dev/null +++ b/tests/storage-cors-preservation.test.mjs @@ -0,0 +1,140 @@ +// Real storage helper + SDK wrapper; only SDK classes and S3 transport are fake. +import test from "node:test"; +import assert from "node:assert/strict"; +import { registerHooks } from "node:module"; + +const commandNames = ["S3Client", "HeadBucketCommand", "CreateBucketCommand", "GetBucketCorsCommand", "PutBucketCorsCommand"]; +const hooks = registerHooks({ + resolve(specifier, context, nextResolve) { + if (specifier.startsWith("https://esm.sh/@aws-sdk/client-s3@")) { + return { url: "offline:storage-cors-sdk", shortCircuit: true }; + } + if (/^https?:/.test(specifier)) throw new Error("Network forbidden in CORS fixtures"); + return nextResolve(specifier, context); + }, + load(url, context, nextLoad) { + if (url === "offline:storage-cors-sdk") { + return { format: "module", shortCircuit: true, source: commandNames.map(name => + `export class ${name} { constructor(input) { this.input = input; } }` + ).join("\n") }; + } + return nextLoad(url, context); + }, +}); +// The full runner disables network imports globally. Our hook supplies the SDK locally. +globalThis.__BITVID_DISABLE_NETWORK_IMPORTS__ = false; +const { ensureS3SdkLoaded } = await import("../js/storage/s3-client.js"); +await ensureS3SdkLoaded(); +const { ensureBucketCors } = await import("../js/storage/s3-multipart.js"); +hooks.deregister(); +globalThis.__BITVID_DISABLE_NETWORK_IMPORTS__ = true; + +const first = "https://first.example.invalid"; +const second = "https://second.example.invalid"; +const third = "https://third.example.invalid"; +const custom = { ID: "read-only", AllowedOrigins: [first], AllowedMethods: ["GET"], AllowedHeaders: ["Range"], ExposeHeaders: ["ETag"], MaxAgeSeconds: 75 }; +const defaults = origins => ({ AllowedHeaders: ["*"], AllowedMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "OPTIONS"], AllowedOrigins: origins, ExposeHeaders: ["ETag", "Content-Length", "Content-Range", "Accept-Ranges"], MaxAgeSeconds: 3600 }); +function transport(rules, readError) { + return { + policy: rules === null ? null : { CORSRules: structuredClone(rules) }, puts: 0, calls: [], + async send(command) { + const name = command.constructor.name; + this.calls.push(name); + assert.equal(command.input.Bucket, "offline-bucket"); + if (name === "HeadBucketCommand") return {}; + if (name === "GetBucketCorsCommand") { + if (readError) throw readError; + if (this.policy === null) throw Object.assign(new Error("No policy"), { name: "NoSuchCORSConfiguration" }); + return structuredClone(this.policy); + } + if (name === "PutBucketCorsCommand") { + this.puts++; + this.policy = structuredClone(command.input.CORSConfiguration); + return {}; + } + throw new Error(`Unexpected command ${name}`); + }, + }; +} +const ensure = (s3, origins) => ensureBucketCors({ s3, bucket: "offline-bucket", origins }); + +test("adding a second origin preserves the complete existing custom policy", async () => { + const s3 = transport([custom]); + await ensure(s3, [second]); + assert.deepEqual(s3.policy, { CORSRules: [custom, defaults([second])] }); + assert.equal(s3.puts, 1); +}); + +test("wildcard and multiple custom rules keep their fields and order", async () => { + const wildcard = { ...custom, ID: "wildcard", AllowedOrigins: ["*"], AllowedHeaders: ["X-Custom"], AllowedMethods: ["HEAD"], ExtraField: "preserve" }; + const s3 = transport([wildcard, custom]); + await ensure(s3, [second]); + assert.deepEqual(s3.policy, { CORSRules: [wildcard, custom, defaults([second])] }); +}); + +test("duplicate origins and repeated calls are idempotent without another PUT", async () => { + const s3 = transport([custom]); + await ensure(s3, [second, second, null, ""]); + const once = structuredClone(s3.policy); + assert.deepEqual(once, { CORSRules: [custom, defaults([second])] }); + await ensure(s3, [second, second]); + assert.deepEqual(s3.policy, once); + assert.equal(s3.puts, 1); +}); + +test("equivalent default rules cover origins across rules regardless of array order", async () => { + const existing = [defaults([first]), { ...defaults([second]), AllowedMethods: [...defaults([]).AllowedMethods].reverse() }]; + const s3 = transport(existing); + await ensure(s3, [first, second]); + assert.deepEqual(s3.policy, { CORSRules: existing }); + assert.equal(s3.puts, 0); +}); + +test("similar rules with different non-origin permissions do not cover a new request", async () => { + for (const override of [ + { AllowedMethods: ["GET"] }, + { AllowedHeaders: ["Range"] }, + { ExposeHeaders: ["ETag"] }, + { MaxAgeSeconds: 75 }, + { ExtraField: "preserve" }, + ]) { + const existing = { ...defaults([second]), ...override }; + const s3 = transport([existing]); + await ensure(s3, [second]); + assert.deepEqual(s3.policy, { CORSRules: [existing, defaults([second])] }); + assert.equal(s3.puts, 1); + } +}); + +test("only uncovered origins receive a separate rule", async () => { + const existing = [defaults([first]), custom]; + const s3 = transport(existing); + await ensure(s3, [first, second, third]); + assert.deepEqual(s3.policy, { CORSRules: [...existing, defaults([second, third])] }); +}); + +test("an explicit absent policy creates defaults and then skips repeated writes", async () => { + const s3 = transport(null); + await ensure(s3, [first]); + assert.deepEqual(s3.policy, { CORSRules: [defaults([first])] }); + await ensure(s3, [first]); + assert.equal(s3.puts, 1); +}); + +test("all other policy read failures reject unchanged without PUT", async () => { + for (const error of [Object.assign(new Error("Denied"), { name: "AccessDenied" }), Object.assign(new Error("Not found"), { name: "NoSuchBucket", $metadata: { httpStatusCode: 404 } }), new Error("Transport failed")]) { + const s3 = transport([custom], error); + await assert.rejects(ensure(s3, [second]), actual => actual === error); + assert.deepEqual(s3.policy, { CORSRules: [custom] }); + assert.equal(s3.puts, 0); + } +}); + +test("empty or missing origins perform no IO and preserve policy", async () => { + for (const origins of [[], undefined, [null, "", false]]) { + const s3 = transport([custom]); + await ensure(s3, origins); + assert.deepEqual(s3.calls, []); + assert.deepEqual(s3.policy, { CORSRules: [custom] }); + } +}); diff --git a/tests/url-health-lifecycle.test.mjs b/tests/url-health-lifecycle.test.mjs index 1ea32a56..909310b5 100644 --- a/tests/url-health-lifecycle.test.mjs +++ b/tests/url-health-lifecycle.test.mjs @@ -436,3 +436,144 @@ test("shared inflight ownership also survives a second controller using the same assert.equal(badge.dataset.urlHealthState, "healthy"); assert.equal(f.heads.length, 4); }); + +test("active primary error after last-owner disposal starts no mirror HEAD and permits a fresh request", async (t) => { + const f = fixture(t), + v = f.video("disposed-active-mirror"); + v.sources = [{ url: "https://mirror.invalid/obsolete.mp4" }]; + const old = f.feed([v]); + old.observer.show(); + await flush(); + assert.equal(f.heads.length, 1); + assert.equal(f.media.length, 1); + old.dispose(); + f.done(f.media[0], "error"); + await flush(); + assert.equal( + f.heads.length, + 1, + "disposed chain must not start a mirror HEAD" + ); + assert.equal(await f.promises[0], null); + assert.equal(cache.getCachedUrlHealth(v.id, v.url), null); + assert.equal(f.controller.activeVideoProbes, 0); + assert.equal(f.media[0].getAttribute("src"), null); + assert.equal(old.badges[0].dataset.urlHealthState, "checking"); + const fresh = f.feed([v]); + fresh.observer.show(); + await flush(); + assert.equal(f.heads.length, 2); + assert.equal(f.media.length, 2); + f.done(f.media[1]); + await flush(); + assert.equal(fresh.badges[0].dataset.urlHealthState, "healthy"); + assert.equal(cache.getCachedUrlHealth(v.id, v.url).status, "healthy"); +}); + +for (const timing of ["shared", "rejoin"]) { + test(`active primary error preserves ordered mirror for a ${timing} live owner`, async (t) => { + const f = fixture(t), + v = f.video(`active-${timing}`); + v.sources = [ + { url: "https://mirror.invalid/live-first.mp4" }, + { url: "https://mirror.invalid/unused-second.mp4" } + ]; + const old = f.feed([v]); + old.observer.show(); + await flush(); + if (timing === "rejoin") old.dispose(); + const live = f.feed([v]); + live.observer.show(); + await flush(); + if (timing === "shared") old.dispose(); + assert.equal(f.heads.length, 1); + assert.equal(f.promises.length, 1); + f.done(f.media[0], "error"); + await flush(); + assert.equal(f.heads.length, 2); + assert.equal(f.heads[1].source, v.sources[0].url); + assert.equal(f.media.length, 2); + f.done(f.media[1]); + await flush(); + assert.equal(f.heads.length, 2, "healthy first mirror must stop fallback"); + assert.equal(live.badges[0].dataset.urlHealthState, "healthy"); + assert.equal(old.badges[0].dataset.urlHealthState, "checking"); + assert.equal(f.controller.activeVideoProbes, 0); + const cached = f.feed([v]); + cached.observer.show(); + await flush(); + assert.equal(f.heads.length, 2); + assert.equal(cached.badges[0].dataset.urlHealthState, "healthy"); + }); +} + +test("active first mirror error after disposal does not start the next mirror", async (t) => { + const f = fixture(t), + v = f.video("later-mirror"); + v.sources = [ + { url: "https://mirror.invalid/first.mp4" }, + { url: "https://mirror.invalid/obsolete-second.mp4" } + ]; + const old = f.feed([v]); + old.observer.show(); + await flush(); + f.done(f.media[0], "error"); + await flush(); + assert.equal(f.heads.length, 2); + old.dispose(); + f.done(f.media[1], "error"); + await flush(); + assert.equal(f.heads.length, 2); + assert.equal(f.media.length, 2); + assert.equal(await f.promises[0], null); + assert.equal(cache.getCachedUrlHealth(v.id, v.url), null); + assert.equal(f.controller.activeVideoProbes, 0); + assert.equal(old.badges[0].dataset.urlHealthState, "checking"); +}); + +test("disposed active media timeout releases its slot without retry resources and later requests remain playable", async (t) => { + const f = fixture(t), + v = f.video("active-timeout"); + f.controller.constants = { + URL_PROBE_TIMEOUT_MS: cache.URL_PROBE_TIMEOUT_MS, + urlHealthConstants: cache.urlHealthConstants + }; + t.mock.timers.enable({ apis: ["setTimeout"] }); + const old = f.feed([v]); + old.observer.show(); + await flush(); + old.dispose(); + t.mock.timers.tick(cache.URL_PROBE_TIMEOUT_MS); + await flush(); + assert.equal(f.heads.length, 1); + assert.equal(f.media.length, 1); + assert.equal(f.controller.activeVideoProbes, 0); + assert.equal(f.media[0].getAttribute("src"), null); + assert.equal(await f.promises[0], null); + assert.equal(cache.getCachedUrlHealth(v.id, v.url), null); + assert.equal(old.badges[0].dataset.urlHealthState, "checking"); + const fresh = f.feed([v]); + fresh.observer.show(); + await flush(); + assert.equal(f.heads.length, 2); + assert.equal(f.media.length, 2); + f.done(f.media[1]); + await flush(); + assert.equal(fresh.badges[0].dataset.urlHealthState, "healthy"); + assert.equal(f.controller.activeVideoProbes, 0); +}); + +test("completed active failure without another candidate preserves its existing offline cache result", async (t) => { + const f = fixture(t), + v = f.video("final-active-failure"), + old = f.feed([v]); + old.observer.show(); + await flush(); + old.dispose(); + f.done(f.media[0], "error"); + await flush(); + assert.equal(f.heads.length, 1); + assert.equal(f.controller.activeVideoProbes, 0); + assert.equal(cache.getCachedUrlHealth(v.id, v.url).status, "offline"); + assert.equal(old.badges[0].dataset.urlHealthState, "checking"); +}); diff --git a/todo/TODO_2026-10-20_13-30-00_client-reliability-5.md b/todo/TODO_2026-10-20_13-30-00_client-reliability-5.md new file mode 100644 index 00000000..26eb7b22 --- /dev/null +++ b/todo/TODO_2026-10-20_13-30-00_client-reliability-5.md @@ -0,0 +1,8 @@ +# client-reliability-5 + +- [x] Merge rules/brief,check filedAt,claim. +- [x] Actual observer/controller/cache active success/timeout resource measurements. +- [x] Reproduce obsolete mirror HEAD1(expected0),assertion after cleanup naturally exits1. +- [x] Shared owner,rejoin and later same-source controls naturally exit0. +- [x] Report smallest scope/acceptance to dispatch. +- [ ] Dispatch independent assessment/routing before product/regression changes. diff --git a/todo/TODO_2026-10-20_13-36-00_client-reliability-5.md b/todo/TODO_2026-10-20_13-36-00_client-reliability-5.md new file mode 100644 index 00000000..1c279a80 --- /dev/null +++ b/todo/TODO_2026-10-20_13-36-00_client-reliability-5.md @@ -0,0 +1,7 @@ +# Item5 + +- [x] Reproduce/independent route. +- [x] Controller-only guard/six scenarios and six rejected mutants. +- [ ] Focused/lint/build results and committed merged-tree checks. +- [ ] Explicit full-suite slot and browser slot/QA coverage. +- [ ] Normal clean-env queue,durable status/ancestry/close/dispatch events. diff --git a/todo/TODO_2026-10-20_13-39-30_storage-cors.md b/todo/TODO_2026-10-20_13-39-30_storage-cors.md new file mode 100644 index 00000000..aaaf8682 --- /dev/null +++ b/todo/TODO_2026-10-20_13-39-30_storage-cors.md @@ -0,0 +1,7 @@ +# storage-cors-1 +- [x] Read rules/mailbox/memory/known issues/test integrity; claim current applicable item. +- [x] Trace callers and reproduce offline loss at bbc1be79. +- [x] Send demonstrated loss and proposed preservation semantics to dispatch. +- [ ] Obtain fix routing; implement bounded regression and product change only after routing. +- [ ] Run mutant/focused checks, coordinated full gates/build, commit/merge/normal queue and durable ancestry check. +- Blocked: awaiting dispatch routing. PR overlap check unavailable: curl exit6 DNS resolution. diff --git a/todo/TODO_2026-10-20_13-44-40_storage-cors.md b/todo/TODO_2026-10-20_13-44-40_storage-cors.md new file mode 100644 index 00000000..32c01380 --- /dev/null +++ b/todo/TODO_2026-10-20_13-44-40_storage-cors.md @@ -0,0 +1,7 @@ +# storage-cors-1 +- [x] Offline loss reproduced and dispatch independently confirmed; fix routed. +- [x] Preserve policy implementation2fb6a536; merged unstable0919be93. +- [x]9 regression scenarios,10 restored mutants,focused3/3 files/13 scenarios; lint11/11 and build pass on merged tree. +- [ ] Dispatch explicit full-suite/submit grant (client ahead). +- [ ] Full normal gate; submit/durable landing+unstable ancestry; close backlog; dispatch final evidence. +- Remaining scope: actual cloud/API permissions/serialization and concurrent policy writers unverified; no browser needed. diff --git a/todo/TODO_2026-10-20_13-51-00_client-reliability-5-ready.md b/todo/TODO_2026-10-20_13-51-00_client-reliability-5-ready.md new file mode 100644 index 00000000..ac2d44c9 --- /dev/null +++ b/todo/TODO_2026-10-20_13-51-00_client-reliability-5-ready.md @@ -0,0 +1,7 @@ +# Item5 + +- [x] Controller pre-candidate guard,six new regressions/six restored mutants. +- [x] Focused28,lifecycle18,full427files,lint11,build/artifact,native Chromium+Firefox controlled smoke. +- [x] Respect/measured full/browser wait10m57s;browser released. +- [ ] Normal clean-env product queue,durable landed status/unstable ancestry. +- [ ] Close metadata,dispatch landing/final/slot-release. diff --git a/todo/TODO_2026-10-20_13-58-00_client-reliability-5-landed.md b/todo/TODO_2026-10-20_13-58-00_client-reliability-5-landed.md new file mode 100644 index 00000000..a415a35e --- /dev/null +++ b/todo/TODO_2026-10-20_13-58-00_client-reliability-5-landed.md @@ -0,0 +1,7 @@ +# Item5 + +- [x] Reproduce/route/controller6line guard+six regressions/six mutants. +- [x] Focused28/lifecycle18/full427files/lint11/build/artifact/browser smoke. +- [x] Normal product landing0bd0b1c3/durable status/ancestry/backlog close. +- [x] Product event and expensive/browser slot release. +- [ ] Docs-only closure landing/final dispatch event,then idle. diff --git a/todo/TODO_2026-10-20_14-10-00_storage-cors.md b/todo/TODO_2026-10-20_14-10-00_storage-cors.md new file mode 100644 index 00000000..f741c824 --- /dev/null +++ b/todo/TODO_2026-10-20_14-10-00_storage-cors.md @@ -0,0 +1,7 @@ +# storage-cors-1 complete +- [x] Offline loss reproduced; independent dispatch confirmation/routing. +- [x] Policy preservation/current-default coverage fix,9 regressions,10 restored mutants. +- [x] Final merged focused13scenarios/3files,lint11/build,full428files pass. +- [x] Normal gate exit0/durable landed d3f4c215/ancestry exit0; backlog closed; full slot released. +- [x] Explicit cloud/concurrent-writer limitations and trial timings recorded. +- Remaining: no cloud/provider IAM/concurrent-writer qualification; no further task started.