From fc8d5f203a56d5c63edee531a12bbb538a55891e Mon Sep 17 00:00:00 2001 From: alt-glitch Date: Fri, 12 Jun 2026 08:40:53 +0530 Subject: [PATCH] =?UTF-8?q?opentui(v6):=20post-rebase=20fixups=20=E2=80=94?= =?UTF-8?q?=20dedup=20probe=20mouse,=20.demo=20lint=20ignore,=20cap=20test?= =?UTF-8?q?s=20to=20windowing-aware=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebasing onto fcf49f313 (multi-click selection) collided in the test probe (both sides added 'mouse' — deduped) and surfaced two test debts the cap-restore commit (3cc56517a) had shipped masked by a piped exit code: store cap tests still asserted the 1000 default (now: 3000 windowed, 1000 with HERMES_TUI_WINDOWING=0, both covered), and the burst-interplay test relied on the old cap trimming a 1500-row burst (now pins HERMES_TUI_MAX_MESSAGES=1000 explicitly for both stores). Also: .demo/ build artifacts excluded from typed linting. 669 tests exit 0 (verified unpiped). Multi-click selections flow through the same renderer selection seam, so windowing's drag-freeze + row-pinning covers them with no changes. --- ui-opentui/eslint.config.mjs | 6 ++-- ui-opentui/src/test/lib/render.ts | 3 -- ui-opentui/src/test/store.test.ts | 34 ++++++++++++++----- ui-opentui/src/test/transcriptWindow.test.tsx | 22 ++++++++++-- 4 files changed, 49 insertions(+), 16 deletions(-) diff --git a/ui-opentui/eslint.config.mjs b/ui-opentui/eslint.config.mjs index cfe89bf821b..9f4dcba7f09 100644 --- a/ui-opentui/eslint.config.mjs +++ b/ui-opentui/eslint.config.mjs @@ -4,9 +4,9 @@ import unusedImports from "eslint-plugin-unused-imports" export default tseslint.config( { - // .bench/ is a build artifact of the bench suite's `nodes` cell - // (bench/run.mjs builds scripts/mem-bench.tsx into it) — never lint it. - ignores: ["node_modules/**", "dist/**", ".bench/**", ".repos/**", "*.frame.txt", "*.ansi"], + // .bench/ and .demo/ are build artifacts (bench `nodes` cell and the + // smoke demo: `node scripts/build.mjs scripts/demo.tsx .demo`) — never lint. + ignores: ["node_modules/**", "dist/**", ".bench/**", ".demo/**", ".repos/**", "*.frame.txt", "*.ansi"], }, js.configs.recommended, ...tseslint.configs.recommendedTypeChecked, diff --git a/ui-opentui/src/test/lib/render.ts b/ui-opentui/src/test/lib/render.ts index 37e53473396..f0dfe613612 100644 --- a/ui-opentui/src/test/lib/render.ts +++ b/ui-opentui/src/test/lib/render.ts @@ -65,8 +65,6 @@ export interface RenderProbe { readonly scroll: (x: number, y: number, direction: 'up' | 'down') => Promise /** The mock keyboard (typeText / pressArrow / pressEnter / …) — pair with `settle()`. */ readonly keys: TestRendererSetup['mockInput'] - /** The full mock mouse (drag / pressDown / release / …) — selection tests. */ - readonly mouse: TestRendererSetup['mockMouse'] /** The live test renderer (e.g. `getSelection()` assertions). */ readonly renderer: TestRendererSetup['renderer'] /** Run a render pass + flush so simulated input lands in the next `frame()`. */ @@ -125,7 +123,6 @@ export async function renderProbe( await setup.flush() }, keys: setup.mockInput, - mouse: setup.mockMouse, renderer: setup.renderer, settle: async () => { await setup.renderOnce() diff --git a/ui-opentui/src/test/store.test.ts b/ui-opentui/src/test/store.test.ts index a7cfad7f6f1..5216a392c54 100644 --- a/ui-opentui/src/test/store.test.ts +++ b/ui-opentui/src/test/store.test.ts @@ -506,10 +506,14 @@ describe('session store — resume hydrate (Phase 4b)', () => { describe('session store — rolling message cap (bounds the Yoga node high-water mark)', () => { const ENV_KEY = 'HERMES_TUI_MAX_MESSAGES' + const WINDOWING_KEY = 'HERMES_TUI_WINDOWING' const prev = process.env[ENV_KEY] + const prevWindowing = process.env[WINDOWING_KEY] afterEach(() => { if (prev === undefined) delete process.env[ENV_KEY] else process.env[ENV_KEY] = prev + if (prevWindowing === undefined) delete process.env[WINDOWING_KEY] + else process.env[WINDOWING_KEY] = prevWindowing }) test('caps the message array at the env-tuned MESSAGE_CAP, dropping the oldest (head)', () => { @@ -578,23 +582,37 @@ describe('session store — rolling message cap (bounds the Yoga node high-water expect(store.state.messages.at(-1)!.text).toBe('h7') }) - test('defaults to 1000 (the handle-safe ceiling) when the env var is unset/invalid', () => { + test('defaults to 3000 (windowed ceiling) when the env var is unset/invalid and windowing is on', () => { + // With transcript windowing (the default) the mounted set is ~3 viewports + // regardless of store size, so the scrollback ceiling is 3000 (#27 payoff). delete process.env[ENV_KEY] + delete process.env[WINDOWING_KEY] const store = createSessionStore() - for (let i = 0; i < 1050; i++) store.pushUser(`m${i}`) - expect(store.state.messages).toHaveLength(1000) + for (let i = 0; i < 3050; i++) store.pushUser(`m${i}`) + expect(store.state.messages).toHaveLength(3000) expect(store.state.messages[0]!.text).toBe('m50') // oldest 50 dropped }) - test('env values ABOVE the handle-safe ceiling are clamped to it (the native handle table binds, not memory)', () => { + test('HERMES_TUI_WINDOWING=0 keeps the handle-safe 1000 ceiling (every row mounts again)', () => { + delete process.env[ENV_KEY] + process.env[WINDOWING_KEY] = '0' + const store = createSessionStore() + for (let i = 0; i < 1050; i++) store.pushUser(`m${i}`) + expect(store.state.messages).toHaveLength(1000) + expect(store.state.messages[0]!.text).toBe('m50') + }) + + test('env values ABOVE the ceiling are clamped to it (the native handle table binds, not memory)', () => { // @opentui/core's global handle registry holds 65,534 live objects and a // text renderable costs 3; ~47 handles/row on the realistic fixture means - // ≳1,400 live rows crashes mid-mount ("Failed to create SyntaxStyle"). - // A 100000 "cap" is therefore a crash sentence, not a cap — clamp it. + // ≳1,400 live MOUNTED rows crashes mid-mount ("Failed to create + // SyntaxStyle"). Windowing bounds the mounted set (peak 31 measured), so + // the windowed ceiling is 3000 stored rows; a 100000 "cap" still clamps. process.env[ENV_KEY] = '100000' + delete process.env[WINDOWING_KEY] const store = createSessionStore() - for (let i = 0; i < 1100; i++) store.pushUser(`m${i}`) - expect(store.state.messages).toHaveLength(1000) + for (let i = 0; i < 3100; i++) store.pushUser(`m${i}`) + expect(store.state.messages).toHaveLength(3000) expect(store.state.dropped).toBe(100) expect(store.state.messages[0]!.text).toBe('m100') }) diff --git a/ui-opentui/src/test/transcriptWindow.test.tsx b/ui-opentui/src/test/transcriptWindow.test.tsx index 31f941f7a96..423dbef485c 100644 --- a/ui-opentui/src/test/transcriptWindow.test.tsx +++ b/ui-opentui/src/test/transcriptWindow.test.tsx @@ -153,7 +153,18 @@ function clipScrollbar(frame: string): string { describe('transcript windowing — S2 append-time adjudication', () => { test('bursting 1500 appends keeps the peak mounted-row count bounded (< 120)', async () => { - const onStore = createSessionStore() + // Pin the cap below the burst size: this test ALSO exercises the + // cap-trim × windowing interplay (trimmed rows' spacers must be pruned, + // scroll-to-top must land on the oldest SURVIVOR, row-500). The default + // ceiling is 3000 under windowing (#27) — a 1500-row burst never trims it. + process.env.HERMES_TUI_MAX_MESSAGES = '1000' + const onStore = (() => { + try { + return createSessionStore() + } finally { + delete process.env.HERMES_TUI_MAX_MESSAGES + } + })() onStore.apply({ type: 'gateway.ready' }) const on = await mountTranscript(onStore, '1') try { @@ -172,7 +183,14 @@ describe('transcript windowing — S2 append-time adjudication', () => { // ZERO-JANK INVARIANT survives the burst: spacers (measured or // estimated — these rows never soft-wrap) occupy EXACTLY the height the // full tree would. (The store cap trims both to the same 1000 rows.) - const offStore = createSessionStore() + process.env.HERMES_TUI_MAX_MESSAGES = '1000' + const offStore = (() => { + try { + return createSessionStore() + } finally { + delete process.env.HERMES_TUI_MAX_MESSAGES + } + })() offStore.apply({ type: 'gateway.ready' }) for (let i = 0; i < 1500; i++) { offStore.pushSystem(i % 5 === 4 ? `row-${i} marker\nsecond line\nthird line` : `row-${i} marker`)