diff --git a/context/CONTEXT_2026-02-17_16-33-00.md b/context/CONTEXT_2026-02-17_16-33-00.md new file mode 100644 index 00000000..52e9d7cb --- /dev/null +++ b/context/CONTEXT_2026-02-17_16-33-00.md @@ -0,0 +1,8 @@ +# Daily Test Audit Context + +Goal: Run daily test audit to detect flaky tests, coverage gaps, and suspicious patterns. +Scope: All unit tests in `tests/`. +Definition of Done: +- `test-audit-report-YYYY-MM-DD.md` generated. +- `test-audit/` artifacts populated. +- Tests run at least 3 times. diff --git a/decisions/DECISIONS_2026-02-17_16-33-00.md b/decisions/DECISIONS_2026-02-17_16-33-00.md new file mode 100644 index 00000000..73f40572 --- /dev/null +++ b/decisions/DECISIONS_2026-02-17_16-33-00.md @@ -0,0 +1,4 @@ +# Decisions Log + +- Using `npx c8` for coverage if available, falling back to basic test run. +- Using simple grep for static analysis to avoid complex parsing in script. diff --git a/docs/agents/task-logs/daily/2026-02-17_16-33-00_test-audit-agent_failed.md b/docs/agents/task-logs/daily/2026-02-17_16-33-00_test-audit-agent_failed.md new file mode 100644 index 00000000..25ee6758 --- /dev/null +++ b/docs/agents/task-logs/daily/2026-02-17_16-33-00_test-audit-agent_failed.md @@ -0,0 +1,16 @@ +# Daily Test Audit Agent: Failed + +**Agent:** `test-audit-agent` +**Date:** 2026-02-17 +**Status:** Failed (Timeout) + +## Summary +The daily test audit was attempted but the full unit test suite timed out after 400s. +Partial coverage data was collected. + +## Key Findings +- **Critical Slowness:** `tests/profile-modal-controller.test.mjs` takes >60s to run, causing the suite to hang/timeout. +- **Coverage:** Partial coverage data available in `test-audit/coverage-run.log`. +- **Static Analysis:** Found numerous `setTimeout` usages. + +See `test-audit/test-audit-report-2026-02-17.md` for details. diff --git a/test-audit/console_logs.txt b/test-audit/console_logs.txt new file mode 100644 index 00000000..1472e122 --- /dev/null +++ b/test-audit/console_logs.txt @@ -0,0 +1,40 @@ +tests/subscriptions-feed.test.mjs:console.log("subscriptions-feed tests passed"); +tests/webtorrent-regression.test.mjs: console.log("Failed result:", result); +tests/nostr-publish-rejection.test.mjs: console.log("nostr publish rejection tests passed"); +tests/zap-shared-state.test.mjs:console.log("zap-shared-state tests passed"); +tests/view-counter.test.mjs:console.log("View counter tests completed successfully."); +tests/watch-history-feed.test.mjs:console.log("watch-history-feed.test.mjs completed successfully."); +tests/app-batch-fetch-profiles.test.mjs: // console.log("pool.list called with relays:", relays); +tests/app-batch-fetch-profiles.test.mjs: // console.log("poolListCalls.length:", poolListCalls.length); +tests/app-batch-fetch-profiles.test.mjs: // console.log("nostrClient.relays:", nostrClient.relays); +tests/feed-engine.test.mjs:console.log("All feed engine tests passed"); +tests/utils/domUtils.test.mjs: inlineScript.textContent = "console.log('hello')"; +tests/utils/domUtils.test.mjs: assert.equal(container.querySelector("script").textContent, "console.log('hello')"); +tests/nostr-rebroadcast-guard.test.mjs:console.log("nostr rebroadcast guard tests passed"); +tests/nostr-boost-actions.test.mjs:console.log("nostr boost action tests passed"); +tests/zap-split.test.mjs:console.log("zap-split tests passed"); +tests/watch-history.test.mjs: console.log("Running normalizeActorKey short-circuit test..."); +tests/watch-history.test.mjs: console.log("Running normalizeActorKey manual fallback test..."); +tests/watch-history.test.mjs: console.log("Running watch history snapshot inflight queue retention test..."); +tests/watch-history.test.mjs: console.log("Running watch history snapshot merge test..."); +tests/watch-history.test.mjs: console.log("Running watch history card playback metadata test..."); +tests/watch-history.test.mjs: console.log("Running watch history stale cache refresh test..."); +tests/watch-history.test.mjs: console.log("Running watch history local fallback test..."); +tests/watch-history.test.mjs: console.log("Running watch history renderer feed registration fallback test..."); +tests/watch-history.test.mjs: console.log("Running watch history logged-in sync override test..."); +tests/watch-history.test.mjs: console.log("Running watch history app login fallback test..."); +tests/watch-history.test.mjs: console.log("Running watch history feed hydration test..."); +tests/watch-history.test.mjs:console.log("watch-history.test.mjs completed successfully"); +tests/discussion-count-service.test.mjs:console.log("discussion-count-service tests passed"); +tests/nostr-view-events.test.mjs:console.log("nostr view events tests passed"); +tests/admin-list-store.test.mjs:console.log("admin-list-store tests passed"); +tests/nostr-count-fallback.test.mjs:console.log("nostr COUNT fallback tests completed successfully."); +tests/nostr-view-event-bindings.test.mjs:console.log("nostr-view-event-bindings tests passed"); +tests/ui/components/debug_hashtag_strip_helper.test.mjs: console.log(`DEBUG: window.addEventListener('${event}') called`); +tests/ui/components/debug_hashtag_strip_helper.test.mjs: console.log("Global ResizeObserver type:", typeof globalThis.ResizeObserver); +tests/ui/components/debug_hashtag_strip_helper.test.mjs: console.log("Helper window ResizeObserver:", typeof helper.window.ResizeObserver); +tests/ui/uploadModal-reset.test.mjs: global.alert = (msg) => { console.log("ALERT CALLED:", msg); }; +tests/ui/profileModalController-addProfile.test.mjs: showSuccess: (msg) => { console.log("Success:", msg); }, +tests/performance/resolvePostedAt.test.mjs: console.log(`pool.list calls: ${listCalls}`); +tests/performance/resolvePostedAt.test.mjs: console.log(`pool.get calls: ${getCalls}`); +tests/nostr-delete-flow.test.mjs:console.log("nostr delete flow tests passed"); diff --git a/test-audit/network_calls.txt b/test-audit/network_calls.txt new file mode 100644 index 00000000..709d9e6c --- /dev/null +++ b/test-audit/network_calls.txt @@ -0,0 +1,226 @@ +tests/video-modal-zap.test.mjs:if (typeof globalThis.fetch === "undefined") { +tests/video-modal-zap.test.mjs: globalThis.fetch = async () => { +tests/video-modal-zap.test.mjs: throw new Error("Unexpected fetch call in tests."); +tests/video-modal-zap.test.mjs: fetchedAt: Date.now(), +tests/subscriptions-feed.test.mjs: throw new Error("should not fetch active videos when authors missing"); +tests/video-modal-accessibility.test.mjs: const originalFetch = globalThis.fetch; +tests/video-modal-accessibility.test.mjs: globalThis.fetch = async (resource, init) => { +tests/video-modal-accessibility.test.mjs: throw new Error(`Unexpected fetch request in tests: ${String(url || resource)}`); +tests/video-modal-accessibility.test.mjs: globalThis.fetch = originalFetch; +tests/video-modal-accessibility.test.mjs: delete globalThis.fetch; +tests/view-counter.test.mjs: "hydration should fetch fresh data from relays even if cache was preserved" +tests/view-counter.test.mjs: "hydrateVideoHistory should fetch the root event when it is missing locally" +tests/view-counter.test.mjs: const fetchedRoot = history.find((entry) => entry.id === rootId); +tests/view-counter.test.mjs: assert.ok(fetchedRoot, "history should include the fetched root event"); +tests/view-counter.test.mjs: fetchedRoot.created_at, +tests/subscriptions-manager.test.mjs: const originalFetchIncremental = nostrClient.fetchListIncrementally; +tests/subscriptions-manager.test.mjs: nostrClient.fetchListIncrementally = async () => [event]; +tests/subscriptions-manager.test.mjs: nostrClient.fetchListIncrementally = originalFetchIncremental; +tests/nostr/client.test.mjs: describe("fetchListIncrementally", () => { +tests/nostr/client.test.mjs: it("should fetch events and deduplicate results", async () => { +tests/nostr/client.test.mjs: const events = await client.fetchListIncrementally({ +tests/nostr/client.test.mjs: await client.fetchListIncrementally({ +tests/nostr/client.test.mjs: await client.fetchListIncrementally({ +tests/nostr/client.test.mjs: it("should force full fetch if since is 0", async () => { +tests/nostr/client.test.mjs: await client.fetchListIncrementally({ +tests/nostr/client.test.mjs: since: 0 // Force full fetch +tests/nostr/client.test.mjs: describe("fetchVideos", () => { +tests/nostr/client.test.mjs: const result = await client.fetchVideos({ limit: 5 }); +tests/nostr/client.test.mjs: // Ensure we force full fetch by default +tests/nostr/client.test.mjs: await client.fetchVideos({ since: 12345 }); +tests/nostr/client.test.mjs: // and focus on `fetch`/`subscribe` which use `this.pool`. +tests/nostr/watchHistory.test.js: fetchWatchHistory, +tests/nostr/watchHistory.test.js:test("fetchWatchHistory prefers decrypted chunk payloads when nip04 decrypt succeeds", async () => { +tests/nostr/watchHistory.test.js: const result = await fetchWatchHistory(manager, actorHex, { forceRefresh: true }); +tests/nostr/watchHistory.test.js:test("fetchWatchHistory falls back to pointer payload when nip04 decrypt fails", async () => { +tests/nostr/watchHistory.test.js: const result = await fetchWatchHistory(manager, actorHex, { forceRefresh: true }); +tests/nostr/nip71.test.js:test("populateNip71MetadataForVideos fetches missing records once", async () => { +tests/nostr/nip71.test.js: let fetchCalls = 0; +tests/nostr/nip71.test.js: const fetchMetadata = async (map, pointerValues) => { +tests/nostr/nip71.test.js: fetchCalls += 1; +tests/nostr/nip71.test.js: fetchMetadata, +tests/nostr/nip71.test.js: assert.equal(fetchCalls, 1); +tests/nostr/nip71.test.js: assert.ok(cacheEntry?.fetchedPointers.has(pointerValue)); +tests/nostr/nip71.test.js: fetchMetadata, +tests/nostr/nip71.test.js: assert.equal(fetchCalls, 1, "cached pointer should skip subsequent fetches"); +tests/torrent/service-worker-fallback-message.test.mjs: "Failed to register ServiceWorker: TypeError: Failed to fetch" +tests/hashtag-preferences.test.mjs: // Note: fetchListIncrementally calls pool.list separately for each kind +tests/hashtag-preferences.test.mjs: const originalFetchIncremental = nostrClient.fetchListIncrementally; +tests/hashtag-preferences.test.mjs: let fetchCalls = 0; +tests/hashtag-preferences.test.mjs: fetchCalls = 0; +tests/hashtag-preferences.test.mjs: nostrClient.fetchListIncrementally = async () => { +tests/hashtag-preferences.test.mjs: fetchCalls += 1; +tests/hashtag-preferences.test.mjs: return fetchCalls <= 4 ? [event] : []; +tests/hashtag-preferences.test.mjs: nostrClient.fetchListIncrementally = originalFetchIncremental; +tests/app-batch-fetch-profiles.test.mjs: // Should fetch despite cache presence +tests/app-batch-fetch-profiles.test.mjs: "should include cached pubkey in fetch list" +tests/nostr-rebroadcast-guard.test.mjs: fetchRawEventById: async () => null, +tests/nostr-boost-actions.test.mjs: fetchRawEventById: async (id, { relays: requestedRelays } = {}) => { +tests/nostr-boost-actions.test.mjs: client.fetchRawEventById = async () => null; +tests/modal-accessibility.test.mjs: const originalFetch = globalThis.fetch; +tests/modal-accessibility.test.mjs: globalThis.fetch = async (resource) => { +tests/modal-accessibility.test.mjs: throw new Error(`Unexpected fetch request: ${key}`); +tests/modal-accessibility.test.mjs: globalThis.fetch = originalFetch; +tests/zap-split.test.mjs:globalThis.fetch = async () => { +tests/zap-split.test.mjs: throw new Error("Unexpected fetch call during zap split tests."); +tests/zap-split.test.mjs: async fetchPayServiceData(url) { +tests/zap-split.test.mjs: const fetcher = async (url) => { +tests/zap-split.test.mjs: fetcher, +tests/zap-split.test.mjs: assert.equal(requestedUrls.length, 1, "should call fetcher once"); +tests/zap-split.test.mjs: assert.equal(listCalls, 1, "should fetch platform metadata once the pool is ready"); +tests/profile-modal-controller.test.mjs: global.fetch = async (resource) => { +tests/profile-modal-controller.test.mjs: throw new Error(`Unexpected fetch: ${resource}`); +tests/profile-modal-controller.test.mjs: delete global.fetch; +tests/watch-history.test.mjs: const snapshotId = "extension-fetch"; +tests/watch-history.test.mjs: const result = await nostrClient.fetchWatchHistory(actorInput, { +tests/watch-history.test.mjs: const originalFetch = nostrClient.fetchWatchHistory; +tests/watch-history.test.mjs: const originalManagerFetch = nostrClient.watchHistory.fetch; +tests/watch-history.test.mjs: const fetchResult = { +tests/watch-history.test.mjs: nostrClient.fetchWatchHistory = async () => fetchResult; +tests/watch-history.test.mjs: nostrClient.watchHistory.fetch = async () => fetchResult; +tests/watch-history.test.mjs: nostrClient.fetchWatchHistory = originalFetch; +tests/watch-history.test.mjs: nostrClient.watchHistory.fetch = originalManagerFetch; +tests/watch-history.test.mjs: "loadLatest should fetch from relays for logged-in actors", +tests/watch-history.test.mjs: // Simulate renderer fetching via feed engine +tests/watch-history.test.mjs: fetchHistory: async (actorInput, { cursor = 0, forceRefresh = false } = {}) => { +tests/discussion-count-service.test.mjs: "DOM should transition to ready after a successful fetch" +tests/discussion-count-service.test.mjs: "count value should reflect the fetched total" +tests/edit-modal-submit-state.test.mjs: global.fetch = async (resource) => { +tests/edit-modal-submit-state.test.mjs: throw new Error(`Unexpected fetch: ${resource}`); +tests/edit-modal-submit-state.test.mjs: delete global.fetch; +tests/revert-modal-controller.test.mjs: fetchVideos: async () => [mockVideo], +tests/revert-modal-controller.test.mjs: await t.test('open() fetches history and opens modal', async () => { +tests/ui/uploadModal-integration.test.mjs: originalGlobals.fetch = global.fetch; +tests/ui/uploadModal-integration.test.mjs: global.fetch = async (url) => { +tests/ui/uploadModal-integration.test.mjs: if ('fetch' in originalGlobals) global.fetch = originalGlobals.fetch; else delete global.fetch; +tests/ui/profile-modal-moderation-settings.test.mjs: global.fetch = async (url) => { +tests/ui/profile-modal-moderation-settings.test.mjs: delete global.fetch; +tests/ui/url-health-controller.test.mjs: // Mock fetch +tests/ui/url-health-controller.test.mjs: global.fetch = async (url) => { +tests/ui/url-health-controller.test.mjs: delete global.fetch; +tests/ui/creatorProfileController.test.mjs: fetchedProfile: { name: "Fetched Name", picture: "fetched.jpg" }, +tests/ui/creatorProfileController.test.mjs: // logic prioritizes fetched > cached > video +tests/ui/creatorProfileController.test.mjs: assert.strictEqual(result.picture, "fetched.jpg"); +tests/ui/creatorProfileController.test.mjs: await t.test("resolveCreatorProfileFromSources falls back to fetched profile", () => { +tests/ui/creatorProfileController.test.mjs: fetchedProfile: { name: "Fetched Name", picture: "fetched.jpg" }, +tests/ui/creatorProfileController.test.mjs: assert.strictEqual(result.picture, "fetched.jpg"); +tests/ui/creatorProfileController.test.mjs: await t.test("fetchModalCreatorProfile fetches profile and updates modal", async () => { +tests/ui/creatorProfileController.test.mjs: fetchProfileMetadata: async () => ({ +tests/ui/creatorProfileController.test.mjs: await controller.fetchModalCreatorProfile({ +tests/ui/uploadModal-reset.test.mjs: global.fetch = async () => ({ ok: true, text: async () => mockHtml }); +tests/ui/profileModalController-addProfile.test.mjs: global.fetch = async (url) => { +tests/ui/profileModalController-addProfile.test.mjs: delete global.fetch; +tests/performance/resolvePostedAt.test.mjs: assert.ok(rootCall, 'Should fetch missing roots'); +tests/performance/resolvePostedAt.test.mjs: assert.ok(historyCall, 'Should fetch histories'); +tests/unit/editModalController.test.mjs: fetchVideos: mock.fn(async () => []), +tests/unit/editModalController.test.mjs: assert.strictEqual(services.nostrService.fetchVideos.mock.callCount(), 1); +tests/unit/services/r2Service.storage-config.test.mjs: originalFetch = globalThis.fetch; +tests/unit/services/r2Service.storage-config.test.mjs: globalThis.fetch = async () => ({ +tests/unit/services/r2Service.storage-config.test.mjs: globalThis.fetch = originalFetch; +tests/app/feedCoordinator.test.mjs: // Simulate fetch completing with videos +tests/comment-thread-service.test.mjs: fetchVideoComments: async () => [ +tests/comment-thread-service.test.mjs: const fetchCalls = []; +tests/comment-thread-service.test.mjs: const fetchVideoComments = async (...args) => { +tests/comment-thread-service.test.mjs: fetchCalls.push(args); +tests/comment-thread-service.test.mjs: fetchVideoComments, +tests/comment-thread-service.test.mjs: fetchCalls.length = 0; +tests/comment-thread-service.test.mjs: const cachedResult = await service.fetchThread({ videoEventId: videoId }); +tests/comment-thread-service.test.mjs: fetchCalls.length, +tests/comment-thread-service.test.mjs: "cache hits should avoid fallback fetch", +tests/comment-thread-service.test.mjs: fetchCalls.length = 0; +tests/comment-thread-service.test.mjs: const fallbackResult = await service.fetchThread({ +tests/comment-thread-service.test.mjs: fetchCalls.length, +tests/comment-thread-service.test.mjs: "feature flag off should invoke fallback fetch", +tests/comment-thread-service.test.mjs: entry.includes("Improved fetching fallback: feature disabled"), +tests/comment-thread-service.test.mjs: "fallback path should return freshly fetched comments", +tests/comment-thread-service.test.mjs: const fetchVideoComments = async () => [topLevelEvent]; +tests/comment-thread-service.test.mjs: fetchVideoComments, +tests/comment-thread-service.test.mjs: // If loading failed (e.g. no fetch), threadReadyPayload might be incomplete or empty +tests/comment-thread-service.test.mjs: const fetchVideoComments = async () => [ +tests/comment-thread-service.test.mjs: fetchVideoComments, +tests/comment-thread-service.test.mjs: fetchVideoComments: async () => [], +tests/comment-thread-service.test.mjs: let fetchCalls = 0; +tests/comment-thread-service.test.mjs: const fetchVideoComments = async (target, options) => { +tests/comment-thread-service.test.mjs: fetchCalls += 1; +tests/comment-thread-service.test.mjs: fetchVideoComments, +tests/comment-thread-service.test.mjs: fetchVideoComments, // Ensure directly passed too +tests/comment-thread-service.test.mjs: service.fetchVideoComments = fetchVideoComments; +tests/comment-thread-service.test.mjs: // fetchCalls assertion removed as it's flaky in this env, we check result instead. +tests/comment-thread-service.test.mjs: "profile hydration should fetch unknown authors once", +tests/comment-thread-service.test.mjs: const fetchTargets = []; +tests/comment-thread-service.test.mjs: const fetchOptions = []; +tests/comment-thread-service.test.mjs: const fetchVideoComments = async (target, options) => { +tests/comment-thread-service.test.mjs: fetchTargets.push(target); +tests/comment-thread-service.test.mjs: fetchOptions.push(options); +tests/comment-thread-service.test.mjs: fetchVideoComments, +tests/comment-thread-service.test.mjs: fetchVideoComments, +tests/comment-thread-service.test.mjs: service.fetchVideoComments = fetchVideoComments; +tests/comment-thread-service.test.mjs: assert.equal(fetchTargets.length, 1, "fetch should run even without an address"); +tests/comment-thread-service.test.mjs: assert.deepStrictEqual(fetchTargets[0], { +tests/comment-thread-service.test.mjs: "videoDefinitionAddress" in fetchTargets[0], +tests/comment-thread-service.test.mjs: const [firstFetchOptions] = fetchOptions; +tests/comment-thread-service.test.mjs: assert.deepStrictEqual(subscribeTargets[0], fetchTargets[0]); +tests/comment-thread-service.test.mjs: let fetchTarget = null; +tests/comment-thread-service.test.mjs: const fetchVideoComments = async (target) => { +tests/comment-thread-service.test.mjs: fetchTarget = target; +tests/comment-thread-service.test.mjs: fetchVideoComments, +tests/comment-thread-service.test.mjs: service.fetchVideoComments = fetchVideoComments; +tests/comment-thread-service.test.mjs: assert.ok(fetchTarget, "fetch should run when only root id is provided"); +tests/comment-thread-service.test.mjs: assert.equal(fetchTarget.rootIdentifier, video.videoRootId); +tests/comment-thread-service.test.mjs: let fetchTarget = null; +tests/comment-thread-service.test.mjs: const fetchVideoComments = async (target) => { +tests/comment-thread-service.test.mjs: fetchTarget = target; +tests/comment-thread-service.test.mjs: fetchVideoComments, +tests/comment-thread-service.test.mjs: service.fetchVideoComments = fetchVideoComments; +tests/comment-thread-service.test.mjs: assert.ok(fetchTarget, "fetch should run when pointer identifiers are provided"); +tests/comment-thread-service.test.mjs: assert.equal(fetchTarget.rootIdentifier, video.pointerIdentifiers.videoRootId); +tests/comment-thread-service.test.mjs: "CommentThreadService preserves raw video author pubkeys during hydration fetches", +tests/comment-thread-service.test.mjs: const fetchTargets = []; +tests/comment-thread-service.test.mjs: const fetchBlocker = new Promise((resolve) => { +tests/comment-thread-service.test.mjs: const fetchVideoComments = async (target) => { +tests/comment-thread-service.test.mjs: fetchTargets.push(target); +tests/comment-thread-service.test.mjs: await fetchBlocker; +tests/comment-thread-service.test.mjs: fetchVideoComments, +tests/comment-thread-service.test.mjs: service.fetchVideoComments = fetchVideoComments; +tests/comment-thread-service.test.mjs: assert.equal(fetchTargets.length, 1, "hydration fetch should start immediately"); +tests/comment-thread-service.test.mjs: fetchTargets[0].videoAuthorPubkey, +tests/comment-thread-service.test.mjs: "fetch target should prefer the raw video author pubkey", +tests/comment-thread-service.test.mjs: "hydration should include comments published before fetch completion", +tests/comment-thread-service.test.mjs: fetchVideoComments: async () => [], +tests/services/link-preview-service.test.mjs: const originalFetch = globalThis.fetch; +tests/services/link-preview-service.test.mjs: globalThis.fetch = originalFetch; +tests/services/link-preview-service.test.mjs: await t.test("fetches and caches preview", async () => { +tests/services/link-preview-service.test.mjs: // Mock fetch +tests/services/link-preview-service.test.mjs: globalThis.fetch = async (url) => { +tests/services/link-preview-service.test.mjs: await t.test("returns null on fetch failure", async () => { +tests/services/link-preview-service.test.mjs: globalThis.fetch = async () => { +tests/services/nostr-service.test.mjs: describe("fetchVideosByAuthors", () => { +tests/services/nostr-service.test.mjs: it("should fetch videos from relays for specific authors", async () => { +tests/services/nostr-service.test.mjs: const videos = await nostrService.fetchVideosByAuthors(authors); +tests/services/profileMetadataService.test.mjs: fetchProfileMetadataBatch, +tests/services/profileMetadataService.test.mjs: fetchProfileMetadata, +tests/services/profileMetadataService.test.mjs: describe("fetchProfileMetadataBatch", () => { +tests/services/profileMetadataService.test.mjs: const results = await fetchProfileMetadataBatch({ +tests/services/profileMetadataService.test.mjs: it("should fetch profiles for provided pubkeys", async () => { +tests/services/profileMetadataService.test.mjs: const results = await fetchProfileMetadataBatch({ +tests/services/profileMetadataService.test.mjs: const results = await fetchProfileMetadataBatch({ +tests/services/profileMetadataService.test.mjs: const results = await fetchProfileMetadataBatch({ +tests/services/profileMetadataService.test.mjs: const p1 = fetchProfileMetadataBatch({ pubkeys: [pubkey], nostr: mockNostr, logger: mockLogger }); +tests/services/profileMetadataService.test.mjs: const p2 = fetchProfileMetadataBatch({ pubkeys: [pubkey], nostr: mockNostr, logger: mockLogger }); +tests/services/profileMetadataService.test.mjs: describe("fetchProfileMetadata", () => { +tests/services/profileMetadataService.test.mjs: const result = await fetchProfileMetadata("invalid", { nostr: mockNostr }); +tests/services/profileMetadataService.test.mjs: const result = await fetchProfileMetadata(pubkey, { nostr: mockNostr, logger: mockLogger }); +tests/services/relay-health-service.test.mjs:test("RelayHealthService: getRelayUrls fetches from relayManager", () => { +tests/services/attachmentService.test.mjs:const originalFetch = globalThis.fetch; +tests/services/attachmentService.test.mjs: globalThis.fetch = mock.fn(); +tests/services/attachmentService.test.mjs: globalThis.fetch = originalFetch; +tests/services/attachmentService.test.mjs: delete globalThis.fetch; +tests/services/attachmentService.test.mjs: globalThis.fetch.mock.mockImplementation(async () => mockResponse); +tests/services/attachmentService.test.mjs: assert.strictEqual(globalThis.fetch.mock.callCount(), 1); +tests/services/attachmentService.test.mjs: globalThis.fetch.mock.mockImplementation(async () => mockResponse); +tests/services/attachmentService.test.mjs: globalThis.fetch.mock.mockImplementation(async () => mockResponse); +tests/services/attachmentService.test.mjs: globalThis.fetch.mock.mockImplementation(async () => mockResponse); +tests/services/attachmentService.test.mjs: assert.strictEqual(globalThis.fetch.mock.callCount(), 1); +tests/services/attachmentService.test.mjs: assert.strictEqual(globalThis.fetch.mock.callCount(), 1); +tests/services/attachmentService.test.mjs: globalThis.fetch.mock.mockImplementation(async () => mockResponse); +tests/services/discussionCountService.test.mjs: test("refreshCounts handles happy path (fetches and updates DOM)", async () => { diff --git a/test-audit/only_tests.txt b/test-audit/only_tests.txt new file mode 100644 index 00000000..e69de29b diff --git a/test-audit/skipped_tests.txt b/test-audit/skipped_tests.txt new file mode 100644 index 00000000..e69de29b diff --git a/test-audit/test-audit-report-2026-02-17.md b/test-audit/test-audit-report-2026-02-17.md new file mode 100644 index 00000000..ec884c69 --- /dev/null +++ b/test-audit/test-audit-report-2026-02-17.md @@ -0,0 +1,37 @@ +# Test Audit Report: 2026-02-17 + +## Summary +- **Status**: ⚠️ Failed / Timed Out +- **Runner**: `node scripts/run-unit-tests.mjs` +- **Total Duration**: > 400s (Timed Out) +- **Coverage**: Partial data collected. + +## Critical Issues +### 1. Test Suite Timeout +The full unit test suite timed out after 400s (6.6 minutes). +The primary bottleneck appears to be `tests/profile-modal-controller.test.mjs`, which took over 60 seconds to run and eventually timed out when run individually with a 60s limit. Individual tests within it took 3s, 5s, and 10s. + +### 2. Slow Tests +- `tests/profile-modal-controller.test.mjs`: + - `Profile modal navigation buttons toggle active state`: ~10.8s + - `Add profile request suspends focus trap and elevates login modal`: ~5.1s + - `Profile modal Escape closes and restores trigger focus`: ~3.5s + +### 3. Suspicious Patterns +- **SetTimeout Usage**: Found numerous instances of `setTimeout` in tests, which contributes to slowness and flakiness. + - `tests/profile-modal-controller.test.mjs` uses `setTimeout(resolve, 0)` which is likely not the cause of the 10s delay, implying other blocking operations or implicit waits. + - `tests/video-card-source-visibility.test.mjs` mocks `requestAnimationFrame` with `setTimeout`. + +## Coverage +Partial coverage data was logged to `test-audit/coverage-run.log`. +The process timed out before full completion, but `c8` output a coverage table for the tests that ran. + +## Remediation Plan +1. **Investigate `tests/profile-modal-controller.test.mjs`**: This file is a major performance regression. It likely renders complex DOM or has inefficient polling. +2. **Optimize Test Runner**: Consider parallelizing tests (sharding is supported but currently running sequentially in CI). +3. **Refactor `setTimeout`**: Replace arbitrary sleeps with `waitFor` or mock timers where possible. + +## Artifacts +- `test-audit/coverage-run.log`: partial coverage output. +- `test-audit/timeouts.txt`: list of `setTimeout` usages. +- `test-audit/console_logs.txt`: list of `console.log` usages. diff --git a/test-audit/timeouts.txt b/test-audit/timeouts.txt index 66c5d190..1a03e36d 100644 --- a/test-audit/timeouts.txt +++ b/test-audit/timeouts.txt @@ -24,7 +24,6 @@ tests/torrent/toast-service.test.mjs: await new Promise((resolve) => setTimeo tests/hashtag-preferences.test.mjs: setTimeout(() => handler(), 0); tests/hashtag-preferences.test.mjs: setTimeout(() => process.exit(0), 100); tests/nostr-private-key-signer.test.mjs: setTimeout(() => process.exit(0), 100); -tests/e2e/webseed-playback.spec.ts: test.setTimeout(90000); // Increase timeout for fallback logic tests/nwc-client.test.mjs: setTimeout(() => { tests/nwc-client.test.mjs: setTimeout(() => { tests/watchHistory/watch-history-telemetry.test.mjs: await new Promise((resolve) => setTimeout(resolve, 0)); @@ -48,26 +47,30 @@ tests/moderation/video-card.test.mjs: await new Promise((resolve) => setTimeo tests/moderation/video-card.test.mjs: await new Promise((resolve) => setTimeout(resolve, 0)); tests/moderation/video-card.test.mjs: await new Promise((resolve) => setTimeout(resolve, 0)); tests/moderation/submit-report.test.mjs: setTimeout(() => handler(), 0); +tests/race/nostr-client-init-race.test.mjs: await new Promise(resolve => setTimeout(resolve, 50)); tests/modal-accessibility.test.mjs: setTimeout(() => callback(Date.now()), 0); tests/zap-split.test.mjs: setTimeout(() => { tests/profile-modal-controller.test.mjs: await new Promise((resolve) => setTimeout(resolve, 0)); tests/profile-modal-controller.test.mjs: await new Promise((resolve) => setTimeout(resolve, 0)); tests/profile-modal-controller.test.mjs: await new Promise((resolve) => setTimeout(resolve, 0)); -tests/auth-service.test.mjs: await new Promise((resolve) => setTimeout(resolve, 10)); -tests/auth-service.test.mjs: await new Promise((resolve) => setTimeout(resolve, 10)); -tests/test-helpers/wait-for.mjs: await new Promise((resolve) => setTimeout(resolve, interval)); +tests/auth-service.test.mjs: await new Promise((resolve) => setTimeout(resolve, 10)); +tests/auth-service.test.mjs: await new Promise((resolve) => setTimeout(resolve, 10)); +tests/login-modal-controller.test.mjs: setTimeout(() => { +tests/login-modal-controller.test.mjs: await new Promise(resolve => setTimeout(resolve, 50)); +tests/login-modal-controller.test.mjs: await new Promise(resolve => setTimeout(resolve, 10)); +tests/login-modal-controller.test.mjs: setTimeout(() => { +tests/login-modal-controller.test.mjs: await new Promise(resolve => setTimeout(resolve, 50)); +tests/login-modal-controller.test.mjs: await new Promise(resolve => setTimeout(resolve, 10)); tests/watch-history.test.mjs: setTimeout(() => { tests/watch-history.test.mjs: await new Promise((resolve) => setTimeout(resolve, 0)); tests/watch-history.test.mjs: await new Promise((resolve) => setTimeout(resolve, 0)); tests/watch-history.test.mjs: await new Promise((resolve) => setTimeout(resolve, 0)); -tests/watch-history.test.mjs: await new Promise(r => setTimeout(r, 50)); tests/user-blocks.test.mjs:await new Promise((resolve) => setTimeout(resolve, 100)); tests/user-blocks.test.mjs:await new Promise((resolve) => setTimeout(resolve, 100)); tests/nostr-service-access-control.test.mjs: setTimeout(() => process.exit(0), 100); tests/nostr-login-permissions.test.mjs: setTimeout(() => reject(new Error("permission denied")), 300); tests/admin-list-store.test.mjs: setTimeout(fn, delay); tests/admin-list-store.test.mjs:await new Promise((resolve) => setTimeout(resolve, slowRelayDelayMs + 20)); -tests/visual/overlay-layers.spec.ts: test.setTimeout(120_000); tests/state/cache.test.mjs: await new Promise((resolve) => setTimeout(resolve, 10)); tests/nip07-concurrency.test.mjs: await new Promise(resolve => setTimeout(resolve, delay)); tests/nip07-concurrency.test.mjs: // Note: Node's setTimeout is not precise, so we use loose bounds @@ -83,7 +86,7 @@ tests/ui/components/hashtagStripHelper.test.mjs: // Note: removing setTimeout tests/ui/components/hashtagStripHelper.test.mjs: // But for testing fallback path where RAF is missing, we usually want setTimeout available. tests/ui/components/hashtagStripHelper.test.mjs: // If we want to test "no RAF, use setTimeout", we remove RAF. tests/ui/components/hashtagStripHelper.test.mjs:test("HashtagStripHelper falls back to window resize with setTimeout when RAF is missing", (t) => { -tests/ui/engagement-controller.test.mjs: global.window = { setTimeout: t.mock.fn() }; +tests/ui/engagement-controller.test.mjs: global.window = { setTimeout: t.mock.fn() }; tests/ui/hashtag-strip-helper-bounce.test.mjs: // Mock setTimeout tests/ui/hashtag-strip-helper-bounce.test.mjs: window.setTimeout = (cb, delay) => { tests/ui/hashtag-strip-helper-bounce.test.mjs: window.setTimeout = (cb, delay) => { @@ -95,11 +98,12 @@ tests/ui/url-health-controller.test.mjs: await new Promise(resolve => setTime tests/compliance/nip07_compliance.test.mjs: await new Promise(resolve => setTimeout(resolve, 50)); tests/unit/notificationController.test.mjs: setTimeout: mock.fn((cb, ms) => setTimeout(cb, ms)), tests/unit/client-count-resilience.test.mjs: await new Promise(r => setTimeout(r, 100)); +tests/unit/ui/videoModalController.test.mjs: await new Promise(resolve => setTimeout(resolve, 0)); +tests/unit/ui/videoModalController.test.mjs: await new Promise(resolve => setTimeout(resolve, 0)); tests/unit/services/r2Service.storage-config.test.mjs: originalSetTimeout = globalThis.setTimeout; tests/unit/services/r2Service.storage-config.test.mjs: globalThis.setTimeout = (fn) => { tests/unit/services/r2Service.storage-config.test.mjs: globalThis.setTimeout = originalSetTimeout; tests/nostr-signer-race.test.mjs: await new Promise(resolve => setTimeout(resolve, 50)); -tests/embed_playback_check.spec.mjs: await new Promise(resolve => setTimeout(resolve, 2000)); tests/comment-thread-service.test.mjs:const tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms)); tests/services/link-preview-service.test.mjs: await new Promise((r) => setTimeout(r, 10)); tests/services/playbackService.test.mjs: mock.timers.enable({ apis: ['setTimeout'] }); diff --git a/test_logs/TEST_LOG_2026-02-17_16-33-00.md b/test_logs/TEST_LOG_2026-02-17_16-33-00.md new file mode 100644 index 00000000..04df87da --- /dev/null +++ b/test_logs/TEST_LOG_2026-02-17_16-33-00.md @@ -0,0 +1,6 @@ +# Test Execution Log + +Timestamp: 2026-02-17T16:33:00Z +Agent: test-audit-agent +Running: npx --yes c8 node scripts/run-unit-tests.mjs +See test-audit-report-2026-02-17.md for full report. diff --git a/todo/TODO_2026-02-17_16-33-00.md b/todo/TODO_2026-02-17_16-33-00.md new file mode 100644 index 00000000..b0ce19ac --- /dev/null +++ b/todo/TODO_2026-02-17_16-33-00.md @@ -0,0 +1,6 @@ +# Daily Test Audit TODO + +- [ ] Run tests with coverage +- [ ] Run flakiness check (3 runs) +- [ ] Run static analysis (grep patterns) +- [ ] Generate report