mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
test: protect actual profile history removal wiring (client-reliability-6)
This commit is contained in:
1 parent
5de86e7578
commit
9afe48a5b5
8 files changed
+387
-1
No files matched your search
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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(`<!doctype html><body>${markup}</body>`, {
|
||||
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, []);
|
||||
});
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user