chore(client): keep expected-path noise out of the test output
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.
This commit is contained in:
1 parent
bda3537b2c
commit
b6eda0833c
5 files changed
+38
-38
No files matched your search
@@ -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<HeaderEpochContent | null> = () =>
|
||||
@@ -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<ConversationNodeLike | null> => {
|
||||
|
||||
@@ -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<void> {
|
||||
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,
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> => {
|
||||
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')
|
||||
|
||||
@@ -65,6 +65,24 @@ export async function flush(): Promise<void> {
|
||||
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<void> {
|
||||
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
|
||||
|
||||
@@ -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<void> {
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user