From 9afe48a5b534ac273b7ad321ccffc5b216711645 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Tue, 20 Oct 2026 11:13:42 -0400 Subject: [PATCH] test: protect actual profile history removal wiring (client-reliability-6) --- ...15-12-00_client-reliability-6-retention.md | 3 + ...15-12-00_client-reliability-6-retention.md | 7 + docs/TODO.md | 2 + docs/agents/backlog/client-reliability-6.json | 2 +- .../TEST_INTEGRITY_client-reliability-6.md | 42 +++ ...-20_15-13-30_client-reliability-6-ready.md | 10 + tests/profile-history-removal-wiring.test.mjs | 315 ++++++++++++++++++ ...15-12-00_client-reliability-6-retention.md | 7 + 8 files changed, 387 insertions(+), 1 deletion(-) create mode 100644 context/CONTEXT_2026-10-20_15-12-00_client-reliability-6-retention.md create mode 100644 decisions/DECISIONS_2026-10-20_15-12-00_client-reliability-6-retention.md create mode 100644 test_logs/TEST_INTEGRITY_client-reliability-6.md create mode 100644 test_logs/TEST_LOG_2026-10-20_15-13-30_client-reliability-6-ready.md create mode 100644 tests/profile-history-removal-wiring.test.mjs create mode 100644 todo/TODO_2026-10-20_15-12-00_client-reliability-6-retention.md diff --git a/context/CONTEXT_2026-10-20_15-12-00_client-reliability-6-retention.md b/context/CONTEXT_2026-10-20_15-12-00_client-reliability-6-retention.md new file mode 100644 index 00000000..55a0c77a --- /dev/null +++ b/context/CONTEXT_2026-10-20_15-12-00_client-reliability-6-retention.md @@ -0,0 +1,3 @@ +# Item6 retention + +New tests/profile-history-removal-wiring.test.mjs retains3 actual profile/factory/DOM/controller/service scenarios;five runtime mutants fail with0cancelled. Product and existing suites unchanged. Focused/lint running;no full/browser/build until explicit resource grant. Unique integrity in test_logs/TEST_INTEGRITY_client-reliability-6.md. No real signer/relay/fullapp/reload claim. diff --git a/decisions/DECISIONS_2026-10-20_15-12-00_client-reliability-6-retention.md b/decisions/DECISIONS_2026-10-20_15-12-00_client-reliability-6-retention.md new file mode 100644 index 00000000..dfa45ae3 --- /dev/null +++ b/decisions/DECISIONS_2026-10-20_15-12-00_client-reliability-6-retention.md @@ -0,0 +1,7 @@ +# Item6 test design + +- Use actual profile config/factory and DOM click with original renderer removal/controller/local service. Observe actual removal promise only for deterministic completion. Required unrelated constructor services and read/metadata boundary are controlled; no fake renderer/remove implementation. +- Local and synced removal include final-item case and renderer recreation; local Clear is a separate scenario with real queue reset. No synced Clear assumption. +- Synced IO twin respects replace vs merge, so missing replace is observable rather than rubber-stamped. Assert all published payload/options plus publication occurrence, without requiring duplicate implementation calls. +- Isolated runtime mutants exercise historical config no-op,clear no-op,drop-replace,wrong actor and final-item no-op. Explicit teardown runs on assertion failure and restores queue/app/client/globals. +- No product/existing suite edits; focused/lint only until dispatch allocates full/normal slot. Build/browser remain unverified/outside current grant for test-only retention. diff --git a/docs/TODO.md b/docs/TODO.md index f0a0184f..ffd33e74 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -37,6 +37,8 @@ ## client-reliability +- 2026-10-20: item6 test-only retention ready:new real-profile wiring3/3,focused5files/38scenarios,5 isolated runtime mutants failnaturally/0cancelled,lint11pass. Local remove/lastitem/clear/recreation andfake-synced reduced+empty replacement covered;historicalno-op rejected. No product/existing suite edits;full/normal slot pendingexplicit grant,full/browser/build unrun;real signers/relays/fullapp/syncedClear unverified. Unique integrity/evidence in test_logs/*client-reliability-6*. + - 2026-10-20: item6 audit atd88716e3(profile paths unchanged throughbbf57388):real local profile remove/clear and fake-synced remove/recreation pass,0network;historical runtime no-op failsboth aftercleanup but existingprofile27/27 stillpasses;lowerlayers3files/8pass. Classify missing real-wiring regression,notcurrentproductfailure. Proposed test-only focused file+uniqueintegrity;await routing,QA expensive slot respected. Evidence test_logs/TEST_LOG_2026-10-20_15-04-00_client-reliability-6.md. No product/test changes or live signer/relay/fullapp proof. - 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. diff --git a/docs/agents/backlog/client-reliability-6.json b/docs/agents/backlog/client-reliability-6.json index 87676365..1f434608 100644 --- a/docs/agents/backlog/client-reliability-6.json +++ b/docs/agents/backlog/client-reliability-6.json @@ -6,5 +6,5 @@ "blockedOn": null, "addedOn": "2026-10-20", "filedAt": "7f99c0456931c917082d6a6680c79655221dcc82", - "evidence": "Audit atd88716e3,profile paths unchanged throughbbf57388:actual local+fake-synced wiring works;historical no-op fails2diagnostics but existingprofile27survives,lowerlayer8pass. Missing regression,propose test-only real-profile file;await routing. Evidence test_logs/TEST_LOG_2026-10-20_15-04-00_client-reliability-6.md" + "evidence": "New real-profile history test3/3;focused5files38scenarios,5 runtime mutants naturalexit1/0cancelled,lint11pass. Test-only,no product/existing suite edits;full/normal slot pending. Unique integrity and ready note test_logs/TEST_LOG_2026-10-20_15-13-30_client-reliability-6-ready.md" } diff --git a/test_logs/TEST_INTEGRITY_client-reliability-6.md b/test_logs/TEST_INTEGRITY_client-reliability-6.md new file mode 100644 index 00000000..b175a9a6 --- /dev/null +++ b/test_logs/TEST_INTEGRITY_client-reliability-6.md @@ -0,0 +1,42 @@ +# Item6 retained profile history wiring + +```yaml +test_integrity_note: + change_type: ["new_tests"] + scenarios: + - id: SCN-profile-local-remove-real-wiring + given: "local-only actor has two real service queue entries rendered by the actual profile history factory" + when: "actual profile DOM remove controls delete one item then the last item, recreating the renderer between steps" + then: "backing queue and DOM remain reduced then empty, no publish IO or historical onHistoryReady callback runs" + - id: SCN-profile-local-clear-real-wiring + given: "actual profile history contains two local entries" + when: "actual Clear local history button is clicked and renderer recreated" + then: "real local queue and DOM remain empty with zero publish IO" + - id: SCN-profile-synced-remove-real-wiring + given: "synced actor renders two entries through real profile/default removal/controller/service with fake read/publish IO" + when: "profile DOM removes an item then the last, with renderer recreation" + then: "publish IO receives reduced then empty replacements with correct actor; recreation never resurrects removed entries" + observable_outcomes: + - "actual DOM removal controls and card state, service loadLatest/queue persistence, renderer recreation" + - "fake external publish payload, replace and actor options; zero local publishes and network attempts" + determinism_controls: + - "actual component markup, ProfileModalController config/factory, renderer, removal controller and local service" + - "fixed fake pointers, unique actors, isolated jsdom localStorage, controlled synced read/publish twin honoring merge vs replace" + - "task-turn drain and actual removal promise observation; no arbitrary sleeps/retries, real signer/relay/network" + - "teardown destroys renderer/subscriptions, resets actor service state, restores application/client/globals and closes DOM, even on failure" + anti_cheat_rationale: + prevents: + - "historical no-op removal hidden by optimistic DOM changes" + - "fake renderer or source-string-only configuration assertion" + - "local clear without backing persistence or missing synced replace/actor options" + - "last-item no-op and publish twin that always replaces regardless of options" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +Only a new test file is retained; no existing suite/product assertions changed. Publish boundary assertions require at least one appropriate publication and verify every emitted payload/options, preserving behavior without locking in today's duplicate publication count. Clear coverage is local-only; no synced Clear-local/relay-list-clear equivalence claim. + +Fault injections operate only in isolated CLI process memory, never edit product files. `python3 /tmp/item6-retention-mutants.py`: all five naturally exit1 with named assertion failures,zero cancelled; process exit restores injected prototypes and fixture after-hooks release queue/app/client/globals on failed assertions. Historical-noop kills local+synced real wiring; clear-noop (one dropped reset then cleanup forwards normally) kills local Clear; drop-replace kills local+synced; wrong-actor kills synced and explicitly releases injected actor state; ignore-last kills local+synced final removal. Logs `/tmp/item6-retained-mutant-*.log`,summary `/tmp/item6-retained-mutants.json`. + +Independent audit showed existing profile27/27 survive the historical no-op while both actual wiring scratch variants fail after cleanup. New retained local+synced tests reject that same injected override. Actual application load/show/reload,real signing/encryption/relay persistence and manual user confirmation remain unverified. diff --git a/test_logs/TEST_LOG_2026-10-20_15-13-30_client-reliability-6-ready.md b/test_logs/TEST_LOG_2026-10-20_15-13-30_client-reliability-6-ready.md new file mode 100644 index 00000000..47218933 --- /dev/null +++ b/test_logs/TEST_LOG_2026-10-20_15-13-30_client-reliability-6-ready.md @@ -0,0 +1,10 @@ +# Item6 test-only ready + +- Retained `tests/profile-history-removal-wiring.test.mjs` only; no product/existing suite changes. Test-only routing merged from verifiedac99ae9a after audit notes8356f27d. +- `env -u NODE_TEST_CONTEXT NODE_ENV=test node tests/profile-history-removal-wiring.test.mjs`: natural exit0,3/3pass,0failed/cancelled/skipped,1388.42ms final readable run (`/tmp/item6-retention-final.log`). Real local removal through final item+recreation,actual local Clear+recreation,and synced reduced+empty replacements through fake IO. +- `python3 /tmp/item6-retention-mutants.py`: five/five isolated runtime faults each naturally exit1,0cancelled,cleanup after-hooks still pass. Historical no-op kills both real removal scenarios;skip clear/drop replace/wrong actor/skip last cover all new scenarios. No source file mutated; unique mapping `test_logs/TEST_INTEGRITY_client-reliability-6.md`. +- `env -u NODE_TEST_CONTEXT node scripts/run-unit-tests.mjs profile-history-removal-wiring profile-modal-controller watch-history-local-remove watch-history-controller-local-remove watch-history-remove-snapshot`: natural exit0,5files/38scenarios (3+27+8),log `/tmp/item6-retention-focused.log`. +- `env -u NODE_TEST_CONTEXT npm run lint` with approved git subprocess access: exit0,11stages (`/tmp/item6-retention-lint.log`). +- Globals/app/client/local actor queues restored and DOM/subscriptions closed even on named failing mutations;network guards cover global and window fetch/WebSocket. No arbitrary sleeps/retries/live signer/relay/credentials. +- Full/browser/build/normal queue have not been launched;await explicit full/normal slot after QA bounded browser work. Build/browser have no fresh qualification from this test-only change. No product behavior fix claimed;real signing/encryption/relay persistence/full app modal lifecycle/reload/manual confirmation and synced Clear remain unverified. +- Retention data is behavior-based,publish twin honors merge vs replace;all emitted replacement actor/payload/options checked without locking in duplicate call count. Existing profile27 passes historical no-op in audit;new tests reject it. diff --git a/tests/profile-history-removal-wiring.test.mjs b/tests/profile-history-removal-wiring.test.mjs new file mode 100644 index 00000000..447bffb4 --- /dev/null +++ b/tests/profile-history-removal-wiring.test.mjs @@ -0,0 +1,315 @@ +import "./test-helpers/setup-localstorage.mjs"; +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { JSDOM } from "jsdom"; +import ProfileModalController from "../js/ui/profileModalController.js"; +import { createWatchHistoryRenderer } from "../js/historyView.js"; +import WatchHistoryController from "../js/ui/watchHistoryController.js"; +import watchHistoryService from "../js/watchHistoryService.js"; +import { nostrClient } from "../js/nostrClientFacade.js"; +import { getApplication, setApplication } from "../js/applicationContext.js"; + +const markup = await readFile( + new URL("../components/profile-modal.html", import.meta.url), + "utf8" +); +const flush = () => new Promise((resolve) => setImmediate(resolve)); +let actorSequence = 100; + +async function fixture(t, { synced = false } = {}) { + const dom = new JSDOM(`${markup}`, { + url: "https://example.invalid" + }); + const globals = [ + "window", + "document", + "HTMLElement", + "HTMLInputElement", + "HTMLImageElement", + "HTMLButtonElement", + "Node", + "Event", + "CustomEvent", + "MutationObserver", + "localStorage", + "fetch", + "WebSocket", + "IntersectionObserver" + ]; + const savedGlobals = globals.map((key) => [ + key, + Object.getOwnPropertyDescriptor(globalThis, key) + ]); + for (const key of globals.slice(0, 10)) { + globalThis[key] = + key === "window" + ? dom.window + : key === "document" + ? dom.window.document + : dom.window[key]; + } + globalThis.localStorage = dom.window.localStorage; + globalThis.IntersectionObserver = class { + observe() {} + unobserve() {} + disconnect() {} + }; + let networkAttempts = 0; + globalThis.fetch = dom.window.fetch = () => { + networkAttempts++; + throw new Error("Unexpected history fixture network request"); + }; + globalThis.WebSocket = dom.window.WebSocket = class { + constructor() { + networkAttempts++; + throw new Error("Unexpected history fixture socket"); + } + }; + + const actor = (++actorSequence).toString(16).padStart(64, "0"); + const clientKeys = [ + "pubkey", + "sessionActor", + "updateWatchHistoryList", + "resolveWatchHistory", + "getWatchHistoryFingerprint" + ]; + const savedClient = clientKeys.map((key) => [key, nostrClient[key]]); + const previousApp = getApplication(); + const seed = [ + { type: "e", value: "profile-first", watchedAt: 1700000000 }, + { type: "e", value: "profile-keep", watchedAt: 1700000001 } + ]; + let publishedItems = structuredClone(seed); + const publishes = [], + errors = [], + noops = []; + nostrClient.pubkey = synced ? actor : ""; + nostrClient.sessionActor = synced ? null : { pubkey: actor }; + // Publish/read IO twin honors merge vs replace so a lost replace flag cannot + // look successful merely because the fake publisher always replaces its list. + nostrClient.updateWatchHistoryList = async (items, options = {}) => { + publishes.push({ items: structuredClone(items), options: { ...options } }); + publishedItems = + options.replace === true + ? structuredClone(items) + : [ + ...new Map( + [...publishedItems, ...items].map((item) => [ + `${item.type}:${item.value}`, + structuredClone(item) + ]) + ).values() + ]; + return { ok: true, items: structuredClone(publishedItems) }; + }; + nostrClient.resolveWatchHistory = async () => structuredClone(publishedItems); + nostrClient.getWatchHistoryFingerprint = async () => + JSON.stringify(publishedItems); + const historyController = new WatchHistoryController({ + watchHistoryService, + nostrClient, + getActivePubkey: () => actor, + showError: (message) => errors.push(message) + }); + setApplication({ + watchHistoryController: historyController, + showSuccess() {}, + showError: (message) => errors.push(message), + isAuthorBlocked: () => false, + getHashtagPreferences: () => ({ interests: [], disinterests: [] }) + }); + let renderer; + const profile = new ProfileModalController({ + services: { + relayManager: {}, + userBlocks: {}, + accessControl: {}, + nostrClient + }, + callbacks: { + onHistoryReady(payload) { + noops.push(payload); + return null; + } + }, + createWatchHistoryRenderer: (config) => + createWatchHistoryRenderer({ + ...config, + getActor: () => actor, + fetchHistory: async () => ({ + items: await watchHistoryService.loadLatest(actor, { + allowStale: false, + forceRefresh: true + }) + }) + }) + }); + profile.profileModalPanel = document.body; + profile.profileModalRoot = document.body; + t.after(() => { + try { + renderer?.destroy(); + watchHistoryService.resetProgress(actor); + assert.deepEqual( + watchHistoryService.getQueuedPointers(actor), + [], + "fixture must release its local queue" + ); + } finally { + setApplication(previousApp); + for (const [key, value] of savedClient) nostrClient[key] = value; + dom.window.close(); + for (const [key, descriptor] of savedGlobals) { + if (descriptor) Object.defineProperty(globalThis, key, descriptor); + else delete globalThis[key]; + } + } + assert.equal( + networkAttempts, + 0, + "no network or relay activity is permitted" + ); + }); + if (!synced) + await watchHistoryService.snapshot(seed, { actor, replace: true }); + renderer = profile.ensureProfileHistoryRenderer(); + await renderer.init({ actor, force: true }); + const values = () => + renderer + .getState() + .items.map((item) => item.pointer.value) + .sort(); + const persisted = async () => + ( + await watchHistoryService.loadLatest(actor, { + allowStale: false, + forceRefresh: true + }) + ) + .map((item) => item.value) + .sort(); + const grid = document.querySelector("#profileHistoryGrid"); + assert.deepEqual(values(), ["profile-first", "profile-keep"]); + assert.equal(grid.querySelectorAll("[data-history-card]").length, 2); + + async function remove(value) { + // Observe completion of the actual method invoked by the real DOM listener; + // no remove/controller/service implementation is replaced by this wrapper. + let pending; + const original = renderer.handleRemove.bind(renderer); + renderer.handleRemove = (...args) => { + pending = original(...args); + return pending; + }; + try { + const trigger = grid.querySelector( + `[data-history-action="remove"][data-pointer-key="e:${value}"]` + ); + assert.ok(trigger, "profile pane must render an actual removal control"); + trigger.click(); + assert.ok( + pending instanceof Promise, + "DOM click must invoke renderer removal" + ); + await pending; + await flush(); + } finally { + renderer.handleRemove = original; + } + } + async function recreate() { + renderer.destroy(); + profile.profileHistoryRenderer = null; + renderer = profile.ensureProfileHistoryRenderer(); + await renderer.init({ actor, force: true }); + await flush(); + } + return { + actor, + profile, + values, + persisted, + remove, + recreate, + grid, + publishes, + errors, + noops + }; +} + +test("real profile local removal persists through recreation and removing the last item", async (t) => { + const f = await fixture(t); + await f.remove("profile-first"); + assert.deepEqual(await f.persisted(), ["profile-keep"]); + assert.deepEqual(f.values(), ["profile-keep"]); + assert.equal( + f.grid.querySelector('[data-pointer-key="e:profile-first"]'), + null + ); + await f.recreate(); + assert.deepEqual(f.values(), ["profile-keep"]); + await f.remove("profile-keep"); + assert.deepEqual(await f.persisted(), []); + await f.recreate(); + assert.deepEqual(f.values(), []); + assert.equal(f.grid.querySelectorAll("[data-history-card]").length, 0); + assert.equal(f.publishes.length, 0, "local-only removal must never publish"); + assert.equal( + f.noops.length, + 0, + "historical onHistoryReady no-op must not handle removal" + ); + assert.deepEqual(f.errors, []); +}); + +test("real profile Clear local history empties the queue and remains empty after recreation", async (t) => { + const f = await fixture(t); + const clear = document.querySelector("#profileHistoryClear"); + assert.ok(clear); + clear.click(); + await flush(); + assert.deepEqual(await f.persisted(), []); + assert.deepEqual(f.values(), []); + await f.recreate(); + assert.deepEqual(f.values(), []); + assert.equal(f.grid.querySelectorAll("[data-history-card]").length, 0); + assert.equal(f.publishes.length, 0); + assert.deepEqual(f.errors, []); +}); + +test("real profile synced removal publishes reduced replacement and empty final replacement through fake IO", async (t) => { + const f = await fixture(t, { synced: true }); + await f.remove("profile-first"); + assert.deepEqual(await f.persisted(), ["profile-keep"]); + assert.ok(f.publishes.length > 0, "synced removal must reach publish IO"); + for (const { items, options } of f.publishes) { + assert.deepEqual( + items.map((item) => item.value), + ["profile-keep"] + ); + assert.equal(options.replace, true); + assert.equal(options.actorPubkey, f.actor); + } + const firstPublishCount = f.publishes.length; + await f.recreate(); + assert.deepEqual(f.values(), ["profile-keep"]); + await f.remove("profile-keep"); + assert.deepEqual(await f.persisted(), []); + assert.ok( + f.publishes.length > firstPublishCount, + "last removal must publish an empty replacement" + ); + for (const { items, options } of f.publishes.slice(firstPublishCount)) { + assert.deepEqual(items, []); + assert.equal(options.replace, true); + assert.equal(options.actorPubkey, f.actor); + } + await f.recreate(); + assert.deepEqual(f.values(), []); + assert.equal(f.grid.querySelectorAll("[data-history-card]").length, 0); + assert.equal(f.noops.length, 0); + assert.deepEqual(f.errors, []); +}); diff --git a/todo/TODO_2026-10-20_15-12-00_client-reliability-6-retention.md b/todo/TODO_2026-10-20_15-12-00_client-reliability-6-retention.md new file mode 100644 index 00000000..806ad023 --- /dev/null +++ b/todo/TODO_2026-10-20_15-12-00_client-reliability-6-retention.md @@ -0,0 +1,7 @@ +# Item6 retention + +- [x] Independent audit/routing and3 actual-wiring scenarios. +- [x] Five isolated failing runtime mutants,cleanup preserved. +- [ ] Focused/lint final results,ready commit and slot request. +- [ ] Verified unstable merge,full/normal gate slot,durable landing/ancestry. +- [ ] Close backlog and final dispatch report.