From b6eda0833ca4ae2a7ee8fa9515e9e5366e2dedf2 Mon Sep 17 00:00:00 2001 From: bowenliang123 Date: Thu, 17 Sep 2026 13:58:08 +0800 Subject: [PATCH] chore(client): keep expected-path noise out of the test output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three local until-polling copies let the debounced detail updates land outside act — React warned 19 times per run. They collapse into one shared act-per-tick until in the kit: timers and promise chains only fire while a tick's sleep is awaiting, so the updates stay act-attributed and commit as each scope exits. The four browser specs that arm the fetch-on-miss retry with a deliberately failing fetcher silence the production console.warn the failure legitimately emits, restored by a file-level afterEach. A default-reporter run now prints zero stderr. --- tests/client/components/browser.spec.ts | 17 ++++++++++++++++- tests/client/components/contextModal.spec.ts | 11 +---------- tests/client/components/contextView.spec.ts | 19 ++----------------- tests/client/helpers/kit.ts | 18 ++++++++++++++++++ tests/client/timelineSource.spec.ts | 11 +---------- 5 files changed, 38 insertions(+), 38 deletions(-) diff --git a/tests/client/components/browser.spec.ts b/tests/client/components/browser.spec.ts index 2d64e83..aec8d41 100644 --- a/tests/client/components/browser.spec.ts +++ b/tests/client/components/browser.spec.ts @@ -5,7 +5,7 @@ import { act, createElement as h, useState } from 'react' import assert from 'node:assert/strict' -import { describe, test } from 'vitest' +import { afterEach, describe, test, vi } from 'vitest' import { makeContextBrowser, type ContextBrowserProps } from '../../../src/client/components/browser' import { makeStackedBar } from '../../../src/client/components/stackedBar' import { DICT_EN } from '../../../src/client/i18n' @@ -18,6 +18,17 @@ const kit = makeKit() const settings = createContextSettings() const Browser = makeContextBrowser(kit, makeStackedBar(kit), settings) +// Tests that arm the fetch-on-miss retry with a deliberately failing fetcher +// silence the production `console.warn` the failure legitimately emits. +afterEach(() => { + vi.restoreAllMocks() +}) + +/** Swallow the fetch-on-miss warning one deliberately failing test would print. */ +function silenceFetchWarn(): void { + vi.spyOn(console, 'warn').mockImplementation(() => {}) +} + // Category row order is CATS: system, tools, user, inject, assistant, tool. const ROW = { system: 0, tools: 1, user: 2, inject: 3, skill: 4, assistant: 5, tool: 6 } as const @@ -373,6 +384,7 @@ describe('ContextBrowser header epochs', () => { }) test('a failed epoch fetch arms the retry button; retrying succeeds', async () => { + silenceFetchWarn() const data = tl({ current: { system: 30, tools: 9, user: 0, inject: 0, skill: 0, assistant: 0, tool: 0, total: 39 } }) const pendings: Array<{ resolve: (v: HeaderEpochContent | null) => void; reject: (e: unknown) => void }> = [] const deferred: () => Promise = () => @@ -394,6 +406,7 @@ describe('ContextBrowser header epochs', () => { }) test('a stale epoch fetch resolves and rejects ignored once another epoch takes over', async () => { + silenceFetchWarn() const data = tl({ current: { system: 30, tools: 9, user: 0, inject: 0, skill: 0, assistant: 0, tool: 0, total: 39 }, requests: [req({ seq: 20, turn: 1, step: 0 }), req({ seq: 40, turn: 1, step: 1 })], @@ -1557,6 +1570,7 @@ describe('ContextBrowser targeted content fetch', () => { }) test('a failed read arms the retry button; retrying succeeds', async () => { + silenceFetchWarn() let calls = 0 const fetchContent = async (seq: number) => { calls += 1 @@ -1584,6 +1598,7 @@ describe('ContextBrowser targeted content fetch', () => { }) test('a late fetch for an abandoned row never lands; the next row fetches its own seq', async () => { + silenceFetchWarn() const deferreds: { resolve: (node: ConversationNodeLike | null) => void; reject: (reason: Error) => void }[] = [] const fetchedFor: number[] = [] const fetchContent = (seq: number): Promise => { diff --git a/tests/client/components/contextModal.spec.ts b/tests/client/components/contextModal.spec.ts index c8b9c4b..0aae388 100644 --- a/tests/client/components/contextModal.spec.ts +++ b/tests/client/components/contextModal.spec.ts @@ -13,7 +13,7 @@ import { createContextSettings } from '../../../src/client/settings' import type { ContextTimeline } from '../../../src/shared/types' import { DICT_EN } from '../../../src/client/i18n' import { TestClientCtx, TestSessions, asClientCtx } from '../helpers/harness' -import { click, flush, hover, keydown, makeKit, mount, query, queryAll, text, unhover } from '../helpers/kit' +import { click, flush, hover, keydown, makeKit, mount, query, queryAll, text, unhover, until } from '../helpers/kit' const kit = makeKit() const settings = createContextSettings() @@ -411,15 +411,6 @@ describe('ContextModal', () => { }) describe('ContextModal — the split generation', () => { - /** Poll until the predicate holds (the detail store's debounce rides real timers). */ - async function until(fn: () => boolean, message: string): Promise { - for (let i = 0; i < 400; i++) { - if (fn()) return - await new Promise(resolve => setTimeout(resolve, 5)) - } - assert.fail(message) - } - test('the modal shares the detail route: the browser lands the collections on open', async () => { vi.stubGlobal('fetch', async () => ({ ok: true, diff --git a/tests/client/components/contextView.spec.ts b/tests/client/components/contextView.spec.ts index 91574fb..f6e2ff7 100644 --- a/tests/client/components/contextView.spec.ts +++ b/tests/client/components/contextView.spec.ts @@ -19,7 +19,7 @@ import type { UseChatLike } from '../../../src/client/services' import type { ContextTimeline } from '../../../src/shared/types' import { DICT_EN } from '../../../src/client/i18n' import { TestClientCtx, TestLocale, asClientCtx } from '../helpers/harness' -import { click, flush, hover, makeKit, mount, query, queryAll, silenceWindowErrors, text, unhover } from '../helpers/kit' +import { click, flush, hover, makeKit, mount, query, queryAll, silenceWindowErrors, text, unhover, until } from '../helpers/kit' // pluginInfo's npm-registry probe stays inert (and '0.0.0-dev' short-circuits // it anyway). @@ -1245,14 +1245,6 @@ describe('ContextView — the split generation (slim head + detail channel)', () return new TestClientCtx() } - async function until(fn: () => boolean, message: string): Promise { - for (let i = 0; i < 400; i++) { - if (fn()) return - await new Promise(resolve => setTimeout(resolve, 5)) - } - assert.fail(message) - } - test('the head paints the counters immediately; the detail collections land through the channel', async () => { const ctx = slimCtx(async () => ({ ok: true, value: slimDetail() })) const View = makeView(ctx) @@ -1344,14 +1336,7 @@ describe('ContextView — the op-log generation (fileOps on the detail payload)' detailRev: 1, })), })) - const until2 = async (fn: () => boolean): Promise => { - for (let i = 0; i < 400; i++) { - if (fn()) return - await new Promise(r => setTimeout(r, 5)) - } - assert.fail('the detail never landed') - } - await until2(() => text(m.container).includes('README.md')) + await until(() => text(m.container).includes('README.md'), 'the detail never landed') assert.ok(text(m.container).includes('a.ts'), 'both served ops row') assert.ok(text(m.container).includes('+3'), 'the edit delta shows') assert.ok(!text(m.container).includes('No file reads'), 'not the empty state') diff --git a/tests/client/helpers/kit.ts b/tests/client/helpers/kit.ts index 1bf0114..a1cd31d 100644 --- a/tests/client/helpers/kit.ts +++ b/tests/client/helpers/kit.ts @@ -65,6 +65,24 @@ export async function flush(): Promise { await act(async () => {}) } +/** + * Poll until the predicate holds, one short act() scope per tick: timers and + * promise chains only get to run while a scope's sleep is awaiting, so the + * state updates they land are act-attributed (no React act() warnings) and + * committed when the scope exits — the check between ticks sees them. Real + * timers ride the store's own debounce; `message` labels the give-up failure. + */ +export async function until(fn: () => boolean, message: string): Promise { + for (let i = 0; i < 400; i++) { + await act(async () => { + if (fn()) return + await new Promise(resolve => setTimeout(resolve, 5)) + }) + if (fn()) return + } + throw new Error(message) +} + /** * Silence a deliberately-thrown render error: React 18 dev replays a failed * render through a fake DOM event, which jsdom reports as an uncaught window diff --git a/tests/client/timelineSource.spec.ts b/tests/client/timelineSource.spec.ts index 9b3b568..8e695f3 100644 --- a/tests/client/timelineSource.spec.ts +++ b/tests/client/timelineSource.spec.ts @@ -18,7 +18,7 @@ import { } from '../../src/client/timelineSource' import type { ContextTimeline, ContextTimelineDetail } from '../../src/shared/types' import { TestClientCtx, asClientCtx } from './helpers/harness' -import { mount, text } from './helpers/kit' +import { mount, text, until } from './helpers/kit' afterEach(() => { resetTimelineDetailStores() @@ -67,15 +67,6 @@ function ctxWithCall(behavior: () => { status?: number; body?: unknown }): Clien return asClientCtx(new TestClientCtx()) } -/** Poll until the predicate holds (real timers ride the store's debounce). */ -async function until(fn: () => boolean, message: string): Promise { - for (let i = 0; i < 400; i++) { - if (fn()) return - await new Promise(resolve => setTimeout(resolve, 5)) - } - assert.fail(message) -} - describe('detailOf', () => { test('rejects non-records and revision-less/negative/NaN payloads', () => { assert.equal(detailOf(null), null)