From 9c9eb5747a3c50e2ad8bdf6068306a16f653364e Mon Sep 17 00:00:00 2001 From: acoliver Date: Mon, 3 Aug 2026 13:58:01 -0300 Subject: [PATCH 001/128] wip(cli-bun): bun runner, preload parity, and shared vi.mock shim fixes --- packages/cli/bun-test-setup.ts | 54 ++- packages/cli/run-bun-tests.ts | 236 ++++++++++++ .../HistoryItemDisplay.test.tsx.snap | 160 ++++++++ .../InputPrompt.paste.test.tsx.snap | 78 ++++ .../InputPrompt.vim.test.tsx.snap | 156 ++++++++ .../ModelStatsDisplay.test.tsx.snap | 137 +++++++ .../SettingsDialog.interactions.test.tsx.snap | 344 ++++++++++++++++++ .../SettingsDialog.test.tsx.snap | 43 +++ .../StatsDisplay.sections.test.tsx.snap | 222 +++++++++++ .../__snapshots__/StatsDisplay.test.tsx.snap | 217 +++++++++++ .../__snapshots__/Table.test.tsx.snap | 13 + .../ToolStatsDisplay.test.tsx.snap | 90 +++++ .../ToolGroupMessage.test.tsx.snap | 127 +++++++ .../__snapshots__/ToolMessage.test.tsx.snap | 37 ++ .../__snapshots__/ChatList.test.tsx.snap | 19 + .../MarkdownDisplay.test.tsx.snap | 180 +++++++++ project-plans/issue2843/plan.md | 91 +++++ test-setup/augment-bun-vi.ts | 77 +++- test-setup/module-resolution.ts | 17 +- 19 files changed, 2274 insertions(+), 24 deletions(-) create mode 100644 packages/cli/run-bun-tests.ts create mode 100644 project-plans/issue2843/plan.md diff --git a/packages/cli/bun-test-setup.ts b/packages/cli/bun-test-setup.ts index 761d10c7bd..ee1e3312cd 100644 --- a/packages/cli/bun-test-setup.ts +++ b/packages/cli/bun-test-setup.ts @@ -18,7 +18,18 @@ import { JSDOM } from 'jsdom'; import { join } from 'node:path'; import React from 'react'; -import { mock, afterEach } from 'bun:test'; +import { + mock, + afterEach, + afterAll, + beforeAll, + beforeEach, + describe, + expect, + it, + test, + vi, +} from 'bun:test'; import { clearActiveProviderRuntimeContext, DebugLogger, @@ -45,6 +56,36 @@ Object.assign(globalThis, { getComputedStyle: dom.window.getComputedStyle.bind(dom.window), }); +// --------------------------------------------------------------------------- +// Test globals +// +// The Vitest configuration for this workspace sets `globals: true`, so many +// test files reference `describe` / `it` / `expect` without importing them. +// Bun only injects those names into files that import from 'bun:test' or +// 'vitest', so expose the same globals here to preserve that contract. +// --------------------------------------------------------------------------- +const testGlobals: Record = { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + test, + vi, +}; +for (const [name, value] of Object.entries(testGlobals)) { + if (!(name in globalThis)) { + Object.defineProperty(globalThis, name, { + value, + writable: true, + enumerable: false, + configurable: true, + }); + } +} + // --------------------------------------------------------------------------- // Environment // --------------------------------------------------------------------------- @@ -275,6 +316,13 @@ const { __resetCleanupStateForTesting } = await import( './src/utils/cleanup.js' ); +// Bun's mock.module patches a module namespace in place and shares it with +// this preload, so a test that mocks '@vybestack/llxprt-code-core' would +// otherwise replace the cleanup helpers this file relies on. Capture the real +// implementations now, before any test file can register a module mock. +const resetDebugLoggerForTesting = DebugLogger.resetForTesting.bind(DebugLogger); +const clearProviderRuntimeContext = clearActiveProviderRuntimeContext; + const managedProcessEvents = [ 'exit', 'SIGINT', @@ -319,7 +367,7 @@ afterEach(async () => { for (const eventName of managedProcessEvents) { restoreProcessListeners(eventName); } - await DebugLogger.resetForTesting(); + await resetDebugLoggerForTesting(); __resetCleanupStateForTesting(); - clearActiveProviderRuntimeContext(); + clearProviderRuntimeContext(); }); diff --git a/packages/cli/run-bun-tests.ts b/packages/cli/run-bun-tests.ts new file mode 100644 index 0000000000..ba0aed2b6b --- /dev/null +++ b/packages/cli/run-bun-tests.ts @@ -0,0 +1,236 @@ +/** + * @license + * Copyright 2026 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Bun test runner for the CLI workspace. + * + * Discovers every unit test file in the workspace and runs each one in its own + * `bun test` process with bounded parallelism. A process per file is required + * because Bun's `mock.module` registry is process-wide (unlike Vitest's + * per-file module graph), so sharing a process would leak mocks between files. + * + * Integration tests (`*.integration.test.ts`) are excluded here; they are + * selected by `test:integration`, exactly as under the Vitest configuration. + * + * Exit code is 0 when every file passes and 1 when any file fails. + */ + +import { spawn } from 'node:child_process'; +import { readdirSync, statSync, writeFileSync } from 'node:fs'; +import { join, relative } from 'node:path'; +import { availableParallelism } from 'node:os'; + +const PER_FILE_TIMEOUT_MS = 120_000; +const SKIPPED_DIRECTORIES = new Set([ + 'node_modules', + 'dist', + 'coverage', + 'tmp', + '__snapshots__', +]); +const TEST_ROOTS = ['src', 'test', 'test-bun', 'test-utils']; +const TEST_FILE_PATTERN = /\.(test|spec)\.(ts|tsx)$/; +const INTEGRATION_FILE_PATTERN = /\.integration\.(test|spec)\.(ts|tsx)$/; + +function parseConcurrency(): number { + const flagIndex = process.argv.indexOf('--concurrency'); + if (flagIndex >= 0) { + const parsed = Number.parseInt(process.argv[flagIndex + 1] ?? '', 10); + if (Number.isFinite(parsed) && parsed > 0) { + return parsed; + } + } + return Math.max(1, Math.min(8, availableParallelism())); +} + +export function isUnitTestFile(fileName: string): boolean { + return ( + TEST_FILE_PATTERN.test(fileName) && !INTEGRATION_FILE_PATTERN.test(fileName) + ); +} + +function collectTestFiles(dir: string, results: string[]): void { + let entries: string[]; + try { + entries = readdirSync(dir); + } catch { + return; + } + for (const entry of entries) { + if (SKIPPED_DIRECTORIES.has(entry) || entry.startsWith('.')) { + continue; + } + const fullPath = join(dir, entry); + if (statSync(fullPath).isDirectory()) { + collectTestFiles(fullPath, results); + } else if (isUnitTestFile(entry)) { + results.push(fullPath); + } + } +} + +export function discoverTestFiles(root: string): string[] { + const results: string[] = []; + for (const testRoot of TEST_ROOTS) { + collectTestFiles(join(root, testRoot), results); + } + return results.map((file) => relative(root, file).split('\\').join('/')).sort(); +} + +interface TestResult { + readonly file: string; + readonly passed: boolean; + readonly exitCode: number | null; + readonly timedOut: boolean; + readonly output: string; +} + +function runTestFile(file: string): Promise { + return new Promise((resolve) => { + let settled = false; + let output = ''; + const child = spawn(process.execPath, ['test', file], { + cwd: process.cwd(), + stdio: ['ignore', 'pipe', 'pipe'], + env: process.env, + }); + + child.stdout?.on('data', (chunk: Buffer) => { + output += chunk.toString(); + }); + child.stderr?.on('data', (chunk: Buffer) => { + output += chunk.toString(); + }); + + const timer = setTimeout(() => { + if (settled) return; + settled = true; + child.kill('SIGKILL'); + resolve({ file, passed: false, exitCode: null, timedOut: true, output }); + }, PER_FILE_TIMEOUT_MS); + + child.on('exit', (code) => { + if (settled) return; + settled = true; + clearTimeout(timer); + resolve({ + file, + passed: code === 0, + exitCode: code, + timedOut: false, + output, + }); + }); + + child.on('error', (error: Error) => { + if (settled) return; + settled = true; + clearTimeout(timer); + resolve({ + file, + passed: false, + exitCode: -1, + timedOut: false, + output: `${output}\nFailed to spawn bun test: ${error.message}`, + }); + }); + }); +} + +function escapeXml(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +export function generateJUnit(results: readonly TestResult[]): string { + const failedCount = results.filter((result) => !result.passed).length; + const testCases = results + .map((result) => { + const className = escapeXml(result.file.replace(/\.(test|spec)\.tsx?$/, '')); + const failure = result.passed + ? '' + : result.timedOut + ? `TIMEOUT` + : `${escapeXml(result.output.slice(-4000))}`; + return ` ${failure}`; + }) + .join('\n'); + + return [ + '', + ``, + ` `, + testCases, + ' ', + '', + ].join('\n'); +} + +async function main(): Promise { + const root = import.meta.dir; + const testFiles = discoverTestFiles(root); + if (testFiles.length === 0) { + console.error('No CLI test files were discovered.'); + process.exit(1); + } + + const concurrency = parseConcurrency(); + console.log( + `Running ${testFiles.length} CLI test files with concurrency ${concurrency}`, + ); + + const results: TestResult[] = []; + let nextIndex = 0; + let completed = 0; + + async function worker(): Promise { + for (;;) { + const index = nextIndex++; + if (index >= testFiles.length) return; + const result = await runTestFile(testFiles[index]); + results.push(result); + completed++; + if (!result.passed) { + console.error( + `FAIL (${completed}/${testFiles.length}) ${result.file}${ + result.timedOut ? ' [timeout]' : '' + }`, + ); + } + } + } + + await Promise.all( + Array.from({ length: Math.min(concurrency, testFiles.length) }, worker), + ); + + results.sort((a, b) => a.file.localeCompare(b.file)); + const failed = results.filter((result) => !result.passed); + + for (const result of failed) { + console.error(`\n----- ${result.file} -----`); + console.error(result.output.slice(-6000)); + } + + console.log( + `Passed ${results.length - failed.length}/${results.length} CLI test files` + + (failed.length > 0 ? ` (${failed.length} failed)` : ''), + ); + + writeFileSync(join(root, 'junit.xml'), generateJUnit(results)); + process.exit(failed.length > 0 ? 1 : 0); +} + +if (import.meta.main) { + await main(); +} diff --git a/packages/cli/src/ui/components/__snapshots__/HistoryItemDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/HistoryItemDisplay.test.tsx.snap index c1585a9e81..225db45110 100644 --- a/packages/cli/src/ui/components/__snapshots__/HistoryItemDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/HistoryItemDisplay.test.tsx.snap @@ -135,3 +135,163 @@ exports[` > should render a truncated gemini_content item 49 Line 49 50 Line 50" `; + +exports[` should render a truncated gemini item 1`] = ` +" + ERROR RuntimeContextProvider is missing from the component tree. + + src/ui/contexts/RuntimeContext.tsx:245:15 + + 242: export function useRuntimeBridge(): RuntimeContextBridge { + 243: const context = useContext(RuntimeContext); + 244: if (!context) { + 245: throw new Error( + 246: 'RuntimeContextProvider is missing from the component tree.', + 247: ); + 248: } + + - useRuntimeBridge (src/ui/contexts/RuntimeContext.tsx:245:15) + - useRuntimeApi (src/ui/contexts/RuntimeContext.tsx:253:10) + - AiMessage (src/ui/components/messages/AiMessage.tsx:54:35) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) + -renderRootSy + c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c + js/react-reconciler.development.js:12624:11) +" +`; + +exports[` should render a full gemini item when using availableTerminalHeightAi 1`] = ` +" + ERROR RuntimeContextProvider is missing from the component tree. + + src/ui/contexts/RuntimeContext.tsx:245:15 + + 242: export function useRuntimeBridge(): RuntimeContextBridge { + 243: const context = useContext(RuntimeContext); + 244: if (!context) { + 245: throw new Error( + 246: 'RuntimeContextProvider is missing from the component tree.', + 247: ); + 248: } + + - useRuntimeBridge (src/ui/contexts/RuntimeContext.tsx:245:15) + - useRuntimeApi (src/ui/contexts/RuntimeContext.tsx:253:10) + - AiMessage (src/ui/components/messages/AiMessage.tsx:54:35) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) + -renderRootSy + c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c + js/react-reconciler.development.js:12624:11) +" +`; + +exports[` should render a truncated gemini_content item 1`] = ` +" + ERROR RuntimeContextProvider is missing from the component tree. + + src/ui/contexts/RuntimeContext.tsx:245:15 + + 242: export function useRuntimeBridge(): RuntimeContextBridge { + 243: const context = useContext(RuntimeContext); + 244: if (!context) { + 245: throw new Error( + 246: 'RuntimeContextProvider is missing from the component tree.', + 247: ); + 248: } + + - useRuntimeBridge (src/ui/contexts/RuntimeContext.tsx:245:15) + - useRuntimeApi (src/ui/contexts/RuntimeContext.tsx:253:10) + - useResolvedWorkspaceDirectories (src/ui/hooks/useResolvedWorkspaceDirectories.ts:24:37) + - AiMessageContent (src/ui/components/messages/AiMessageContent.tsx:36:5) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) +" +`; + +exports[` should render a full gemini_content item when using availableTerminalHeightAi 1`] = ` +" + ERROR RuntimeContextProvider is missing from the component tree. + + src/ui/contexts/RuntimeContext.tsx:245:15 + + 242: export function useRuntimeBridge(): RuntimeContextBridge { + 243: const context = useContext(RuntimeContext); + 244: if (!context) { + 245: throw new Error( + 246: 'RuntimeContextProvider is missing from the component tree.', + 247: ); + 248: } + + - useRuntimeBridge (src/ui/contexts/RuntimeContext.tsx:245:15) + - useRuntimeApi (src/ui/contexts/RuntimeContext.tsx:253:10) + - useResolvedWorkspaceDirectories (src/ui/hooks/useResolvedWorkspaceDirectories.ts:24:37) + - AiMessageContent (src/ui/components/messages/AiMessageContent.tsx:36:5) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) +" +`; diff --git a/packages/cli/src/ui/components/__snapshots__/InputPrompt.paste.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/InputPrompt.paste.test.tsx.snap index 6b4cebca98..0ac4b406f2 100644 --- a/packages/cli/src/ui/components/__snapshots__/InputPrompt.paste.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/InputPrompt.paste.test.tsx.snap @@ -123,3 +123,81 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match - performUnitOfWork (node_modules/react-reconciler/cjs/react-reconciler.development.js:12834:22) " `; + +exports[`InputPrompt command search (Ctrl+R when not in shell) renders match window and expanded view (snapshots): command-search-render-collapsed-match 1`] = ` +" + ERROR useMouseContext must be used within a MouseProvider + + src/ui/contexts/MouseContext.tsx:40:15 + + 37: export function useMouseContext() { + 38: const context = useContext(MouseContext); + 39: if (!context) { + 40: throw new Error('useMouseContext must be used within a MouseProvider'); + 41: } + 42: return context; + 43: } + + - useMouseContext (src/ui/contexts/MouseContext.tsx:40:15) + - useMouse (src/ui/hooks/useMouse.ts:24:38) + - useMousePaste (src/ui/components/inputPromptHooks.ts:544:3) + - InputPrompt (src/ui/components/InputPrompt.tsx:69:3) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) +" +`; + +exports[`InputPrompt command search (Ctrl+R when not in shell) renders match window and expanded view (snapshots): command-search-render-expanded-match 1`] = ` +" + ERROR useMouseContext must be used within a MouseProvider + + src/ui/contexts/MouseContext.tsx:40:15 + + 37: export function useMouseContext() { + 38: const context = useContext(MouseContext); + 39: if (!context) { + 40: throw new Error('useMouseContext must be used within a MouseProvider'); + 41: } + 42: return context; + 43: } + + - useMouseContext (src/ui/contexts/MouseContext.tsx:40:15) + - useMouse (src/ui/hooks/useMouse.ts:24:38) + - useMousePaste (src/ui/components/inputPromptHooks.ts:544:3) + - InputPrompt (src/ui/components/InputPrompt.tsx:69:3) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) +" +`; diff --git a/packages/cli/src/ui/components/__snapshots__/InputPrompt.vim.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/InputPrompt.vim.test.tsx.snap index 0ab53ce3b8..324763d2d6 100644 --- a/packages/cli/src/ui/components/__snapshots__/InputPrompt.vim.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/InputPrompt.vim.test.tsx.snap @@ -247,3 +247,159 @@ exports[`InputPrompt > snapshots > should render correctly when accepting edits - performUnitOfWork (node_modules/react-reconciler/cjs/react-reconciler.development.js:12834:22) " `; + +exports[`InputPrompt snapshots should render correctly in shell mode 1`] = ` +" + ERROR useMouseContext must be used within a MouseProvider + + src/ui/contexts/MouseContext.tsx:40:15 + + 37: export function useMouseContext() { + 38: const context = useContext(MouseContext); + 39: if (!context) { + 40: throw new Error('useMouseContext must be used within a MouseProvider'); + 41: } + 42: return context; + 43: } + + - useMouseContext (src/ui/contexts/MouseContext.tsx:40:15) + - useMouse (src/ui/hooks/useMouse.ts:24:38) + - useMousePaste (src/ui/components/inputPromptHooks.ts:544:3) + - InputPrompt (src/ui/components/InputPrompt.tsx:69:3) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) +" +`; + +exports[`InputPrompt snapshots should render correctly when accepting edits 1`] = ` +" + ERROR useMouseContext must be used within a MouseProvider + + src/ui/contexts/MouseContext.tsx:40:15 + + 37: export function useMouseContext() { + 38: const context = useContext(MouseContext); + 39: if (!context) { + 40: throw new Error('useMouseContext must be used within a MouseProvider'); + 41: } + 42: return context; + 43: } + + - useMouseContext (src/ui/contexts/MouseContext.tsx:40:15) + - useMouse (src/ui/hooks/useMouse.ts:24:38) + - useMousePaste (src/ui/components/inputPromptHooks.ts:544:3) + - InputPrompt (src/ui/components/InputPrompt.tsx:69:3) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) +" +`; + +exports[`InputPrompt snapshots should render correctly in yolo mode 1`] = ` +" + ERROR useMouseContext must be used within a MouseProvider + + src/ui/contexts/MouseContext.tsx:40:15 + + 37: export function useMouseContext() { + 38: const context = useContext(MouseContext); + 39: if (!context) { + 40: throw new Error('useMouseContext must be used within a MouseProvider'); + 41: } + 42: return context; + 43: } + + - useMouseContext (src/ui/contexts/MouseContext.tsx:40:15) + - useMouse (src/ui/hooks/useMouse.ts:24:38) + - useMousePaste (src/ui/components/inputPromptHooks.ts:544:3) + - InputPrompt (src/ui/components/InputPrompt.tsx:69:3) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) +" +`; + +exports[`InputPrompt snapshots should not show inverted cursor when shell is focused 1`] = ` +" + ERROR useMouseContext must be used within a MouseProvider + + src/ui/contexts/MouseContext.tsx:40:15 + + 37: export function useMouseContext() { + 38: const context = useContext(MouseContext); + 39: if (!context) { + 40: throw new Error('useMouseContext must be used within a MouseProvider'); + 41: } + 42: return context; + 43: } + + - useMouseContext (src/ui/contexts/MouseContext.tsx:40:15) + - useMouse (src/ui/hooks/useMouse.ts:24:38) + - useMousePaste (src/ui/components/inputPromptHooks.ts:544:3) + - InputPrompt (src/ui/components/InputPrompt.tsx:69:3) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) +" +`; diff --git a/packages/cli/src/ui/components/__snapshots__/ModelStatsDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ModelStatsDisplay.test.tsx.snap index 4a2ef97579..6e711284fa 100644 --- a/packages/cli/src/ui/components/__snapshots__/ModelStatsDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/ModelStatsDisplay.test.tsx.snap @@ -136,3 +136,140 @@ exports[` > should render "no API calls" message when there │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; + +exports[` should render "no API calls" message when there are no active models 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ No API calls have been made in this session. │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` should not display conditional rows if no model has data for them 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Model Stats For Nerds │ +│ │ +│ Metric gemini-2.5-pro │ +│ ────────────────────────────────────────────────────────────────────────────────────────────── │ +│ API │ +│ Requests 1 │ +│ Errors 0 (0.0%) │ +│ Avg Latency 100ms │ +│ Tokens │ +│ Total 30 │ +│ ↳ Input 10 │ +│ ↳ Output 20 │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` should display conditional rows if at least one model has data 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Model Stats For Nerds │ +│ │ +│ Metric gemini-2.5-pro gemini-2.5-flash │ +│ ────────────────────────────────────────────────────────────────────────────────────────────── │ +│ API │ +│ Requests 1 1 │ +│ Errors 0 (0.0%) 0 (0.0%) │ +│ Avg Latency 100ms 50ms │ +│ Tokens │ +│ Total 30 15 │ +│ ↳ Input 5 5 │ +│ ↳ Cache Reads 5 (50.0%) 0 (0.0%) │ +│ ↳ Thoughts 2 0 │ +│ ↳ Tool 0 3 │ +│ ↳ Output 20 10 │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` should display stats for multiple models correctly 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Model Stats For Nerds │ +│ │ +│ Metric gemini-2.5-pro gemini-2.5-flash │ +│ ────────────────────────────────────────────────────────────────────────────────────────────── │ +│ API │ +│ Requests 10 20 │ +│ Errors 1 (10.0%) 2 (10.0%) │ +│ Avg Latency 100ms 25ms │ +│ Tokens │ +│ Total 300 600 │ +│ ↳ Input 50 100 │ +│ ↳ Cache Reads 50 (50.0%) 100 (50.0%) │ +│ ↳ Thoughts 10 20 │ +│ ↳ Tool 5 10 │ +│ ↳ Output 200 400 │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` should handle large values without wrapping or overlapping 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Model Stats For Nerds │ +│ │ +│ Metric gemini-2.5-pro │ +│ ────────────────────────────────────────────────────────────────────────────────────────────── │ +│ API │ +│ Requests 999,999,999 │ +│ Errors 123,456,789 (12.3%) │ +│ Avg Latency 0ms │ +│ Tokens │ +│ Total 999,999,999 │ +│ ↳ Input 864,197,532 │ +│ ↳ Cache Reads 123,456,789 (12.5%) │ +│ ↳ Thoughts 111,111,111 │ +│ ↳ Tool 222,222,222 │ +│ ↳ Output 123,456,789 │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` should display a single model correctly 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Model Stats For Nerds │ +│ │ +│ Metric gemini-2.5-pro │ +│ ────────────────────────────────────────────────────────────────────────────────────────────── │ +│ API │ +│ Requests 1 │ +│ Errors 0 (0.0%) │ +│ Avg Latency 100ms │ +│ Tokens │ +│ Total 30 │ +│ ↳ Input 5 │ +│ ↳ Cache Reads 5 (50.0%) │ +│ ↳ Thoughts 2 │ +│ ↳ Tool 1 │ +│ ↳ Output 20 │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` should handle models with long names (gemini-3-*-preview) without layout breaking 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Model Stats For Nerds │ +│ │ +│ Metric gemini-3-pro-preview gemini-3-flash-preview │ +│ ────────────────────────────────────────────────────────────────────────────────────────────── │ +│ API │ +│ Requests 10 20 │ +│ Errors 0 (0.0%) 0 (0.0%) │ +│ Avg Latency 200ms 50ms │ +│ Tokens │ +│ Total 6,000 12,000 │ +│ ↳ Input 1,000 2,000 │ +│ ↳ Cache Reads 500 (25.0%) 1,000 (25.0%) │ +│ ↳ Thoughts 100 200 │ +│ ↳ Tool 50 100 │ +│ ↳ Output 4,000 8,000 │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.interactions.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.interactions.test.tsx.snap index 2072696099..751f1070e3 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.interactions.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.interactions.test.tsx.snap @@ -367,3 +367,347 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; + +exports[`SettingsDialog Snapshot Tests should render default state correctly 1`] = ` +" + ERROR useVimMode must be used within a VimModeProvider + + src/ui/contexts/VimModeContext.tsx:87:15 + + 84: export const useVimMode = () => { + 85: const context = useContext(VimModeContext); + 86: if (context === undefined) { + 87: throw new Error('useVimMode must be used within a VimModeProvider'); + 88: } + 89: return context; + 90: }; + + - useVimMode (src/ui/contexts/VimModeContext.tsx:87:15) + - SettingsDialog (src/ui/components/SettingsDialog.tsx:36:26) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) + -renderRootSy + c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c + js/react-reconciler.development.js:12624:11) + -performWorkOnR + ot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12135:44) +" +`; + +exports[`SettingsDialog Snapshot Tests should render various boolean settings enabled correctly 1`] = ` +" + ERROR useVimMode must be used within a VimModeProvider + + src/ui/contexts/VimModeContext.tsx:87:15 + + 84: export const useVimMode = () => { + 85: const context = useContext(VimModeContext); + 86: if (context === undefined) { + 87: throw new Error('useVimMode must be used within a VimModeProvider'); + 88: } + 89: return context; + 90: }; + + - useVimMode (src/ui/contexts/VimModeContext.tsx:87:15) + - SettingsDialog (src/ui/components/SettingsDialog.tsx:36:26) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) + -renderRootSy + c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c + js/react-reconciler.development.js:12624:11) + -performWorkOnR + ot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12135:44) +" +`; + +exports[`SettingsDialog Snapshot Tests should render mixed boolean and number settings correctly 1`] = ` +" + ERROR useVimMode must be used within a VimModeProvider + + src/ui/contexts/VimModeContext.tsx:87:15 + + 84: export const useVimMode = () => { + 85: const context = useContext(VimModeContext); + 86: if (context === undefined) { + 87: throw new Error('useVimMode must be used within a VimModeProvider'); + 88: } + 89: return context; + 90: }; + + - useVimMode (src/ui/contexts/VimModeContext.tsx:87:15) + - SettingsDialog (src/ui/components/SettingsDialog.tsx:36:26) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) + -renderRootSy + c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c + js/react-reconciler.development.js:12624:11) + -performWorkOnR + ot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12135:44) +" +`; + +exports[`SettingsDialog Snapshot Tests should render focused on scope selector correctly 1`] = ` +" + ERROR useVimMode must be used within a VimModeProvider + + src/ui/contexts/VimModeContext.tsx:87:15 + + 84: export const useVimMode = () => { + 85: const context = useContext(VimModeContext); + 86: if (context === undefined) { + 87: throw new Error('useVimMode must be used within a VimModeProvider'); + 88: } + 89: return context; + 90: }; + + - useVimMode (src/ui/contexts/VimModeContext.tsx:87:15) + - SettingsDialog (src/ui/components/SettingsDialog.tsx:36:26) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) + -renderRootSy + c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c + js/react-reconciler.development.js:12624:11) + -performWorkOnR + ot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12135:44) +" +`; + +exports[`SettingsDialog Snapshot Tests should render accessibility settings enabled correctly 1`] = ` +" + ERROR useVimMode must be used within a VimModeProvider + + src/ui/contexts/VimModeContext.tsx:87:15 + + 84: export const useVimMode = () => { + 85: const context = useContext(VimModeContext); + 86: if (context === undefined) { + 87: throw new Error('useVimMode must be used within a VimModeProvider'); + 88: } + 89: return context; + 90: }; + + - useVimMode (src/ui/contexts/VimModeContext.tsx:87:15) + - SettingsDialog (src/ui/components/SettingsDialog.tsx:36:26) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) + -renderRootSy + c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c + js/react-reconciler.development.js:12624:11) + -performWorkOnR + ot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12135:44) +" +`; + +exports[`SettingsDialog Snapshot Tests should render file filtering settings configured correctly 1`] = ` +" + ERROR useVimMode must be used within a VimModeProvider + + src/ui/contexts/VimModeContext.tsx:87:15 + + 84: export const useVimMode = () => { + 85: const context = useContext(VimModeContext); + 86: if (context === undefined) { + 87: throw new Error('useVimMode must be used within a VimModeProvider'); + 88: } + 89: return context; + 90: }; + + - useVimMode (src/ui/contexts/VimModeContext.tsx:87:15) + - SettingsDialog (src/ui/components/SettingsDialog.tsx:36:26) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) + -renderRootSy + c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c + js/react-reconciler.development.js:12624:11) + -performWorkOnR + ot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12135:44) +" +`; + +exports[`SettingsDialog Snapshot Tests should render tools and security settings correctly 1`] = ` +" + ERROR useVimMode must be used within a VimModeProvider + + src/ui/contexts/VimModeContext.tsx:87:15 + + 84: export const useVimMode = () => { + 85: const context = useContext(VimModeContext); + 86: if (context === undefined) { + 87: throw new Error('useVimMode must be used within a VimModeProvider'); + 88: } + 89: return context; + 90: }; + + - useVimMode (src/ui/contexts/VimModeContext.tsx:87:15) + - SettingsDialog (src/ui/components/SettingsDialog.tsx:36:26) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) + -renderRootSy + c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c + js/react-reconciler.development.js:12624:11) + -performWorkOnR + ot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12135:44) +" +`; + +exports[`SettingsDialog Snapshot Tests should render all boolean settings disabled correctly 1`] = ` +" + ERROR useVimMode must be used within a VimModeProvider + + src/ui/contexts/VimModeContext.tsx:87:15 + + 84: export const useVimMode = () => { + 85: const context = useContext(VimModeContext); + 86: if (context === undefined) { + 87: throw new Error('useVimMode must be used within a VimModeProvider'); + 88: } + 89: return context; + 90: }; + + - useVimMode (src/ui/contexts/VimModeContext.tsx:87:15) + - SettingsDialog (src/ui/components/SettingsDialog.tsx:36:26) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) + -renderRootSy + c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c + js/react-reconciler.development.js:12624:11) + -performWorkOnR + ot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12135:44) +" +`; diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap index fb62fbf975..87726abb1b 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap @@ -413,3 +413,46 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; + +exports[`SettingsDialog Initial Rendering should render settings list with visual indicators 1`] = ` +" + ERROR useVimMode must be used within a VimModeProvider + + src/ui/contexts/VimModeContext.tsx:87:15 + + 84: export const useVimMode = () => { + 85: const context = useContext(VimModeContext); + 86: if (context === undefined) { + 87: throw new Error('useVimMode must be used within a VimModeProvider'); + 88: } + 89: return context; + 90: }; + + - useVimMode (src/ui/contexts/VimModeContext.tsx:87:15) + - SettingsDialog (src/ui/components/SettingsDialog.tsx:36:26) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) + -renderRootSy + c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c + js/react-reconciler.development.js:12624:11) + -performWorkOnR + ot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12135:44) +" +`; diff --git a/packages/cli/src/ui/components/__snapshots__/StatsDisplay.sections.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/StatsDisplay.sections.test.tsx.snap index 64d7b3de7c..e12297cc79 100644 --- a/packages/cli/src/ui/components/__snapshots__/StatsDisplay.sections.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/StatsDisplay.sections.test.tsx.snap @@ -221,3 +221,225 @@ exports[` sections > performance metrics display > shows through │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; + +exports[` sections Title Rendering renders the default title when no title prop is provided 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` sections Title Rendering renders the custom title when a title prop is provided 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Agent powering down. Goodbye! │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` sections Quota Display renders quota information when quotaLines are provided 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Session API │ +│ Total Requests: 1 │ +│ Total Errors: 0 │ +│ Avg Latency: 100ms │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +│ Model Usage Reqs Input Tokens Cache Reads Output Tokens │ +│ ──────────────────────────────────────────────────────────────────────────── │ +│ gemini-2.5-pro 1 50 50 100 │ +│ Latency: 100ms avg / 100ms total │ +│ │ +│ Savings Highlight: 50 (50.0%) of input tokens were served from the cache, reducing costs. │ +│ │ +│ Quota Information │ +│ ## Anthropic Quota Information │ +│ │ +│ **Daily Usage** │ +│ Used: 1000 / 10000 tokens (10.0%) │ +│ Remaining: 9000 tokens │ +│ Resets: 2026-02-15 00:00:00 UTC │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` sections Quota Display does not render quota section when quotaLines are not provided 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` sections Quota Display handles empty quotaLines gracefully 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` sections Model Usage Table Updates should display separate Input Tokens and Cache Reads columns 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Session API │ +│ Total Requests: 5 │ +│ Total Errors: 0 │ +│ Avg Latency: 200ms │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +│ Model Usage Reqs Input Tokens Cache Reads Output Tokens │ +│ ──────────────────────────────────────────────────────────────────────────── │ +│ gemini-2.5-pro 5 600 400 500 │ +│ Latency: 200ms avg / 1.0s total │ +│ │ +│ Savings Highlight: 400 (40.0%) of input tokens were served from the cache, reducing costs. │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` sections Model Usage Table Updates should apply color to cache efficiency percentage 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Session API │ +│ Total Requests: 1 │ +│ Total Errors: 0 │ +│ Avg Latency: 100ms │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +│ Model Usage Reqs Input Tokens Cache Reads Output Tokens │ +│ ──────────────────────────────────────────────────────────────────────────── │ +│ gemini-2.5-pro 1 500 500 100 │ +│ Latency: 100ms avg / 100ms total │ +│ │ +│ Savings Highlight: 500 (50.0%) of input tokens were served from the cache, reducing costs. │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` sections performance metrics display shows throughput, TTFT, and output rate in performance section when available 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Session API │ +│ Total Requests: 2 │ +│ Total Errors: 0 │ +│ Avg Latency: 1.3s │ +│ │ +│ Interaction Summary │ +│ Session ID: test-session-id │ +│ Tool Calls: 1 ( OK 1 ERR 0 ) │ +│ » Total Duration: 250ms │ +│ Success Rate: 100.0% │ +│ User Agreement: 100.0% (1 reviewed) │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ » Throughput (TPM): 1.23k TPM (session weighted) │ +│ » TTFT (last): 187ms │ +│ » Output Gen Rate: 15.50 tok/s (session weighted) │ +│ » Input Rate (eff): 3200.00 tok/s (ΣP/ΣTTFT) │ +│ │ +│ │ +│ Model Usage Reqs Input Tokens Cache Reads Output Tokens │ +│ ──────────────────────────────────────────────────────────────────────────── │ +│ gemini-2.5-pro 2 600 300 400 │ +│ Latency: 1.3s avg / 2.5s total │ +│ │ +│ Savings Highlight: 300 (33.3%) of input tokens were served from the cache, reducing costs. │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` sections performance metrics display hides throughput, TTFT, and output rate when values are non-finite 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` sections performance metrics display hides throughput, TTFT, and output rate when values are unavailable 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` sections performance metrics display shows throughput when TPM is present even if TTFT/output rate are unavailable 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ » Throughput (TPM): 250.00 TPM (session weighted) │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; diff --git a/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap index 43bba71702..fa760b521d 100644 --- a/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap @@ -216,3 +216,220 @@ exports[` > renders only the Performance section in its zero sta │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; + +exports[` renders only the Performance section in its zero state 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` renders a table with two models correctly 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Session API │ +│ Total Requests: 8 │ +│ Total Errors: 1 │ +│ Avg Latency: 2.4s │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +│ Model Usage Reqs Input Tokens Cache Reads Output Tokens │ +│ ──────────────────────────────────────────────────────────────────────────── │ +│ gemini-2.5-pro 3 500 500 2,000 │ +│ Latency: 5.0s avg / 15.0s total │ +│ gemini-2.5-flash 5 15,000 10,000 15,000 │ +│ Latency: 900ms avg / 4.5s total (1 errors) │ +│ │ +│ Savings Highlight: 10,500 (40.4%) of input tokens were served from the cache, reducing costs. │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` renders all sections when all data is present 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Session API │ +│ Total Requests: 1 │ +│ Total Errors: 0 │ +│ Avg Latency: 100ms │ +│ │ +│ Interaction Summary │ +│ Session ID: test-session-id │ +│ Tool Calls: 2 ( OK 1 ERR 1 ) │ +│ » Total Duration: 123ms │ +│ Success Rate: 50.0% │ +│ User Agreement: 100.0% (1 reviewed) │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +│ Model Usage Reqs Input Tokens Cache Reads Output Tokens │ +│ ──────────────────────────────────────────────────────────────────────────── │ +│ gemini-2.5-pro 1 50 50 100 │ +│ Latency: 100ms avg / 100ms total │ +│ │ +│ Savings Highlight: 50 (50.0%) of input tokens were served from the cache, reducing costs. │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` Conditional Rendering Tests hides User Agreement when no decisions are made 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Interaction Summary │ +│ Session ID: test-session-id │ +│ Tool Calls: 2 ( OK 1 ERR 1 ) │ +│ » Total Duration: 123ms │ +│ Success Rate: 50.0% │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` Conditional Rendering Tests hides Efficiency section when cache is not used 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Session API │ +│ Total Requests: 1 │ +│ Total Errors: 0 │ +│ Avg Latency: 100ms │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +│ Model Usage Reqs Input Tokens Cache Reads Output Tokens │ +│ ──────────────────────────────────────────────────────────────────────────── │ +│ gemini-2.5-pro 1 100 0 100 │ +│ Latency: 100ms avg / 100ms total │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` Conditional Color Tests renders success rate in green for high values 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Interaction Summary │ +│ Session ID: test-session-id │ +│ Tool Calls: 10 ( OK 10 ERR 0 ) │ +│ Success Rate: 100.0% │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` Conditional Color Tests renders success rate in yellow for medium values 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Interaction Summary │ +│ Session ID: test-session-id │ +│ Tool Calls: 10 ( OK 9 ERR 1 ) │ +│ Success Rate: 90.0% │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` Conditional Color Tests renders success rate in red for low values 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Interaction Summary │ +│ Session ID: test-session-id │ +│ Tool Calls: 10 ( OK 5 ERR 5 ) │ +│ Success Rate: 50.0% │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` Code Changes Display displays Code Changes when line counts are present 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Interaction Summary │ +│ Session ID: test-session-id │ +│ Tool Calls: 1 ( OK 1 ERR 0 ) │ +│ » Total Duration: 100ms │ +│ Success Rate: 100.0% │ +│ Code Changes: +42 -18 │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` Code Changes Display hides Code Changes when no lines are added or removed 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Interaction Summary │ +│ Session ID: test-session-id │ +│ Tool Calls: 1 ( OK 1 ERR 0 ) │ +│ » Total Duration: 100ms │ +│ Success Rate: 100.0% │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; diff --git a/packages/cli/src/ui/components/__snapshots__/Table.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/Table.test.tsx.snap index 231784c6e1..a8fa29311c 100644 --- a/packages/cli/src/ui/components/__snapshots__/Table.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/Table.test.tsx.snap @@ -12,3 +12,16 @@ exports[`Table > should support custom cell rendering 1`] = ` ──────────────────────────────────────────────────────────────────────────────────────────────────── 20" `; + +exports[`Table should render headers and data correctly 1`] = ` +"ID Name +──────────────────────────────────────────────────────────────────────────────────────────────────── +1 Alice +2 Bob" +`; + +exports[`Table should support custom cell rendering 1`] = ` +"Value +──────────────────────────────────────────────────────────────────────────────────────────────────── +20" +`; diff --git a/packages/cli/src/ui/components/__snapshots__/ToolStatsDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ToolStatsDisplay.test.tsx.snap index ca7d1e2fd4..e2c09b7c7c 100644 --- a/packages/cli/src/ui/components/__snapshots__/ToolStatsDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/ToolStatsDisplay.test.tsx.snap @@ -89,3 +89,93 @@ exports[` > should render "no tool calls" message when there │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; + +exports[` should render "no tool calls" message when there are no active tools 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ No tool calls have been made in this session. │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` should display stats for a single tool correctly 1`] = ` +"╭────────────────────────────────────────────────────────────────────╮ +│ │ +│ Tool Stats │ +│ │ +│ Tool Name Calls Success Rate Avg Duration │ +│ ──────────────────────────────────────────────────────────────── │ +│ test-tool 1 100.0% 100ms │ +│ │ +│ User Decision Summary │ +│ Total Reviewed Suggestions: 1 │ +│ » Accepted: 1 │ +│ » Rejected: 0 │ +│ » Modified: 0 │ +│ ──────────────────────────────────────────────────────────────── │ +│ Overall Agreement Rate: 100.0% │ +│ │ +╰────────────────────────────────────────────────────────────────────╯" +`; + +exports[` should display stats for multiple tools correctly 1`] = ` +"╭────────────────────────────────────────────────────────────────────╮ +│ │ +│ Tool Stats │ +│ │ +│ Tool Name Calls Success Rate Avg Duration │ +│ ──────────────────────────────────────────────────────────────── │ +│ tool-a 2 50.0% 100ms │ +│ tool-b 1 100.0% 100ms │ +│ │ +│ User Decision Summary │ +│ Total Reviewed Suggestions: 3 │ +│ » Accepted: 1 │ +│ » Rejected: 1 │ +│ » Modified: 1 │ +│ ──────────────────────────────────────────────────────────────── │ +│ Overall Agreement Rate: 33.3% │ +│ │ +╰────────────────────────────────────────────────────────────────────╯" +`; + +exports[` should handle large values without wrapping or overlapping 1`] = ` +"╭────────────────────────────────────────────────────────────────────╮ +│ │ +│ Tool Stats │ +│ │ +│ Tool Name Calls Success Rate Avg Duration │ +│ ──────────────────────────────────────────────────────────────── │ +│ long-named-tool-for-testi99999999 88.9% 1ms │ +│ ng-wrapping-and-such 9 │ +│ │ +│ User Decision Summary │ +│ Total Reviewed Suggestions: 222234566 │ +│ » Accepted: 123456789 │ +│ » Rejected: 98765432 │ +│ » Modified: 12345 │ +│ ──────────────────────────────────────────────────────────────── │ +│ Overall Agreement Rate: 55.6% │ +│ │ +╰────────────────────────────────────────────────────────────────────╯" +`; + +exports[` should handle zero decisions gracefully 1`] = ` +"╭────────────────────────────────────────────────────────────────────╮ +│ │ +│ Tool Stats │ +│ │ +│ Tool Name Calls Success Rate Avg Duration │ +│ ──────────────────────────────────────────────────────────────── │ +│ test-tool 1 100.0% 100ms │ +│ │ +│ User Decision Summary │ +│ Total Reviewed Suggestions: 0 │ +│ » Accepted: 0 │ +│ » Rejected: 0 │ +│ » Modified: 0 │ +│ ──────────────────────────────────────────────────────────────── │ +│ Overall Agreement Rate: -- │ +│ │ +╰────────────────────────────────────────────────────────────────────╯" +`; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap index 6bea5eecd5..ce66c75a80 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap @@ -220,3 +220,130 @@ exports[` > Height Calculation > calculates available height │ │ ╰──────────────────────────────────────────────────────────────────────────────╯" `; + +exports[` Golden Snapshots renders single successful tool call 1`] = ` +" ╭────────────────────────────────────────────────────────────────────────────────────────────────── + │ Agent: helper-agent + │ + │MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) + ╰──────────────────────────────────────────────────────────────────────────────────────────────────" +`; + +exports[` Golden Snapshots renders multiple tool calls with different statuses 1`] = ` +" ╭────────────────────────────────────────────────────────────────────────────────────────────────── + │ Agent: helper-agent + │ + │MockTool[tool-1]: ✓ successful-tool - This tool succeeded (medium) + │ + │MockTool[tool-2]: o pending-tool - This tool is pending (medium) + │ + │MockTool[tool-3]: x error-tool - This tool failed (medium) + ╰──────────────────────────────────────────────────────────────────────────────────────────────────" +`; + +exports[` Golden Snapshots renders tool call awaiting confirmation 1`] = ` +" ╭────────────────────────────────────────────────────────────────────────────────────────────────── + │ Agent: helper-agent + │ + │MockTool[tool-confirm]: ? confirmation-tool - This tool needs confirmation (high) + │MockConfirmation: Are you sure you want to proceed? + ╰──────────────────────────────────────────────────────────────────────────────────────────────────" +`; + +exports[` Golden Snapshots renders shell command with yellow border 1`] = ` +" ╭────────────────────────────────────────────────────────────────────────────────────────────────── + │ Agent: helper-agent + │ + │MockTool[shell-1]: ✓ run_shell_command - Execute shell command (medium) + ╰──────────────────────────────────────────────────────────────────────────────────────────────────" +`; + +exports[` Golden Snapshots renders mixed tool calls including shell command 1`] = ` +" ╭────────────────────────────────────────────────────────────────────────────────────────────────── + │ Agent: helper-agent + │ + │MockTool[tool-1]: ✓ read_file - Read a file (medium) + │ + │MockTool[tool-2]: ⊷ run_shell_command - Run command (medium) + │ + │MockTool[tool-3]: o write_file - Write to file (medium) + ╰──────────────────────────────────────────────────────────────────────────────────────────────────" +`; + +exports[` Golden Snapshots renders with limited terminal height 1`] = ` +" ╭────────────────────────────────────────────────────────────────────────────────────────────────── + │ Agent: helper-agent + │ + │MockTool[tool-1]: ✓ tool-with-result - Tool with output (medium) + │ + │MockTool[tool-2]: ✓ another-tool - Another tool (medium) + ╰──────────────────────────────────────────────────────────────────────────────────────────────────" +`; + +exports[` Golden Snapshots renders when not focused 1`] = ` +" ╭────────────────────────────────────────────────────────────────────────────────────────────────── + │ Agent: helper-agent + │ + │MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) + ╰──────────────────────────────────────────────────────────────────────────────────────────────────" +`; + +exports[` Golden Snapshots renders with narrow terminal width 1`] = ` +" ╭────────────────────────────────────────────────────────────────────────────────────────────────── + │ Agent: helper-agent + │ + │MockTool[tool-123]: ✓ very-long-tool-name-that-might-wrap - This is a very long description that + │might cause wrapping issues (medium) + ╰──────────────────────────────────────────────────────────────────────────────────────────────────" +`; + +exports[` Golden Snapshots renders empty tool calls array 1`] = `""`; + +exports[` Border Color Logic uses yellow border when tools are pending 1`] = ` +" ╭────────────────────────────────────────────────────────────────────────────────────────────────── + │ Agent: helper-agent + │ + │MockTool[tool-123]: o test-tool - A tool for testing (medium) + ╰──────────────────────────────────────────────────────────────────────────────────────────────────" +`; + +exports[` Border Color Logic uses yellow border for shell commands even when successful 1`] = ` +" ╭────────────────────────────────────────────────────────────────────────────────────────────────── + │ Agent: helper-agent + │ + │MockTool[tool-123]: ✓ run_shell_command - A tool for testing (medium) + ╰──────────────────────────────────────────────────────────────────────────────────────────────────" +`; + +exports[` Border Color Logic uses gray border when all tools are successful and no shell commands 1`] = ` +" ╭────────────────────────────────────────────────────────────────────────────────────────────────── + │ Agent: helper-agent + │ + │MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) + │ + │MockTool[tool-2]: ✓ another-tool - A tool for testing (medium) + ╰──────────────────────────────────────────────────────────────────────────────────────────────────" +`; + +exports[` Height Calculation calculates available height correctly with multiple tools with results 1`] = ` +" ╭────────────────────────────────────────────────────────────────────────────────────────────────── + │ Agent: helper-agent + │ + │MockTool[tool-1]: ✓ test-tool - A tool for testing (medium) + │ + │MockTool[tool-2]: ✓ test-tool - A tool for testing (medium) + │ + │MockTool[tool-3]: ✓ test-tool - A tool for testing (medium) + ╰──────────────────────────────────────────────────────────────────────────────────────────────────" +`; + +exports[` Confirmation Handling shows confirmation dialog for first confirming tool only 1`] = ` +" ╭────────────────────────────────────────────────────────────────────────────────────────────────── + │ Agent: helper-agent + │ + │MockTool[tool-1]: ? first-confirm - A tool for testing (high) + │MockConfirmation: Confirm first tool + │ + │MockTool[tool-2]: ? second-confirm - A tool for testing (low) + ╰──────────────────────────────────────────────────────────────────────────────────────────────────" +`; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolMessage.test.tsx.snap index 10d3c077ad..4cf23180f0 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolMessage.test.tsx.snap @@ -36,3 +36,40 @@ exports[` > renders emphasis correctly 2`] = ` │ │ │ MockMarkdown:Test result │" `; + +exports[` renders DiffRenderer for diff results 1`] = ` +"│ │ +│ ✓ │ +│ test-tool A tool for testing │ +│ │ +│ MockDiff:--- a/file.txt │ +│ +++ b/file.txt │ +│ @@ -1 +1 @@ │ +│ -old │ +│ +new │" +`; + +exports[` renders emphasis correctly 1`] = ` +"│ │ +│ ✓ │ +│ test-tool A tool for testing │ +│ ← │ +│ │ +│ MockMarkdown:Test result │" +`; + +exports[` renders emphasis correctly 2`] = ` +"│ │ +│ ✓ │ +│ test-tool A tool for testing │ +│ │ +│ MockMarkdown:Test result │" +`; + +exports[` renders AnsiOutputText for AnsiOutput results 1`] = ` +"│ │ +│ ✓ │ +│ test-tool A tool for testing │ +│ │ +│ hello │" +`; diff --git a/packages/cli/src/ui/components/views/__snapshots__/ChatList.test.tsx.snap b/packages/cli/src/ui/components/views/__snapshots__/ChatList.test.tsx.snap index eddfa2ca2b..6da12d6ab0 100644 --- a/packages/cli/src/ui/components/views/__snapshots__/ChatList.test.tsx.snap +++ b/packages/cli/src/ui/components/views/__snapshots__/ChatList.test.tsx.snap @@ -18,3 +18,22 @@ Note: Newest last, oldest first" `; exports[` > renders correctly with no chats 1`] = `"No saved conversation checkpoints found."`; + +exports[` renders correctly with a list of chats 1`] = ` +"List of saved conversations: + + - chat-1 (2025-10-02 10:00:00) + - another-chat (2025-10-01 12:30:00) + +Note: Newest last, oldest first" +`; + +exports[` renders correctly with no chats 1`] = `"No saved conversation checkpoints found."`; + +exports[` handles invalid date formats gracefully 1`] = ` +"List of saved conversations: + + - bad-date-chat (Invalid Date) + +Note: Newest last, oldest first" +`; diff --git a/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap b/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap index c340dc8f61..3d7a868982 100644 --- a/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap +++ b/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap @@ -179,3 +179,183 @@ exports[` > with 'Windows' line endings > renders unordered l `; exports[` > with 'Windows' line endings > shows line numbers in code blocks by default 1`] = `" 1 const x = 1;"`; + +exports[` renders nothing for empty text 1`] = `""`; + +exports[` renders a simple paragraph 1`] = `"Hello, world."`; + +exports[` with Windows line endings renders headers with correct levels 1`] = ` +"Header 1 +Header 2 +Header 3 +Header 4 +" +`; + +exports[` with Windows line endings renders a fenced code block with a language 1`] = ` +" const x = 1; + console.log(x);" +`; + +exports[` with Windows line endings renders a fenced code block without a language 1`] = `" plain text"`; + +exports[` with Windows line endings handles unclosed (pending) code blocks 1`] = `" let y = 2;"`; + +exports[` with Windows line endings renders unordered lists with different markers 1`] = ` +" - item A + * item B + + item C +" +`; + +exports[` with Windows line endings renders nested unordered lists 1`] = ` +" * Level 1 + * Level 2 + * Level 3 +" +`; + +exports[` with Windows line endings renders ordered lists 1`] = ` +" 1. First item + 2. Second item +" +`; + +exports[` with Windows line endings renders horizontal rules 1`] = ` +"Hello +--- +World +--- +Test +" +`; + +exports[` with Windows line endings renders tables correctly 1`] = ` +" +┌──────────┬──────────┐ +│ Header 1 │ Header 2 │ +├──────────┼──────────┤ +│ Cell 1 │ Cell 2 │ +│ Cell 3 │ Cell 4 │ +└──────────┴──────────┘ +" +`; + +exports[` with Windows line endings handles a table at the end of the input 1`] = ` +"Some text before. +| A | B | +|---| +| 1 | 2 |" +`; + +exports[` with Windows line endings inserts a single space between paragraphs 1`] = ` +"Paragraph 1. + +Paragraph 2." +`; + +exports[` with Windows line endings correctly parses a mix of markdown elements 1`] = ` +"Main Title + +Here is a paragraph. + + - List item 1 + - List item 2 + + some code + +Another paragraph. +" +`; + +exports[` with Windows line endings hides line numbers in code blocks when showLineNumbers is false 1`] = `" const x = 1;"`; + +exports[` with Windows line endings shows line numbers in code blocks by default 1`] = `" const x = 1;"`; + +exports[` with Unix line endings renders headers with correct levels 1`] = ` +"Header 1 +Header 2 +Header 3 +Header 4 +" +`; + +exports[` with Unix line endings renders a fenced code block with a language 1`] = ` +" const x = 1; + console.log(x);" +`; + +exports[` with Unix line endings renders a fenced code block without a language 1`] = `" plain text"`; + +exports[` with Unix line endings handles unclosed (pending) code blocks 1`] = `" let y = 2;"`; + +exports[` with Unix line endings renders unordered lists with different markers 1`] = ` +" - item A + * item B + + item C +" +`; + +exports[` with Unix line endings renders nested unordered lists 1`] = ` +" * Level 1 + * Level 2 + * Level 3 +" +`; + +exports[` with Unix line endings renders ordered lists 1`] = ` +" 1. First item + 2. Second item +" +`; + +exports[` with Unix line endings renders horizontal rules 1`] = ` +"Hello +--- +World +--- +Test +" +`; + +exports[` with Unix line endings renders tables correctly 1`] = ` +" +┌──────────┬──────────┐ +│ Header 1 │ Header 2 │ +├──────────┼──────────┤ +│ Cell 1 │ Cell 2 │ +│ Cell 3 │ Cell 4 │ +└──────────┴──────────┘ +" +`; + +exports[` with Unix line endings handles a table at the end of the input 1`] = ` +"Some text before. +| A | B | +|---| +| 1 | 2 |" +`; + +exports[` with Unix line endings inserts a single space between paragraphs 1`] = ` +"Paragraph 1. + +Paragraph 2." +`; + +exports[` with Unix line endings correctly parses a mix of markdown elements 1`] = ` +"Main Title + +Here is a paragraph. + + - List item 1 + - List item 2 + + some code + +Another paragraph. +" +`; + +exports[` with Unix line endings hides line numbers in code blocks when showLineNumbers is false 1`] = `" const x = 1;"`; + +exports[` with Unix line endings shows line numbers in code blocks by default 1`] = `" const x = 1;"`; diff --git a/project-plans/issue2843/plan.md b/project-plans/issue2843/plan.md new file mode 100644 index 0000000000..47a6988ab4 --- /dev/null +++ b/project-plans/issue2843/plan.md @@ -0,0 +1,91 @@ +# Issue #2843 — Migrate the CLI workspace to Bun-native test execution + +## Behavior to deliver + +The `cli` workspace must execute its entire unit-test suite with Bun's native +test runner instead of Vitest. + +1. `packages/cli/package.json` `test` and `test:ci` run Bun; `test:vitest` + remains as the transitional fallback. +2. Every non-integration test file under `packages/cli/` is discovered and + executed. No manifest, allow-list, or exclusion list filters the run. +3. No test is dropped, filtered, or newly skipped relative to the Vitest run. +4. Vitest-only APIs that Bun cannot support are refactored in the affected + test files rather than silenced. +5. CI runs the CLI workspace under Bun on the required platforms. + +## Inputs and boundaries + +- **Discovery root**: `src/`, `test/`, `test-bun/`, `test-utils/`. +- **Selected**: `*.test.ts`, `*.test.tsx`, `*.spec.ts`, `*.spec.tsx`. +- **Excluded**: `*.integration.test.*` / `*.integration.spec.*`. These remain + owned by `test:integration`, exactly as under `vitest.config.ts`. +- **Isolation**: one `bun test` process per file. Bun's `mock.module` registry + is process-wide, so a shared process would leak module mocks between files. +- **Discovered count**: 649 unit test files (675 total minus 26 integration). + +## Baseline measurement + +`bun run-bun-tests.ts` against unmodified sources: **472 / 649 files passing**. + +The 177 failures fell into a small number of root causes, all confirmed by +direct probes rather than inspection: + +| Root cause | Scope | +| --- | --- | +| `vi.mock()` with an `async` factory never registered the mock | shared shim | +| Preload cleanup helpers replaced by a test's `@vybestack/llxprt-code-core` mock | cli preload | +| `describe`/`it`/`expect` used as globals (Vitest `globals: true`) | cli preload | +| Automock invoked prototype getters (`node:child_process`) and threw | shared shim | +| `.js` specifiers that map to `.tsx` sources failed to resolve | shared shim | +| `vi.resetModules()` / `vi.unmock()` (unsupported by Bun) | per-file refactor | +| Module-scope capture of a mock before its `vi.mock()` call | per-file refactor | +| `resolves.not.toThrow()` (broken under Bun) | per-file refactor | +| `@fast-check/vitest` | per-file refactor | +| Chai-style `expect(x).equals(y)` | per-file refactor | + +## Infrastructure changes (shared) + +`test-setup/augment-bun-vi.ts` + +- Async `vi.mock` factories are now settled synchronously with + `drainMicrotasks()` from `bun:jsc` and registered before the test module body + continues. Vitest hoists `vi.mock`, so registration must not be deferred to a + microtask. A genuinely pending factory still falls back to deferred + re-registration, and that fallback now also registers the absolute resolved + specifier because Bun resolves a relative `mock.module` specifier against the + module executing at call time. +- Automock copies accessor properties as accessors instead of reading them, so + a prototype getter such as `ChildProcess.prototype.stdin` cannot abort the + automock. + +`test-setup/module-resolution.ts` + +- A `.js` specifier now falls back to `.ts` **and** `.tsx`. + +`packages/cli/bun-test-setup.ts` + +- Captures `DebugLogger.resetForTesting` and + `clearActiveProviderRuntimeContext` at preload time so a test that mocks the + core package cannot break the shared `afterEach` cleanup. +- Publishes the `bun:test` lifecycle/assertion functions as globals, matching + the `globals: true` contract of `vitest.config.ts`. + +## Tests that prove it + +Behavioral, no mock theater: + +1. `packages/cli/run-bun-tests.test.ts` — discovery contract: unit test files + are selected, integration files and non-test files are not. +2. `test-setup/augment-bun-vi.test.ts` — an async `vi.mock` factory is visible + to a module-scope capture taken after the `vi.mock` call, and automocking a + module with a throwing prototype getter succeeds. +3. `scripts/tests/bun-workspaces.test.ts` — the CLI package scripts and CI + workflow run the workspace under Bun. +4. The suite itself: `bun run-bun-tests.ts` exits 0 with every discovered file + passing, and the discovered file count matches the file system. + +## Verification + +`npm run test`, `npm run lint`, `npm run typecheck`, `npm run format`, +`npm run build`, and the CLI smoke test. diff --git a/test-setup/augment-bun-vi.ts b/test-setup/augment-bun-vi.ts index 36e6716d16..c8d87755f3 100644 --- a/test-setup/augment-bun-vi.ts +++ b/test-setup/augment-bun-vi.ts @@ -25,6 +25,7 @@ import { describe as bunDescribe, expect, } from 'bun:test'; +import { drainMicrotasks } from 'bun:jsc'; import { createRequire, isBuiltin } from 'node:module'; import { StubRegistry, @@ -358,14 +359,19 @@ function automockValue( value: state.prototype, }); for (const key of Reflect.ownKeys(value)) { - if (!['length', 'name', 'prototype'].includes(String(key))) { - Object.defineProperty(mockedConstructor, key, { - configurable: true, - enumerable: true, - writable: true, - value: automockValue(Reflect.get(value, key), references), - }); + if (['length', 'name', 'prototype'].includes(String(key))) continue; + const staticDescriptor = Object.getOwnPropertyDescriptor(value, key); + if (!staticDescriptor) continue; + if (!('value' in staticDescriptor)) { + Object.defineProperty(mockedConstructor, key, staticDescriptor); + continue; } + Object.defineProperty(mockedConstructor, key, { + configurable: true, + enumerable: true, + writable: true, + value: automockValue(staticDescriptor.value, references), + }); } return mockedConstructor; } @@ -379,16 +385,34 @@ function automockValue( for (const key of Reflect.ownKeys(value)) { const descriptor = Object.getOwnPropertyDescriptor(value, key); if (!descriptor) continue; + // Accessor properties are copied as accessors rather than read. Invoking a + // getter on a prototype (e.g. ChildProcess.prototype.stdin in + // node:child_process) throws because `this` is the prototype and not a + // real instance, which would abort the whole automock. + if (!('value' in descriptor)) { + Object.defineProperty(mockedObject, key, descriptor); + continue; + } Object.defineProperty(mockedObject, key, { configurable: true, enumerable: descriptor.enumerable, writable: true, - value: automockValue(Reflect.get(value, key), references), + value: automockValue(descriptor.value, references), }); } return mockedObject; } +/** + * Normalizes a mock factory result into a module namespace object. Factories + * that return a non-object (rare, but legal for default-only modules) are + * wrapped so consumers still see a `default` export. + */ +const toNamespace = (exports: unknown): object => + typeof exports === 'object' && exports !== null + ? (exports as object) + : { default: exports }; + const registerModuleMock = ( id: string, factory?: (importOriginal: () => Promise) => unknown, @@ -477,21 +501,34 @@ const registerModuleMock = ( return mock.module(mockId, () => factoryResult as object); } - // Async factory result — Bun can't await inside mock.module factories. - // Register a placeholder (real module) and re-register when resolved. - // This is a race: if the module is imported before the factory resolves, - // the real module is returned. In practice, vi.mock() is called at module - // evaluation time, well before the module is first imported by test code. - // IMPORTANT: Factory bodies that call `await import('./local.js')` will - // STILL hang because ESM import() inside factories is intercepted by - // mock.module. Those test files must be refactored. + // Async factory result — Bun cannot await inside a mock.module factory, and + // Vitest hoists vi.mock() so the mocked exports exist before the test module + // body runs. Test modules rely on that ordering (e.g. `const spy = imported + // as Mock` at module scope), so deferring registration to a microtask is not + // equivalent. Every async factory in this repository only awaits values that + // are already available synchronously (vi.importActual / importOriginal), so + // draining the microtask queue settles the promise immediately and lets the + // mock be registered before the module body continues. + drainMicrotasks(); + const settled = Bun.peek(factoryResult); + if (settled !== factoryResult) { + return mock.module(mockId, () => toNamespace(settled)); + } + + // Genuinely pending factory (real async work). Register a placeholder with + // the real module and re-register once the promise settles. mock.module(mockId, () => syncActual as object); factoryResult .then((exports) => { - if (typeof exports === 'object' && exports !== null) { - mock.module(mockId, () => exports as object); - } else { - mock.module(mockId, () => ({ default: exports })); + const namespace = toNamespace(exports); + // Bun resolves a *relative* mock.module specifier against the module + // that is executing when the call is made. Re-registration happens in a + // microtask, after the test module finished evaluating, so a relative + // specifier no longer resolves to the same module and the mock silently + // does not apply. Registering the absolute path resolved at vi.mock() + // time is caller-independent and always targets the right module. + for (const id of new Set([mockId, resolvedId])) { + mock.module(id, () => namespace); } }) .catch(() => { diff --git a/test-setup/module-resolution.ts b/test-setup/module-resolution.ts index 475d9cbc18..682cfdef88 100644 --- a/test-setup/module-resolution.ts +++ b/test-setup/module-resolution.ts @@ -33,13 +33,28 @@ const callerPath = (): string => { throw new Error('Unable to determine the caller for a relative module path'); }; +/** + * TypeScript sources are imported with a `.js` suffix throughout this + * repository, so a `.js` specifier may map to either a `.ts` or a `.tsx` + * source file (React components use `.tsx`). + */ +const TYPESCRIPT_SOURCE_EXTENSIONS = ['.ts', '.tsx'] as const; + const resolveFromCaller = (specifier: string, caller: string): string => { const directory = dirname(caller); try { return Bun.resolveSync(specifier, directory); } catch (error: unknown) { if (!specifier.endsWith('.js')) throw error; - return Bun.resolveSync(`${specifier.slice(0, -3)}.ts`, directory); + const base = specifier.slice(0, -3); + for (const extension of TYPESCRIPT_SOURCE_EXTENSIONS) { + try { + return Bun.resolveSync(`${base}${extension}`, directory); + } catch { + // Try the next candidate extension. + } + } + throw error; } }; From bd5300f05634bbf76006d4961b0a6316175e9d18 Mon Sep 17 00:00:00 2001 From: acoliver Date: Mon, 3 Aug 2026 13:58:38 -0300 Subject: [PATCH 002/128] revert bun-written snapshot entries --- .../HistoryItemDisplay.test.tsx.snap | 160 -------- .../InputPrompt.paste.test.tsx.snap | 78 ---- .../InputPrompt.vim.test.tsx.snap | 156 -------- .../ModelStatsDisplay.test.tsx.snap | 137 ------- .../SettingsDialog.interactions.test.tsx.snap | 344 ------------------ .../SettingsDialog.test.tsx.snap | 43 --- .../StatsDisplay.sections.test.tsx.snap | 222 ----------- .../__snapshots__/StatsDisplay.test.tsx.snap | 217 ----------- .../__snapshots__/Table.test.tsx.snap | 13 - .../ToolStatsDisplay.test.tsx.snap | 90 ----- .../ToolGroupMessage.test.tsx.snap | 127 ------- .../__snapshots__/ToolMessage.test.tsx.snap | 37 -- .../__snapshots__/ChatList.test.tsx.snap | 19 - .../MarkdownDisplay.test.tsx.snap | 180 --------- 14 files changed, 1823 deletions(-) diff --git a/packages/cli/src/ui/components/__snapshots__/HistoryItemDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/HistoryItemDisplay.test.tsx.snap index 225db45110..c1585a9e81 100644 --- a/packages/cli/src/ui/components/__snapshots__/HistoryItemDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/HistoryItemDisplay.test.tsx.snap @@ -135,163 +135,3 @@ exports[` > should render a truncated gemini_content item 49 Line 49 50 Line 50" `; - -exports[` should render a truncated gemini item 1`] = ` -" - ERROR RuntimeContextProvider is missing from the component tree. - - src/ui/contexts/RuntimeContext.tsx:245:15 - - 242: export function useRuntimeBridge(): RuntimeContextBridge { - 243: const context = useContext(RuntimeContext); - 244: if (!context) { - 245: throw new Error( - 246: 'RuntimeContextProvider is missing from the component tree.', - 247: ); - 248: } - - - useRuntimeBridge (src/ui/contexts/RuntimeContext.tsx:245:15) - - useRuntimeApi (src/ui/contexts/RuntimeContext.tsx:253:10) - - AiMessage (src/ui/components/messages/AiMessage.tsx:54:35) - -react-stack-bottom- - rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon - ciler/cjs/react-reconciler.development.js:15859:20) - -renderWithHoo - s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ - cjs/react-reconciler.development.js:3221:22) - -updateFunctionComp - nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc - iler/cjs/react-reconciler.development.js:6475:19) - -runWithFiberIn - EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:1738:13) - -performUnitOfW - rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12834:22) - -workLoopSyn - (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj - s/react-reconciler.development.js:12644:41) - -renderRootSy - c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c - js/react-reconciler.development.js:12624:11) -" -`; - -exports[` should render a full gemini item when using availableTerminalHeightAi 1`] = ` -" - ERROR RuntimeContextProvider is missing from the component tree. - - src/ui/contexts/RuntimeContext.tsx:245:15 - - 242: export function useRuntimeBridge(): RuntimeContextBridge { - 243: const context = useContext(RuntimeContext); - 244: if (!context) { - 245: throw new Error( - 246: 'RuntimeContextProvider is missing from the component tree.', - 247: ); - 248: } - - - useRuntimeBridge (src/ui/contexts/RuntimeContext.tsx:245:15) - - useRuntimeApi (src/ui/contexts/RuntimeContext.tsx:253:10) - - AiMessage (src/ui/components/messages/AiMessage.tsx:54:35) - -react-stack-bottom- - rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon - ciler/cjs/react-reconciler.development.js:15859:20) - -renderWithHoo - s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ - cjs/react-reconciler.development.js:3221:22) - -updateFunctionComp - nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc - iler/cjs/react-reconciler.development.js:6475:19) - -runWithFiberIn - EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:1738:13) - -performUnitOfW - rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12834:22) - -workLoopSyn - (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj - s/react-reconciler.development.js:12644:41) - -renderRootSy - c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c - js/react-reconciler.development.js:12624:11) -" -`; - -exports[` should render a truncated gemini_content item 1`] = ` -" - ERROR RuntimeContextProvider is missing from the component tree. - - src/ui/contexts/RuntimeContext.tsx:245:15 - - 242: export function useRuntimeBridge(): RuntimeContextBridge { - 243: const context = useContext(RuntimeContext); - 244: if (!context) { - 245: throw new Error( - 246: 'RuntimeContextProvider is missing from the component tree.', - 247: ); - 248: } - - - useRuntimeBridge (src/ui/contexts/RuntimeContext.tsx:245:15) - - useRuntimeApi (src/ui/contexts/RuntimeContext.tsx:253:10) - - useResolvedWorkspaceDirectories (src/ui/hooks/useResolvedWorkspaceDirectories.ts:24:37) - - AiMessageContent (src/ui/components/messages/AiMessageContent.tsx:36:5) - -react-stack-bottom- - rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon - ciler/cjs/react-reconciler.development.js:15859:20) - -renderWithHoo - s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ - cjs/react-reconciler.development.js:3221:22) - -updateFunctionComp - nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc - iler/cjs/react-reconciler.development.js:6475:19) - -runWithFiberIn - EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:1738:13) - -performUnitOfW - rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12834:22) - -workLoopSyn - (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj - s/react-reconciler.development.js:12644:41) -" -`; - -exports[` should render a full gemini_content item when using availableTerminalHeightAi 1`] = ` -" - ERROR RuntimeContextProvider is missing from the component tree. - - src/ui/contexts/RuntimeContext.tsx:245:15 - - 242: export function useRuntimeBridge(): RuntimeContextBridge { - 243: const context = useContext(RuntimeContext); - 244: if (!context) { - 245: throw new Error( - 246: 'RuntimeContextProvider is missing from the component tree.', - 247: ); - 248: } - - - useRuntimeBridge (src/ui/contexts/RuntimeContext.tsx:245:15) - - useRuntimeApi (src/ui/contexts/RuntimeContext.tsx:253:10) - - useResolvedWorkspaceDirectories (src/ui/hooks/useResolvedWorkspaceDirectories.ts:24:37) - - AiMessageContent (src/ui/components/messages/AiMessageContent.tsx:36:5) - -react-stack-bottom- - rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon - ciler/cjs/react-reconciler.development.js:15859:20) - -renderWithHoo - s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ - cjs/react-reconciler.development.js:3221:22) - -updateFunctionComp - nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc - iler/cjs/react-reconciler.development.js:6475:19) - -runWithFiberIn - EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:1738:13) - -performUnitOfW - rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12834:22) - -workLoopSyn - (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj - s/react-reconciler.development.js:12644:41) -" -`; diff --git a/packages/cli/src/ui/components/__snapshots__/InputPrompt.paste.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/InputPrompt.paste.test.tsx.snap index 0ac4b406f2..6b4cebca98 100644 --- a/packages/cli/src/ui/components/__snapshots__/InputPrompt.paste.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/InputPrompt.paste.test.tsx.snap @@ -123,81 +123,3 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match - performUnitOfWork (node_modules/react-reconciler/cjs/react-reconciler.development.js:12834:22) " `; - -exports[`InputPrompt command search (Ctrl+R when not in shell) renders match window and expanded view (snapshots): command-search-render-collapsed-match 1`] = ` -" - ERROR useMouseContext must be used within a MouseProvider - - src/ui/contexts/MouseContext.tsx:40:15 - - 37: export function useMouseContext() { - 38: const context = useContext(MouseContext); - 39: if (!context) { - 40: throw new Error('useMouseContext must be used within a MouseProvider'); - 41: } - 42: return context; - 43: } - - - useMouseContext (src/ui/contexts/MouseContext.tsx:40:15) - - useMouse (src/ui/hooks/useMouse.ts:24:38) - - useMousePaste (src/ui/components/inputPromptHooks.ts:544:3) - - InputPrompt (src/ui/components/InputPrompt.tsx:69:3) - -react-stack-bottom- - rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon - ciler/cjs/react-reconciler.development.js:15859:20) - -renderWithHoo - s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ - cjs/react-reconciler.development.js:3221:22) - -updateFunctionComp - nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc - iler/cjs/react-reconciler.development.js:6475:19) - -runWithFiberIn - EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:1738:13) - -performUnitOfW - rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12834:22) - -workLoopSyn - (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj - s/react-reconciler.development.js:12644:41) -" -`; - -exports[`InputPrompt command search (Ctrl+R when not in shell) renders match window and expanded view (snapshots): command-search-render-expanded-match 1`] = ` -" - ERROR useMouseContext must be used within a MouseProvider - - src/ui/contexts/MouseContext.tsx:40:15 - - 37: export function useMouseContext() { - 38: const context = useContext(MouseContext); - 39: if (!context) { - 40: throw new Error('useMouseContext must be used within a MouseProvider'); - 41: } - 42: return context; - 43: } - - - useMouseContext (src/ui/contexts/MouseContext.tsx:40:15) - - useMouse (src/ui/hooks/useMouse.ts:24:38) - - useMousePaste (src/ui/components/inputPromptHooks.ts:544:3) - - InputPrompt (src/ui/components/InputPrompt.tsx:69:3) - -react-stack-bottom- - rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon - ciler/cjs/react-reconciler.development.js:15859:20) - -renderWithHoo - s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ - cjs/react-reconciler.development.js:3221:22) - -updateFunctionComp - nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc - iler/cjs/react-reconciler.development.js:6475:19) - -runWithFiberIn - EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:1738:13) - -performUnitOfW - rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12834:22) - -workLoopSyn - (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj - s/react-reconciler.development.js:12644:41) -" -`; diff --git a/packages/cli/src/ui/components/__snapshots__/InputPrompt.vim.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/InputPrompt.vim.test.tsx.snap index 324763d2d6..0ab53ce3b8 100644 --- a/packages/cli/src/ui/components/__snapshots__/InputPrompt.vim.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/InputPrompt.vim.test.tsx.snap @@ -247,159 +247,3 @@ exports[`InputPrompt > snapshots > should render correctly when accepting edits - performUnitOfWork (node_modules/react-reconciler/cjs/react-reconciler.development.js:12834:22) " `; - -exports[`InputPrompt snapshots should render correctly in shell mode 1`] = ` -" - ERROR useMouseContext must be used within a MouseProvider - - src/ui/contexts/MouseContext.tsx:40:15 - - 37: export function useMouseContext() { - 38: const context = useContext(MouseContext); - 39: if (!context) { - 40: throw new Error('useMouseContext must be used within a MouseProvider'); - 41: } - 42: return context; - 43: } - - - useMouseContext (src/ui/contexts/MouseContext.tsx:40:15) - - useMouse (src/ui/hooks/useMouse.ts:24:38) - - useMousePaste (src/ui/components/inputPromptHooks.ts:544:3) - - InputPrompt (src/ui/components/InputPrompt.tsx:69:3) - -react-stack-bottom- - rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon - ciler/cjs/react-reconciler.development.js:15859:20) - -renderWithHoo - s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ - cjs/react-reconciler.development.js:3221:22) - -updateFunctionComp - nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc - iler/cjs/react-reconciler.development.js:6475:19) - -runWithFiberIn - EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:1738:13) - -performUnitOfW - rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12834:22) - -workLoopSyn - (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj - s/react-reconciler.development.js:12644:41) -" -`; - -exports[`InputPrompt snapshots should render correctly when accepting edits 1`] = ` -" - ERROR useMouseContext must be used within a MouseProvider - - src/ui/contexts/MouseContext.tsx:40:15 - - 37: export function useMouseContext() { - 38: const context = useContext(MouseContext); - 39: if (!context) { - 40: throw new Error('useMouseContext must be used within a MouseProvider'); - 41: } - 42: return context; - 43: } - - - useMouseContext (src/ui/contexts/MouseContext.tsx:40:15) - - useMouse (src/ui/hooks/useMouse.ts:24:38) - - useMousePaste (src/ui/components/inputPromptHooks.ts:544:3) - - InputPrompt (src/ui/components/InputPrompt.tsx:69:3) - -react-stack-bottom- - rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon - ciler/cjs/react-reconciler.development.js:15859:20) - -renderWithHoo - s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ - cjs/react-reconciler.development.js:3221:22) - -updateFunctionComp - nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc - iler/cjs/react-reconciler.development.js:6475:19) - -runWithFiberIn - EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:1738:13) - -performUnitOfW - rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12834:22) - -workLoopSyn - (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj - s/react-reconciler.development.js:12644:41) -" -`; - -exports[`InputPrompt snapshots should render correctly in yolo mode 1`] = ` -" - ERROR useMouseContext must be used within a MouseProvider - - src/ui/contexts/MouseContext.tsx:40:15 - - 37: export function useMouseContext() { - 38: const context = useContext(MouseContext); - 39: if (!context) { - 40: throw new Error('useMouseContext must be used within a MouseProvider'); - 41: } - 42: return context; - 43: } - - - useMouseContext (src/ui/contexts/MouseContext.tsx:40:15) - - useMouse (src/ui/hooks/useMouse.ts:24:38) - - useMousePaste (src/ui/components/inputPromptHooks.ts:544:3) - - InputPrompt (src/ui/components/InputPrompt.tsx:69:3) - -react-stack-bottom- - rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon - ciler/cjs/react-reconciler.development.js:15859:20) - -renderWithHoo - s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ - cjs/react-reconciler.development.js:3221:22) - -updateFunctionComp - nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc - iler/cjs/react-reconciler.development.js:6475:19) - -runWithFiberIn - EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:1738:13) - -performUnitOfW - rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12834:22) - -workLoopSyn - (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj - s/react-reconciler.development.js:12644:41) -" -`; - -exports[`InputPrompt snapshots should not show inverted cursor when shell is focused 1`] = ` -" - ERROR useMouseContext must be used within a MouseProvider - - src/ui/contexts/MouseContext.tsx:40:15 - - 37: export function useMouseContext() { - 38: const context = useContext(MouseContext); - 39: if (!context) { - 40: throw new Error('useMouseContext must be used within a MouseProvider'); - 41: } - 42: return context; - 43: } - - - useMouseContext (src/ui/contexts/MouseContext.tsx:40:15) - - useMouse (src/ui/hooks/useMouse.ts:24:38) - - useMousePaste (src/ui/components/inputPromptHooks.ts:544:3) - - InputPrompt (src/ui/components/InputPrompt.tsx:69:3) - -react-stack-bottom- - rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon - ciler/cjs/react-reconciler.development.js:15859:20) - -renderWithHoo - s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ - cjs/react-reconciler.development.js:3221:22) - -updateFunctionComp - nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc - iler/cjs/react-reconciler.development.js:6475:19) - -runWithFiberIn - EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:1738:13) - -performUnitOfW - rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12834:22) - -workLoopSyn - (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj - s/react-reconciler.development.js:12644:41) -" -`; diff --git a/packages/cli/src/ui/components/__snapshots__/ModelStatsDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ModelStatsDisplay.test.tsx.snap index 6e711284fa..4a2ef97579 100644 --- a/packages/cli/src/ui/components/__snapshots__/ModelStatsDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/ModelStatsDisplay.test.tsx.snap @@ -136,140 +136,3 @@ exports[` > should render "no API calls" message when there │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; - -exports[` should render "no API calls" message when there are no active models 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ No API calls have been made in this session. │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` should not display conditional rows if no model has data for them 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Model Stats For Nerds │ -│ │ -│ Metric gemini-2.5-pro │ -│ ────────────────────────────────────────────────────────────────────────────────────────────── │ -│ API │ -│ Requests 1 │ -│ Errors 0 (0.0%) │ -│ Avg Latency 100ms │ -│ Tokens │ -│ Total 30 │ -│ ↳ Input 10 │ -│ ↳ Output 20 │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` should display conditional rows if at least one model has data 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Model Stats For Nerds │ -│ │ -│ Metric gemini-2.5-pro gemini-2.5-flash │ -│ ────────────────────────────────────────────────────────────────────────────────────────────── │ -│ API │ -│ Requests 1 1 │ -│ Errors 0 (0.0%) 0 (0.0%) │ -│ Avg Latency 100ms 50ms │ -│ Tokens │ -│ Total 30 15 │ -│ ↳ Input 5 5 │ -│ ↳ Cache Reads 5 (50.0%) 0 (0.0%) │ -│ ↳ Thoughts 2 0 │ -│ ↳ Tool 0 3 │ -│ ↳ Output 20 10 │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` should display stats for multiple models correctly 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Model Stats For Nerds │ -│ │ -│ Metric gemini-2.5-pro gemini-2.5-flash │ -│ ────────────────────────────────────────────────────────────────────────────────────────────── │ -│ API │ -│ Requests 10 20 │ -│ Errors 1 (10.0%) 2 (10.0%) │ -│ Avg Latency 100ms 25ms │ -│ Tokens │ -│ Total 300 600 │ -│ ↳ Input 50 100 │ -│ ↳ Cache Reads 50 (50.0%) 100 (50.0%) │ -│ ↳ Thoughts 10 20 │ -│ ↳ Tool 5 10 │ -│ ↳ Output 200 400 │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` should handle large values without wrapping or overlapping 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Model Stats For Nerds │ -│ │ -│ Metric gemini-2.5-pro │ -│ ────────────────────────────────────────────────────────────────────────────────────────────── │ -│ API │ -│ Requests 999,999,999 │ -│ Errors 123,456,789 (12.3%) │ -│ Avg Latency 0ms │ -│ Tokens │ -│ Total 999,999,999 │ -│ ↳ Input 864,197,532 │ -│ ↳ Cache Reads 123,456,789 (12.5%) │ -│ ↳ Thoughts 111,111,111 │ -│ ↳ Tool 222,222,222 │ -│ ↳ Output 123,456,789 │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` should display a single model correctly 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Model Stats For Nerds │ -│ │ -│ Metric gemini-2.5-pro │ -│ ────────────────────────────────────────────────────────────────────────────────────────────── │ -│ API │ -│ Requests 1 │ -│ Errors 0 (0.0%) │ -│ Avg Latency 100ms │ -│ Tokens │ -│ Total 30 │ -│ ↳ Input 5 │ -│ ↳ Cache Reads 5 (50.0%) │ -│ ↳ Thoughts 2 │ -│ ↳ Tool 1 │ -│ ↳ Output 20 │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` should handle models with long names (gemini-3-*-preview) without layout breaking 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Model Stats For Nerds │ -│ │ -│ Metric gemini-3-pro-preview gemini-3-flash-preview │ -│ ────────────────────────────────────────────────────────────────────────────────────────────── │ -│ API │ -│ Requests 10 20 │ -│ Errors 0 (0.0%) 0 (0.0%) │ -│ Avg Latency 200ms 50ms │ -│ Tokens │ -│ Total 6,000 12,000 │ -│ ↳ Input 1,000 2,000 │ -│ ↳ Cache Reads 500 (25.0%) 1,000 (25.0%) │ -│ ↳ Thoughts 100 200 │ -│ ↳ Tool 50 100 │ -│ ↳ Output 4,000 8,000 │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.interactions.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.interactions.test.tsx.snap index 751f1070e3..2072696099 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.interactions.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.interactions.test.tsx.snap @@ -367,347 +367,3 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; - -exports[`SettingsDialog Snapshot Tests should render default state correctly 1`] = ` -" - ERROR useVimMode must be used within a VimModeProvider - - src/ui/contexts/VimModeContext.tsx:87:15 - - 84: export const useVimMode = () => { - 85: const context = useContext(VimModeContext); - 86: if (context === undefined) { - 87: throw new Error('useVimMode must be used within a VimModeProvider'); - 88: } - 89: return context; - 90: }; - - - useVimMode (src/ui/contexts/VimModeContext.tsx:87:15) - - SettingsDialog (src/ui/components/SettingsDialog.tsx:36:26) - -react-stack-bottom- - rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon - ciler/cjs/react-reconciler.development.js:15859:20) - -renderWithHoo - s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ - cjs/react-reconciler.development.js:3221:22) - -updateFunctionComp - nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc - iler/cjs/react-reconciler.development.js:6475:19) - -runWithFiberIn - EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:1738:13) - -performUnitOfW - rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12834:22) - -workLoopSyn - (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj - s/react-reconciler.development.js:12644:41) - -renderRootSy - c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c - js/react-reconciler.development.js:12624:11) - -performWorkOnR - ot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12135:44) -" -`; - -exports[`SettingsDialog Snapshot Tests should render various boolean settings enabled correctly 1`] = ` -" - ERROR useVimMode must be used within a VimModeProvider - - src/ui/contexts/VimModeContext.tsx:87:15 - - 84: export const useVimMode = () => { - 85: const context = useContext(VimModeContext); - 86: if (context === undefined) { - 87: throw new Error('useVimMode must be used within a VimModeProvider'); - 88: } - 89: return context; - 90: }; - - - useVimMode (src/ui/contexts/VimModeContext.tsx:87:15) - - SettingsDialog (src/ui/components/SettingsDialog.tsx:36:26) - -react-stack-bottom- - rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon - ciler/cjs/react-reconciler.development.js:15859:20) - -renderWithHoo - s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ - cjs/react-reconciler.development.js:3221:22) - -updateFunctionComp - nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc - iler/cjs/react-reconciler.development.js:6475:19) - -runWithFiberIn - EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:1738:13) - -performUnitOfW - rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12834:22) - -workLoopSyn - (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj - s/react-reconciler.development.js:12644:41) - -renderRootSy - c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c - js/react-reconciler.development.js:12624:11) - -performWorkOnR - ot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12135:44) -" -`; - -exports[`SettingsDialog Snapshot Tests should render mixed boolean and number settings correctly 1`] = ` -" - ERROR useVimMode must be used within a VimModeProvider - - src/ui/contexts/VimModeContext.tsx:87:15 - - 84: export const useVimMode = () => { - 85: const context = useContext(VimModeContext); - 86: if (context === undefined) { - 87: throw new Error('useVimMode must be used within a VimModeProvider'); - 88: } - 89: return context; - 90: }; - - - useVimMode (src/ui/contexts/VimModeContext.tsx:87:15) - - SettingsDialog (src/ui/components/SettingsDialog.tsx:36:26) - -react-stack-bottom- - rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon - ciler/cjs/react-reconciler.development.js:15859:20) - -renderWithHoo - s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ - cjs/react-reconciler.development.js:3221:22) - -updateFunctionComp - nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc - iler/cjs/react-reconciler.development.js:6475:19) - -runWithFiberIn - EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:1738:13) - -performUnitOfW - rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12834:22) - -workLoopSyn - (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj - s/react-reconciler.development.js:12644:41) - -renderRootSy - c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c - js/react-reconciler.development.js:12624:11) - -performWorkOnR - ot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12135:44) -" -`; - -exports[`SettingsDialog Snapshot Tests should render focused on scope selector correctly 1`] = ` -" - ERROR useVimMode must be used within a VimModeProvider - - src/ui/contexts/VimModeContext.tsx:87:15 - - 84: export const useVimMode = () => { - 85: const context = useContext(VimModeContext); - 86: if (context === undefined) { - 87: throw new Error('useVimMode must be used within a VimModeProvider'); - 88: } - 89: return context; - 90: }; - - - useVimMode (src/ui/contexts/VimModeContext.tsx:87:15) - - SettingsDialog (src/ui/components/SettingsDialog.tsx:36:26) - -react-stack-bottom- - rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon - ciler/cjs/react-reconciler.development.js:15859:20) - -renderWithHoo - s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ - cjs/react-reconciler.development.js:3221:22) - -updateFunctionComp - nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc - iler/cjs/react-reconciler.development.js:6475:19) - -runWithFiberIn - EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:1738:13) - -performUnitOfW - rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12834:22) - -workLoopSyn - (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj - s/react-reconciler.development.js:12644:41) - -renderRootSy - c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c - js/react-reconciler.development.js:12624:11) - -performWorkOnR - ot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12135:44) -" -`; - -exports[`SettingsDialog Snapshot Tests should render accessibility settings enabled correctly 1`] = ` -" - ERROR useVimMode must be used within a VimModeProvider - - src/ui/contexts/VimModeContext.tsx:87:15 - - 84: export const useVimMode = () => { - 85: const context = useContext(VimModeContext); - 86: if (context === undefined) { - 87: throw new Error('useVimMode must be used within a VimModeProvider'); - 88: } - 89: return context; - 90: }; - - - useVimMode (src/ui/contexts/VimModeContext.tsx:87:15) - - SettingsDialog (src/ui/components/SettingsDialog.tsx:36:26) - -react-stack-bottom- - rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon - ciler/cjs/react-reconciler.development.js:15859:20) - -renderWithHoo - s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ - cjs/react-reconciler.development.js:3221:22) - -updateFunctionComp - nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc - iler/cjs/react-reconciler.development.js:6475:19) - -runWithFiberIn - EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:1738:13) - -performUnitOfW - rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12834:22) - -workLoopSyn - (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj - s/react-reconciler.development.js:12644:41) - -renderRootSy - c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c - js/react-reconciler.development.js:12624:11) - -performWorkOnR - ot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12135:44) -" -`; - -exports[`SettingsDialog Snapshot Tests should render file filtering settings configured correctly 1`] = ` -" - ERROR useVimMode must be used within a VimModeProvider - - src/ui/contexts/VimModeContext.tsx:87:15 - - 84: export const useVimMode = () => { - 85: const context = useContext(VimModeContext); - 86: if (context === undefined) { - 87: throw new Error('useVimMode must be used within a VimModeProvider'); - 88: } - 89: return context; - 90: }; - - - useVimMode (src/ui/contexts/VimModeContext.tsx:87:15) - - SettingsDialog (src/ui/components/SettingsDialog.tsx:36:26) - -react-stack-bottom- - rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon - ciler/cjs/react-reconciler.development.js:15859:20) - -renderWithHoo - s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ - cjs/react-reconciler.development.js:3221:22) - -updateFunctionComp - nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc - iler/cjs/react-reconciler.development.js:6475:19) - -runWithFiberIn - EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:1738:13) - -performUnitOfW - rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12834:22) - -workLoopSyn - (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj - s/react-reconciler.development.js:12644:41) - -renderRootSy - c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c - js/react-reconciler.development.js:12624:11) - -performWorkOnR - ot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12135:44) -" -`; - -exports[`SettingsDialog Snapshot Tests should render tools and security settings correctly 1`] = ` -" - ERROR useVimMode must be used within a VimModeProvider - - src/ui/contexts/VimModeContext.tsx:87:15 - - 84: export const useVimMode = () => { - 85: const context = useContext(VimModeContext); - 86: if (context === undefined) { - 87: throw new Error('useVimMode must be used within a VimModeProvider'); - 88: } - 89: return context; - 90: }; - - - useVimMode (src/ui/contexts/VimModeContext.tsx:87:15) - - SettingsDialog (src/ui/components/SettingsDialog.tsx:36:26) - -react-stack-bottom- - rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon - ciler/cjs/react-reconciler.development.js:15859:20) - -renderWithHoo - s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ - cjs/react-reconciler.development.js:3221:22) - -updateFunctionComp - nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc - iler/cjs/react-reconciler.development.js:6475:19) - -runWithFiberIn - EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:1738:13) - -performUnitOfW - rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12834:22) - -workLoopSyn - (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj - s/react-reconciler.development.js:12644:41) - -renderRootSy - c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c - js/react-reconciler.development.js:12624:11) - -performWorkOnR - ot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12135:44) -" -`; - -exports[`SettingsDialog Snapshot Tests should render all boolean settings disabled correctly 1`] = ` -" - ERROR useVimMode must be used within a VimModeProvider - - src/ui/contexts/VimModeContext.tsx:87:15 - - 84: export const useVimMode = () => { - 85: const context = useContext(VimModeContext); - 86: if (context === undefined) { - 87: throw new Error('useVimMode must be used within a VimModeProvider'); - 88: } - 89: return context; - 90: }; - - - useVimMode (src/ui/contexts/VimModeContext.tsx:87:15) - - SettingsDialog (src/ui/components/SettingsDialog.tsx:36:26) - -react-stack-bottom- - rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon - ciler/cjs/react-reconciler.development.js:15859:20) - -renderWithHoo - s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ - cjs/react-reconciler.development.js:3221:22) - -updateFunctionComp - nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc - iler/cjs/react-reconciler.development.js:6475:19) - -runWithFiberIn - EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:1738:13) - -performUnitOfW - rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12834:22) - -workLoopSyn - (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj - s/react-reconciler.development.js:12644:41) - -renderRootSy - c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c - js/react-reconciler.development.js:12624:11) - -performWorkOnR - ot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12135:44) -" -`; diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap index 87726abb1b..fb62fbf975 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap @@ -413,46 +413,3 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; - -exports[`SettingsDialog Initial Rendering should render settings list with visual indicators 1`] = ` -" - ERROR useVimMode must be used within a VimModeProvider - - src/ui/contexts/VimModeContext.tsx:87:15 - - 84: export const useVimMode = () => { - 85: const context = useContext(VimModeContext); - 86: if (context === undefined) { - 87: throw new Error('useVimMode must be used within a VimModeProvider'); - 88: } - 89: return context; - 90: }; - - - useVimMode (src/ui/contexts/VimModeContext.tsx:87:15) - - SettingsDialog (src/ui/components/SettingsDialog.tsx:36:26) - -react-stack-bottom- - rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon - ciler/cjs/react-reconciler.development.js:15859:20) - -renderWithHoo - s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ - cjs/react-reconciler.development.js:3221:22) - -updateFunctionComp - nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc - iler/cjs/react-reconciler.development.js:6475:19) - -runWithFiberIn - EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:1738:13) - -performUnitOfW - rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12834:22) - -workLoopSyn - (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj - s/react-reconciler.development.js:12644:41) - -renderRootSy - c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c - js/react-reconciler.development.js:12624:11) - -performWorkOnR - ot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler - /cjs/react-reconciler.development.js:12135:44) -" -`; diff --git a/packages/cli/src/ui/components/__snapshots__/StatsDisplay.sections.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/StatsDisplay.sections.test.tsx.snap index e12297cc79..64d7b3de7c 100644 --- a/packages/cli/src/ui/components/__snapshots__/StatsDisplay.sections.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/StatsDisplay.sections.test.tsx.snap @@ -221,225 +221,3 @@ exports[` sections > performance metrics display > shows through │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; - -exports[` sections Title Rendering renders the default title when no title prop is provided 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` sections Title Rendering renders the custom title when a title prop is provided 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Agent powering down. Goodbye! │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` sections Quota Display renders quota information when quotaLines are provided 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Session API │ -│ Total Requests: 1 │ -│ Total Errors: 0 │ -│ Avg Latency: 100ms │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ │ -│ Model Usage Reqs Input Tokens Cache Reads Output Tokens │ -│ ──────────────────────────────────────────────────────────────────────────── │ -│ gemini-2.5-pro 1 50 50 100 │ -│ Latency: 100ms avg / 100ms total │ -│ │ -│ Savings Highlight: 50 (50.0%) of input tokens were served from the cache, reducing costs. │ -│ │ -│ Quota Information │ -│ ## Anthropic Quota Information │ -│ │ -│ **Daily Usage** │ -│ Used: 1000 / 10000 tokens (10.0%) │ -│ Remaining: 9000 tokens │ -│ Resets: 2026-02-15 00:00:00 UTC │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` sections Quota Display does not render quota section when quotaLines are not provided 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` sections Quota Display handles empty quotaLines gracefully 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` sections Model Usage Table Updates should display separate Input Tokens and Cache Reads columns 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Session API │ -│ Total Requests: 5 │ -│ Total Errors: 0 │ -│ Avg Latency: 200ms │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ │ -│ Model Usage Reqs Input Tokens Cache Reads Output Tokens │ -│ ──────────────────────────────────────────────────────────────────────────── │ -│ gemini-2.5-pro 5 600 400 500 │ -│ Latency: 200ms avg / 1.0s total │ -│ │ -│ Savings Highlight: 400 (40.0%) of input tokens were served from the cache, reducing costs. │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` sections Model Usage Table Updates should apply color to cache efficiency percentage 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Session API │ -│ Total Requests: 1 │ -│ Total Errors: 0 │ -│ Avg Latency: 100ms │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ │ -│ Model Usage Reqs Input Tokens Cache Reads Output Tokens │ -│ ──────────────────────────────────────────────────────────────────────────── │ -│ gemini-2.5-pro 1 500 500 100 │ -│ Latency: 100ms avg / 100ms total │ -│ │ -│ Savings Highlight: 500 (50.0%) of input tokens were served from the cache, reducing costs. │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` sections performance metrics display shows throughput, TTFT, and output rate in performance section when available 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Session API │ -│ Total Requests: 2 │ -│ Total Errors: 0 │ -│ Avg Latency: 1.3s │ -│ │ -│ Interaction Summary │ -│ Session ID: test-session-id │ -│ Tool Calls: 1 ( OK 1 ERR 0 ) │ -│ » Total Duration: 250ms │ -│ Success Rate: 100.0% │ -│ User Agreement: 100.0% (1 reviewed) │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ » Throughput (TPM): 1.23k TPM (session weighted) │ -│ » TTFT (last): 187ms │ -│ » Output Gen Rate: 15.50 tok/s (session weighted) │ -│ » Input Rate (eff): 3200.00 tok/s (ΣP/ΣTTFT) │ -│ │ -│ │ -│ Model Usage Reqs Input Tokens Cache Reads Output Tokens │ -│ ──────────────────────────────────────────────────────────────────────────── │ -│ gemini-2.5-pro 2 600 300 400 │ -│ Latency: 1.3s avg / 2.5s total │ -│ │ -│ Savings Highlight: 300 (33.3%) of input tokens were served from the cache, reducing costs. │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` sections performance metrics display hides throughput, TTFT, and output rate when values are non-finite 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` sections performance metrics display hides throughput, TTFT, and output rate when values are unavailable 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` sections performance metrics display shows throughput when TPM is present even if TTFT/output rate are unavailable 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ » Throughput (TPM): 250.00 TPM (session weighted) │ -│ │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; diff --git a/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap index fa760b521d..43bba71702 100644 --- a/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap @@ -216,220 +216,3 @@ exports[` > renders only the Performance section in its zero sta │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; - -exports[` renders only the Performance section in its zero state 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` renders a table with two models correctly 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Session API │ -│ Total Requests: 8 │ -│ Total Errors: 1 │ -│ Avg Latency: 2.4s │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ │ -│ Model Usage Reqs Input Tokens Cache Reads Output Tokens │ -│ ──────────────────────────────────────────────────────────────────────────── │ -│ gemini-2.5-pro 3 500 500 2,000 │ -│ Latency: 5.0s avg / 15.0s total │ -│ gemini-2.5-flash 5 15,000 10,000 15,000 │ -│ Latency: 900ms avg / 4.5s total (1 errors) │ -│ │ -│ Savings Highlight: 10,500 (40.4%) of input tokens were served from the cache, reducing costs. │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` renders all sections when all data is present 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Session API │ -│ Total Requests: 1 │ -│ Total Errors: 0 │ -│ Avg Latency: 100ms │ -│ │ -│ Interaction Summary │ -│ Session ID: test-session-id │ -│ Tool Calls: 2 ( OK 1 ERR 1 ) │ -│ » Total Duration: 123ms │ -│ Success Rate: 50.0% │ -│ User Agreement: 100.0% (1 reviewed) │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ │ -│ Model Usage Reqs Input Tokens Cache Reads Output Tokens │ -│ ──────────────────────────────────────────────────────────────────────────── │ -│ gemini-2.5-pro 1 50 50 100 │ -│ Latency: 100ms avg / 100ms total │ -│ │ -│ Savings Highlight: 50 (50.0%) of input tokens were served from the cache, reducing costs. │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` Conditional Rendering Tests hides User Agreement when no decisions are made 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Interaction Summary │ -│ Session ID: test-session-id │ -│ Tool Calls: 2 ( OK 1 ERR 1 ) │ -│ » Total Duration: 123ms │ -│ Success Rate: 50.0% │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` Conditional Rendering Tests hides Efficiency section when cache is not used 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Session API │ -│ Total Requests: 1 │ -│ Total Errors: 0 │ -│ Avg Latency: 100ms │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ │ -│ Model Usage Reqs Input Tokens Cache Reads Output Tokens │ -│ ──────────────────────────────────────────────────────────────────────────── │ -│ gemini-2.5-pro 1 100 0 100 │ -│ Latency: 100ms avg / 100ms total │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` Conditional Color Tests renders success rate in green for high values 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Interaction Summary │ -│ Session ID: test-session-id │ -│ Tool Calls: 10 ( OK 10 ERR 0 ) │ -│ Success Rate: 100.0% │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` Conditional Color Tests renders success rate in yellow for medium values 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Interaction Summary │ -│ Session ID: test-session-id │ -│ Tool Calls: 10 ( OK 9 ERR 1 ) │ -│ Success Rate: 90.0% │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` Conditional Color Tests renders success rate in red for low values 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Interaction Summary │ -│ Session ID: test-session-id │ -│ Tool Calls: 10 ( OK 5 ERR 5 ) │ -│ Success Rate: 50.0% │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` Code Changes Display displays Code Changes when line counts are present 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Interaction Summary │ -│ Session ID: test-session-id │ -│ Tool Calls: 1 ( OK 1 ERR 0 ) │ -│ » Total Duration: 100ms │ -│ Success Rate: 100.0% │ -│ Code Changes: +42 -18 │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` Code Changes Display hides Code Changes when no lines are added or removed 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Interaction Summary │ -│ Session ID: test-session-id │ -│ Tool Calls: 1 ( OK 1 ERR 0 ) │ -│ » Total Duration: 100ms │ -│ Success Rate: 100.0% │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; diff --git a/packages/cli/src/ui/components/__snapshots__/Table.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/Table.test.tsx.snap index a8fa29311c..231784c6e1 100644 --- a/packages/cli/src/ui/components/__snapshots__/Table.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/Table.test.tsx.snap @@ -12,16 +12,3 @@ exports[`Table > should support custom cell rendering 1`] = ` ──────────────────────────────────────────────────────────────────────────────────────────────────── 20" `; - -exports[`Table should render headers and data correctly 1`] = ` -"ID Name -──────────────────────────────────────────────────────────────────────────────────────────────────── -1 Alice -2 Bob" -`; - -exports[`Table should support custom cell rendering 1`] = ` -"Value -──────────────────────────────────────────────────────────────────────────────────────────────────── -20" -`; diff --git a/packages/cli/src/ui/components/__snapshots__/ToolStatsDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ToolStatsDisplay.test.tsx.snap index e2c09b7c7c..ca7d1e2fd4 100644 --- a/packages/cli/src/ui/components/__snapshots__/ToolStatsDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/ToolStatsDisplay.test.tsx.snap @@ -89,93 +89,3 @@ exports[` > should render "no tool calls" message when there │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; - -exports[` should render "no tool calls" message when there are no active tools 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ No tool calls have been made in this session. │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` should display stats for a single tool correctly 1`] = ` -"╭────────────────────────────────────────────────────────────────────╮ -│ │ -│ Tool Stats │ -│ │ -│ Tool Name Calls Success Rate Avg Duration │ -│ ──────────────────────────────────────────────────────────────── │ -│ test-tool 1 100.0% 100ms │ -│ │ -│ User Decision Summary │ -│ Total Reviewed Suggestions: 1 │ -│ » Accepted: 1 │ -│ » Rejected: 0 │ -│ » Modified: 0 │ -│ ──────────────────────────────────────────────────────────────── │ -│ Overall Agreement Rate: 100.0% │ -│ │ -╰────────────────────────────────────────────────────────────────────╯" -`; - -exports[` should display stats for multiple tools correctly 1`] = ` -"╭────────────────────────────────────────────────────────────────────╮ -│ │ -│ Tool Stats │ -│ │ -│ Tool Name Calls Success Rate Avg Duration │ -│ ──────────────────────────────────────────────────────────────── │ -│ tool-a 2 50.0% 100ms │ -│ tool-b 1 100.0% 100ms │ -│ │ -│ User Decision Summary │ -│ Total Reviewed Suggestions: 3 │ -│ » Accepted: 1 │ -│ » Rejected: 1 │ -│ » Modified: 1 │ -│ ──────────────────────────────────────────────────────────────── │ -│ Overall Agreement Rate: 33.3% │ -│ │ -╰────────────────────────────────────────────────────────────────────╯" -`; - -exports[` should handle large values without wrapping or overlapping 1`] = ` -"╭────────────────────────────────────────────────────────────────────╮ -│ │ -│ Tool Stats │ -│ │ -│ Tool Name Calls Success Rate Avg Duration │ -│ ──────────────────────────────────────────────────────────────── │ -│ long-named-tool-for-testi99999999 88.9% 1ms │ -│ ng-wrapping-and-such 9 │ -│ │ -│ User Decision Summary │ -│ Total Reviewed Suggestions: 222234566 │ -│ » Accepted: 123456789 │ -│ » Rejected: 98765432 │ -│ » Modified: 12345 │ -│ ──────────────────────────────────────────────────────────────── │ -│ Overall Agreement Rate: 55.6% │ -│ │ -╰────────────────────────────────────────────────────────────────────╯" -`; - -exports[` should handle zero decisions gracefully 1`] = ` -"╭────────────────────────────────────────────────────────────────────╮ -│ │ -│ Tool Stats │ -│ │ -│ Tool Name Calls Success Rate Avg Duration │ -│ ──────────────────────────────────────────────────────────────── │ -│ test-tool 1 100.0% 100ms │ -│ │ -│ User Decision Summary │ -│ Total Reviewed Suggestions: 0 │ -│ » Accepted: 0 │ -│ » Rejected: 0 │ -│ » Modified: 0 │ -│ ──────────────────────────────────────────────────────────────── │ -│ Overall Agreement Rate: -- │ -│ │ -╰────────────────────────────────────────────────────────────────────╯" -`; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap index ce66c75a80..6bea5eecd5 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap @@ -220,130 +220,3 @@ exports[` > Height Calculation > calculates available height │ │ ╰──────────────────────────────────────────────────────────────────────────────╯" `; - -exports[` Golden Snapshots renders single successful tool call 1`] = ` -" ╭────────────────────────────────────────────────────────────────────────────────────────────────── - │ Agent: helper-agent - │ - │MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) - ╰──────────────────────────────────────────────────────────────────────────────────────────────────" -`; - -exports[` Golden Snapshots renders multiple tool calls with different statuses 1`] = ` -" ╭────────────────────────────────────────────────────────────────────────────────────────────────── - │ Agent: helper-agent - │ - │MockTool[tool-1]: ✓ successful-tool - This tool succeeded (medium) - │ - │MockTool[tool-2]: o pending-tool - This tool is pending (medium) - │ - │MockTool[tool-3]: x error-tool - This tool failed (medium) - ╰──────────────────────────────────────────────────────────────────────────────────────────────────" -`; - -exports[` Golden Snapshots renders tool call awaiting confirmation 1`] = ` -" ╭────────────────────────────────────────────────────────────────────────────────────────────────── - │ Agent: helper-agent - │ - │MockTool[tool-confirm]: ? confirmation-tool - This tool needs confirmation (high) - │MockConfirmation: Are you sure you want to proceed? - ╰──────────────────────────────────────────────────────────────────────────────────────────────────" -`; - -exports[` Golden Snapshots renders shell command with yellow border 1`] = ` -" ╭────────────────────────────────────────────────────────────────────────────────────────────────── - │ Agent: helper-agent - │ - │MockTool[shell-1]: ✓ run_shell_command - Execute shell command (medium) - ╰──────────────────────────────────────────────────────────────────────────────────────────────────" -`; - -exports[` Golden Snapshots renders mixed tool calls including shell command 1`] = ` -" ╭────────────────────────────────────────────────────────────────────────────────────────────────── - │ Agent: helper-agent - │ - │MockTool[tool-1]: ✓ read_file - Read a file (medium) - │ - │MockTool[tool-2]: ⊷ run_shell_command - Run command (medium) - │ - │MockTool[tool-3]: o write_file - Write to file (medium) - ╰──────────────────────────────────────────────────────────────────────────────────────────────────" -`; - -exports[` Golden Snapshots renders with limited terminal height 1`] = ` -" ╭────────────────────────────────────────────────────────────────────────────────────────────────── - │ Agent: helper-agent - │ - │MockTool[tool-1]: ✓ tool-with-result - Tool with output (medium) - │ - │MockTool[tool-2]: ✓ another-tool - Another tool (medium) - ╰──────────────────────────────────────────────────────────────────────────────────────────────────" -`; - -exports[` Golden Snapshots renders when not focused 1`] = ` -" ╭────────────────────────────────────────────────────────────────────────────────────────────────── - │ Agent: helper-agent - │ - │MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) - ╰──────────────────────────────────────────────────────────────────────────────────────────────────" -`; - -exports[` Golden Snapshots renders with narrow terminal width 1`] = ` -" ╭────────────────────────────────────────────────────────────────────────────────────────────────── - │ Agent: helper-agent - │ - │MockTool[tool-123]: ✓ very-long-tool-name-that-might-wrap - This is a very long description that - │might cause wrapping issues (medium) - ╰──────────────────────────────────────────────────────────────────────────────────────────────────" -`; - -exports[` Golden Snapshots renders empty tool calls array 1`] = `""`; - -exports[` Border Color Logic uses yellow border when tools are pending 1`] = ` -" ╭────────────────────────────────────────────────────────────────────────────────────────────────── - │ Agent: helper-agent - │ - │MockTool[tool-123]: o test-tool - A tool for testing (medium) - ╰──────────────────────────────────────────────────────────────────────────────────────────────────" -`; - -exports[` Border Color Logic uses yellow border for shell commands even when successful 1`] = ` -" ╭────────────────────────────────────────────────────────────────────────────────────────────────── - │ Agent: helper-agent - │ - │MockTool[tool-123]: ✓ run_shell_command - A tool for testing (medium) - ╰──────────────────────────────────────────────────────────────────────────────────────────────────" -`; - -exports[` Border Color Logic uses gray border when all tools are successful and no shell commands 1`] = ` -" ╭────────────────────────────────────────────────────────────────────────────────────────────────── - │ Agent: helper-agent - │ - │MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) - │ - │MockTool[tool-2]: ✓ another-tool - A tool for testing (medium) - ╰──────────────────────────────────────────────────────────────────────────────────────────────────" -`; - -exports[` Height Calculation calculates available height correctly with multiple tools with results 1`] = ` -" ╭────────────────────────────────────────────────────────────────────────────────────────────────── - │ Agent: helper-agent - │ - │MockTool[tool-1]: ✓ test-tool - A tool for testing (medium) - │ - │MockTool[tool-2]: ✓ test-tool - A tool for testing (medium) - │ - │MockTool[tool-3]: ✓ test-tool - A tool for testing (medium) - ╰──────────────────────────────────────────────────────────────────────────────────────────────────" -`; - -exports[` Confirmation Handling shows confirmation dialog for first confirming tool only 1`] = ` -" ╭────────────────────────────────────────────────────────────────────────────────────────────────── - │ Agent: helper-agent - │ - │MockTool[tool-1]: ? first-confirm - A tool for testing (high) - │MockConfirmation: Confirm first tool - │ - │MockTool[tool-2]: ? second-confirm - A tool for testing (low) - ╰──────────────────────────────────────────────────────────────────────────────────────────────────" -`; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolMessage.test.tsx.snap index 4cf23180f0..10d3c077ad 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolMessage.test.tsx.snap @@ -36,40 +36,3 @@ exports[` > renders emphasis correctly 2`] = ` │ │ │ MockMarkdown:Test result │" `; - -exports[` renders DiffRenderer for diff results 1`] = ` -"│ │ -│ ✓ │ -│ test-tool A tool for testing │ -│ │ -│ MockDiff:--- a/file.txt │ -│ +++ b/file.txt │ -│ @@ -1 +1 @@ │ -│ -old │ -│ +new │" -`; - -exports[` renders emphasis correctly 1`] = ` -"│ │ -│ ✓ │ -│ test-tool A tool for testing │ -│ ← │ -│ │ -│ MockMarkdown:Test result │" -`; - -exports[` renders emphasis correctly 2`] = ` -"│ │ -│ ✓ │ -│ test-tool A tool for testing │ -│ │ -│ MockMarkdown:Test result │" -`; - -exports[` renders AnsiOutputText for AnsiOutput results 1`] = ` -"│ │ -│ ✓ │ -│ test-tool A tool for testing │ -│ │ -│ hello │" -`; diff --git a/packages/cli/src/ui/components/views/__snapshots__/ChatList.test.tsx.snap b/packages/cli/src/ui/components/views/__snapshots__/ChatList.test.tsx.snap index 6da12d6ab0..eddfa2ca2b 100644 --- a/packages/cli/src/ui/components/views/__snapshots__/ChatList.test.tsx.snap +++ b/packages/cli/src/ui/components/views/__snapshots__/ChatList.test.tsx.snap @@ -18,22 +18,3 @@ Note: Newest last, oldest first" `; exports[` > renders correctly with no chats 1`] = `"No saved conversation checkpoints found."`; - -exports[` renders correctly with a list of chats 1`] = ` -"List of saved conversations: - - - chat-1 (2025-10-02 10:00:00) - - another-chat (2025-10-01 12:30:00) - -Note: Newest last, oldest first" -`; - -exports[` renders correctly with no chats 1`] = `"No saved conversation checkpoints found."`; - -exports[` handles invalid date formats gracefully 1`] = ` -"List of saved conversations: - - - bad-date-chat (Invalid Date) - -Note: Newest last, oldest first" -`; diff --git a/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap b/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap index 3d7a868982..c340dc8f61 100644 --- a/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap +++ b/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap @@ -179,183 +179,3 @@ exports[` > with 'Windows' line endings > renders unordered l `; exports[` > with 'Windows' line endings > shows line numbers in code blocks by default 1`] = `" 1 const x = 1;"`; - -exports[` renders nothing for empty text 1`] = `""`; - -exports[` renders a simple paragraph 1`] = `"Hello, world."`; - -exports[` with Windows line endings renders headers with correct levels 1`] = ` -"Header 1 -Header 2 -Header 3 -Header 4 -" -`; - -exports[` with Windows line endings renders a fenced code block with a language 1`] = ` -" const x = 1; - console.log(x);" -`; - -exports[` with Windows line endings renders a fenced code block without a language 1`] = `" plain text"`; - -exports[` with Windows line endings handles unclosed (pending) code blocks 1`] = `" let y = 2;"`; - -exports[` with Windows line endings renders unordered lists with different markers 1`] = ` -" - item A - * item B - + item C -" -`; - -exports[` with Windows line endings renders nested unordered lists 1`] = ` -" * Level 1 - * Level 2 - * Level 3 -" -`; - -exports[` with Windows line endings renders ordered lists 1`] = ` -" 1. First item - 2. Second item -" -`; - -exports[` with Windows line endings renders horizontal rules 1`] = ` -"Hello ---- -World ---- -Test -" -`; - -exports[` with Windows line endings renders tables correctly 1`] = ` -" -┌──────────┬──────────┐ -│ Header 1 │ Header 2 │ -├──────────┼──────────┤ -│ Cell 1 │ Cell 2 │ -│ Cell 3 │ Cell 4 │ -└──────────┴──────────┘ -" -`; - -exports[` with Windows line endings handles a table at the end of the input 1`] = ` -"Some text before. -| A | B | -|---| -| 1 | 2 |" -`; - -exports[` with Windows line endings inserts a single space between paragraphs 1`] = ` -"Paragraph 1. - -Paragraph 2." -`; - -exports[` with Windows line endings correctly parses a mix of markdown elements 1`] = ` -"Main Title - -Here is a paragraph. - - - List item 1 - - List item 2 - - some code - -Another paragraph. -" -`; - -exports[` with Windows line endings hides line numbers in code blocks when showLineNumbers is false 1`] = `" const x = 1;"`; - -exports[` with Windows line endings shows line numbers in code blocks by default 1`] = `" const x = 1;"`; - -exports[` with Unix line endings renders headers with correct levels 1`] = ` -"Header 1 -Header 2 -Header 3 -Header 4 -" -`; - -exports[` with Unix line endings renders a fenced code block with a language 1`] = ` -" const x = 1; - console.log(x);" -`; - -exports[` with Unix line endings renders a fenced code block without a language 1`] = `" plain text"`; - -exports[` with Unix line endings handles unclosed (pending) code blocks 1`] = `" let y = 2;"`; - -exports[` with Unix line endings renders unordered lists with different markers 1`] = ` -" - item A - * item B - + item C -" -`; - -exports[` with Unix line endings renders nested unordered lists 1`] = ` -" * Level 1 - * Level 2 - * Level 3 -" -`; - -exports[` with Unix line endings renders ordered lists 1`] = ` -" 1. First item - 2. Second item -" -`; - -exports[` with Unix line endings renders horizontal rules 1`] = ` -"Hello ---- -World ---- -Test -" -`; - -exports[` with Unix line endings renders tables correctly 1`] = ` -" -┌──────────┬──────────┐ -│ Header 1 │ Header 2 │ -├──────────┼──────────┤ -│ Cell 1 │ Cell 2 │ -│ Cell 3 │ Cell 4 │ -└──────────┴──────────┘ -" -`; - -exports[` with Unix line endings handles a table at the end of the input 1`] = ` -"Some text before. -| A | B | -|---| -| 1 | 2 |" -`; - -exports[` with Unix line endings inserts a single space between paragraphs 1`] = ` -"Paragraph 1. - -Paragraph 2." -`; - -exports[` with Unix line endings correctly parses a mix of markdown elements 1`] = ` -"Main Title - -Here is a paragraph. - - - List item 1 - - List item 2 - - some code - -Another paragraph. -" -`; - -exports[` with Unix line endings hides line numbers in code blocks when showLineNumbers is false 1`] = `" const x = 1;"`; - -exports[` with Unix line endings shows line numbers in code blocks by default 1`] = `" const x = 1;"`; From 67346c768fa9073abcfb7e3731fe2790d3097788 Mon Sep 17 00:00:00 2001 From: acoliver Date: Tue, 4 Aug 2026 08:44:03 -0300 Subject: [PATCH 003/128] wip(cli-bun): batch remediation from parallel agents + snapshot key migration --- packages/cli/src/cli-sandbox.test.tsx | 6 +- packages/cli/src/cli.provider-init.test.ts | 1 - packages/cli/src/cliStartupOrdering.test.ts | 86 ++-- .../config/__tests__/sandboxConfig.test.ts | 14 +- packages/cli/src/config/auth.test.ts | 1 - .../src/services/BuiltinCommandLoader.test.ts | 22 +- .../FileCommandLoader.processors.test.ts | 179 ++++---- .../src/services/FileCommandLoader.test.ts | 412 ++++++++---------- .../src/services/__testhelpers__/mockFs.ts | 234 ++++++++++ packages/cli/src/test-utils/render.tsx | 25 +- packages/cli/src/ui/App.behavior.test.tsx | 4 +- packages/cli/src/ui/App.components.test.tsx | 8 +- packages/cli/src/ui/App.context.test.tsx | 4 +- packages/cli/src/ui/App.dialogs.test.tsx | 4 +- packages/cli/src/ui/App.test.tsx | 4 +- .../App.components.test.tsx.snap | 46 ++ .../src/ui/__snapshots__/App.test.tsx.snap | 2 +- .../AppContainer.keybindings.test.tsx | 2 +- .../ui/__tests__/AppContainer.mount.test.tsx | 84 ++-- .../AppContainer.render-budget.test.tsx | 2 +- .../ui/__tests__/integrationWiring.spec.tsx | 6 +- .../ui/commands/test/subagentCommand.test.ts | 14 +- .../cli/src/ui/components/AnsiOutput.test.tsx | 2 +- .../cli/src/ui/components/AuthDialog.test.tsx | 40 +- .../ui/components/AuthDialog.theme.test.tsx | 4 +- .../ui/components/Footer.responsive.test.tsx | 5 - .../cli/src/ui/components/Footer.test.tsx | 116 ++--- packages/cli/src/ui/components/Footer.tsx | 15 +- .../ui/components/LoadingIndicator.test.tsx | 7 +- .../HistoryItemDisplay.test.tsx.snap | 8 +- .../IDEContextDetailDisplay.test.tsx.snap | 4 +- .../InputPrompt.paste.test.tsx.snap | 8 +- .../__snapshots__/InputPrompt.test.tsx.snap | 24 +- .../InputPrompt.vim.test.tsx.snap | 16 +- .../LoadingIndicator.test.tsx.snap | 2 +- .../ModelStatsDisplay.test.js.snap | 12 +- .../ModelStatsDisplay.test.tsx.snap | 14 +- .../SessionSummaryDisplay.test.js.snap | 2 +- .../SessionSummaryDisplay.test.tsx.snap | 2 +- .../SettingsDialog.interactions.test.tsx.snap | 16 +- .../SettingsDialog.test.tsx.snap | 18 +- .../StatsDisplay.sections.test.tsx.snap | 22 +- .../__snapshots__/StatsDisplay.test.tsx.snap | 20 +- .../__snapshots__/Table.test.tsx.snap | 4 +- .../ToolStatsDisplay.test.js.snap | 10 +- .../ToolStatsDisplay.test.tsx.snap | 10 +- .../__tests__/LayoutManager.test.tsx | 43 +- .../SessionBrowserDialog.layout.spec.tsx | 5 +- .../__tests__/SessionBrowserDialog.spec.tsx | 5 +- .../ToolGroupMessage.test.tsx.snap | 38 +- .../__snapshots__/ToolMessage.test.tsx.snap | 8 +- .../ui/components/shared/MaxSizedBox.test.tsx | 52 +-- .../RadioButtonSelect.test.js.snap | 12 +- .../shared/text-buffer.part2.test.ts | 4 +- .../shared/text-buffer.part3.test.ts | 6 +- .../shared/text-buffer.part4.test.ts | 8 +- .../shared/text-buffer.part5.test.ts | 2 +- .../ui/components/shared/text-buffer.test.ts | 28 ++ .../__snapshots__/ChatList.test.tsx.snap | 6 +- .../ui/hooks/useAgentStream.approval.test.tsx | 2 +- .../useAgentStream.cancellation.test.tsx | 2 +- .../ui/hooks/useAgentStream.commands.test.tsx | 2 +- .../ui/hooks/useAgentStream.finished.test.tsx | 2 +- .../ui/hooks/useAgentStream.hooks.test.tsx | 2 +- .../ui/hooks/useAgentStream.include.test.tsx | 2 +- .../hooks/useAgentStream.loopdetect.test.tsx | 2 +- .../ui/hooks/useAgentStream.ordering.test.tsx | 2 +- .../cli/src/ui/hooks/useAgentStream.test.tsx | 2 +- .../ui/hooks/useAgentStream.thinking.test.tsx | 2 +- .../ui/hooks/useAgentStream.thought.test.tsx | 8 +- .../hooks/useAgentStream.usercancel.test.tsx | 2 +- .../src/ui/hooks/useGitBranchName.test.tsx | 60 ++- packages/cli/src/ui/themes/theme-compat.ts | 2 +- .../MarkdownDisplay.test.js.snap | 28 +- .../MarkdownDisplay.test.tsx.snap | 60 +-- .../cli/src/ui/utils/clipboardUtils.test.ts | 10 +- .../cli/src/ui/utils/commandUtils.test.ts | 25 +- packages/cli/src/utils/version.test.ts | 38 +- packages/cli/src/utils/version.ts | 14 +- 79 files changed, 1171 insertions(+), 854 deletions(-) create mode 100644 packages/cli/src/services/__testhelpers__/mockFs.ts create mode 100644 packages/cli/src/ui/__snapshots__/App.components.test.tsx.snap diff --git a/packages/cli/src/cli-sandbox.test.tsx b/packages/cli/src/cli-sandbox.test.tsx index d9f9090634..9ef7c0de22 100644 --- a/packages/cli/src/cli-sandbox.test.tsx +++ b/packages/cli/src/cli-sandbox.test.tsx @@ -82,8 +82,10 @@ vi.mock('./utils/bootstrap.js', async (importOriginal) => { shouldRelaunchForMemory: vi.fn(() => []), isDebugMode: vi.fn(() => false), computeSandboxMemoryArgs: vi.fn( - (...args: Parameters) => - actual.computeSandboxMemoryArgs(...args), + (..._args: Parameters) => + ['-m', '4096'] as unknown as ReturnType< + typeof actual.computeSandboxMemoryArgs + >, ), }; }); diff --git a/packages/cli/src/cli.provider-init.test.ts b/packages/cli/src/cli.provider-init.test.ts index e408a1cda4..ab2e489bd8 100644 --- a/packages/cli/src/cli.provider-init.test.ts +++ b/packages/cli/src/cli.provider-init.test.ts @@ -222,7 +222,6 @@ describe('cli main provider initialization', () => { } finally { process.stdin.isTTY = originalIsTTY; dynamicSettingsRegistry.reset(); - vi.resetModules(); } }); diff --git a/packages/cli/src/cliStartupOrdering.test.ts b/packages/cli/src/cliStartupOrdering.test.ts index fe13b0aaf1..7d718c55bf 100644 --- a/packages/cli/src/cliStartupOrdering.test.ts +++ b/packages/cli/src/cliStartupOrdering.test.ts @@ -28,7 +28,7 @@ function makeConfig(hasActive: boolean, interactive: boolean): Config { } function setupCommonMainMocks(callOrder: string[], config: Config): void { - vi.doMock('./cliProviderInit.js', () => ({ + vi.mock('./cliProviderInit.js', () => ({ activateConfiguredProvider: async () => { callOrder.push('activation'); return { authFailed: false, token: undefined, intent: undefined }; @@ -39,36 +39,36 @@ function setupCommonMainMocks(callOrder: string[], config: Config): void { callOrder.push('acp-activated'); }, })); - vi.doMock('./cliTerminalSession.js', () => ({ + vi.mock('./cliTerminalSession.js', () => ({ constructAgentWithSpinner: async () => { callOrder.push('agent-construction'); return {}; }, prepareTerminalSession: async () => {}, })); - vi.doMock('./cliSessionBootstrap.js', () => ({ + vi.mock('./cliSessionBootstrap.js', () => ({ bootstrapRuntimeAndConfig: async () => ({ config, runtimeSettingsService: {}, }), setupSessionRecording: async () => undefined, })); - vi.doMock('./session/nonInteractiveSession.js', () => ({ + vi.mock('./session/nonInteractiveSession.js', () => ({ dispatchInteractiveOrNonInteractive: async () => { callOrder.push('dispatch'); }, })); - vi.doMock('./cliSandbox.js', () => ({ maybeHopIntoSandbox: async () => {} })); - vi.doMock('./config/cliArgParser.js', () => ({ + vi.mock('./cliSandbox.js', () => ({ maybeHopIntoSandbox: async () => {} })); + vi.mock('./config/cliArgParser.js', () => ({ parseArguments: async () => ({ prompt: 'hello' }), })); - vi.doMock('./config/settings.js', () => ({ + vi.mock('./config/settings.js', () => ({ loadSettings: () => { callOrder.push('loadSettings'); return { merged: { ui: { unicode: 'auto' } }, errors: [] }; }, })); - vi.doMock('./cliBootstrap.js', () => ({ + vi.mock('./cliBootstrap.js', () => ({ configureEarlyDebugLogging: () => {}, createMemoizedStdinReader: () => async () => '', ensureStdinOrPromptProvided: async () => {}, @@ -79,33 +79,33 @@ function setupCommonMainMocks(callOrder: string[], config: Config): void { throwIfSettingsErrors: () => {}, ParsedCliArgs: {} as never, })); - vi.doMock('./utils/cleanup.js', () => ({ + vi.mock('./utils/cleanup.js', () => ({ cleanupCheckpoints: async () => {}, runExitCleanup: async () => {}, registerSyncCleanup: () => {}, })); - vi.doMock('./utils/sessionCleanup.js', () => ({ + vi.mock('./utils/sessionCleanup.js', () => ({ cleanupExpiredSessions: async () => {}, })); - vi.doMock('./zed-integration/zedIntegration.js', () => ({ + vi.mock('./zed-integration/zedIntegration.js', () => ({ runZedIntegration: async () => {}, })); - vi.doMock('./config/pathMigration.js', () => ({ + vi.mock('./config/pathMigration.js', () => ({ runStartupMigration: () => ({ migrated: false }), reportStartupResult: () => ({ messages: [], needsLegacyFallback: false }), })); - vi.doMock('./session/errorReporting.js', () => ({ + vi.mock('./session/errorReporting.js', () => ({ formatNonInteractiveError: () => '', })); - vi.doMock('./session/outputListeners.js', () => ({ + vi.mock('./session/outputListeners.js', () => ({ initializeOutputListenersAndFlush: () => {}, })); - vi.doMock('./session/signalHandlers.js', () => ({ + vi.mock('./session/signalHandlers.js', () => ({ installNonInteractiveSigintHandler: () => {}, setupUnhandledRejectionHandler: () => {}, __resetUnhandledRejectionStateForTesting: () => {}, })); - vi.doMock('./session/interactiveUI.js', () => ({ + vi.mock('./session/interactiveUI.js', () => ({ startInteractiveUI: async () => {}, })); } @@ -183,12 +183,10 @@ describe('main() orchestration: guard stops before activation (#2481)', () => { }); afterEach(() => { vi.restoreAllMocks(); - vi.resetModules(); }); async function runMainWithConfig(config: Config): Promise { - vi.resetModules(); - vi.doMock('./unconfiguredProviderGuard.js', async (importOriginal) => { + vi.mock('./unconfiguredProviderGuard.js', async (importOriginal) => { const actual = await importOriginal(); return { @@ -257,23 +255,21 @@ describe('main() orchestration: capability consumption precedes settings/sandbox }); afterEach(() => { vi.restoreAllMocks(); - vi.resetModules(); }); async function runMainConfigured(config: Config): Promise { - vi.resetModules(); - vi.doMock('@vybestack/llxprt-code-providers/auth.js', () => ({ + vi.mock('@vybestack/llxprt-code-providers/auth.js', () => ({ createTokenStore: () => { callOrder.push('createTokenStore'); return {}; }, })); - vi.doMock('./utils/sandbox-bashrc.js', () => ({ + vi.mock('./utils/sandbox-bashrc.js', () => ({ applySandboxBashrc: () => { callOrder.push('applySandboxBashrc'); }, })); - vi.doMock('./unconfiguredProviderGuard.js', () => ({ + vi.mock('./unconfiguredProviderGuard.js', () => ({ guardUnconfiguredProvider: async () => {}, UNCONFIGURED_PROVIDER_MESSAGE: '', })); @@ -332,7 +328,6 @@ describe('main() image mode: bypasses the conversational stdin guard (#2128)', ( }); afterEach(() => { vi.restoreAllMocks(); - vi.resetModules(); // Restore a TTY-like stdin so other suites are unaffected. Object.defineProperty(process.stdin, 'isTTY', { value: true, @@ -342,7 +337,6 @@ describe('main() image mode: bypasses the conversational stdin guard (#2128)', ( }); async function runMainImageMode(config: Config): Promise { - vi.resetModules(); // Simulate non-TTY stdin (piped / /dev/null) so the stdin guard WOULD // fire if not bypassed. Object.defineProperty(process.stdin, 'isTTY', { @@ -351,17 +345,17 @@ describe('main() image mode: bypasses the conversational stdin guard (#2128)', ( configurable: true, }); - vi.doMock('@vybestack/llxprt-code-providers/auth.js', () => ({ + vi.mock('@vybestack/llxprt-code-providers/auth.js', () => ({ createTokenStore: () => ({}), })); - vi.doMock('./utils/sandbox-bashrc.js', () => ({ + vi.mock('./utils/sandbox-bashrc.js', () => ({ applySandboxBashrc: () => {}, })); - vi.doMock('./unconfiguredProviderGuard.js', () => ({ + vi.mock('./unconfiguredProviderGuard.js', () => ({ guardUnconfiguredProvider: async () => {}, UNCONFIGURED_PROVIDER_MESSAGE: '', })); - vi.doMock('./cliProviderInit.js', () => ({ + vi.mock('./cliProviderInit.js', () => ({ activateConfiguredProvider: async () => ({ authFailed: false, token: undefined, @@ -371,38 +365,38 @@ describe('main() image mode: bypasses the conversational stdin guard (#2128)', ( connectIdeClientIfEnabled: async () => {}, ensureAcpProviderActivated: () => {}, })); - vi.doMock('./cliTerminalSession.js', () => ({ + vi.mock('./cliTerminalSession.js', () => ({ constructAgentWithSpinner: async () => ({}), prepareTerminalSession: async () => {}, })); - vi.doMock('./cliSessionBootstrap.js', () => ({ + vi.mock('./cliSessionBootstrap.js', () => ({ bootstrapRuntimeAndConfig: async () => ({ config, runtimeSettingsService: {}, }), setupSessionRecording: async () => undefined, })); - vi.doMock('./session/nonInteractiveSession.js', () => ({ + vi.mock('./session/nonInteractiveSession.js', () => ({ dispatchInteractiveOrNonInteractive: async () => {}, })); - vi.doMock('./cliSandbox.js', () => ({ + vi.mock('./cliSandbox.js', () => ({ maybeHopIntoSandbox: async () => {}, })); // parseArguments returns image-mode flags with NO conversational prompt. - vi.doMock('./config/cliArgParser.js', () => ({ + vi.mock('./config/cliArgParser.js', () => ({ parseArguments: async () => ({ imageOutput: 'out.png', imagePrompt: 'draw a cat', experimentalAcp: false, }), })); - vi.doMock('./config/settings.js', () => ({ + vi.mock('./config/settings.js', () => ({ loadSettings: () => ({ merged: { ui: { unicode: 'auto' } }, errors: [], }), })); - vi.doMock('./cliBootstrap.js', () => ({ + vi.mock('./cliBootstrap.js', () => ({ configureEarlyDebugLogging: () => {}, createMemoizedStdinReader: () => async () => '', // Track whether the guard is invoked. @@ -416,42 +410,42 @@ describe('main() image mode: bypasses the conversational stdin guard (#2128)', ( throwIfSettingsErrors: () => {}, ParsedCliArgs: {} as never, })); - vi.doMock('./utils/cleanup.js', () => ({ + vi.mock('./utils/cleanup.js', () => ({ cleanupCheckpoints: async () => {}, runExitCleanup: async () => {}, registerSyncCleanup: () => {}, })); - vi.doMock('./utils/sessionCleanup.js', () => ({ + vi.mock('./utils/sessionCleanup.js', () => ({ cleanupExpiredSessions: async () => {}, })); - vi.doMock('./zed-integration/zedIntegration.js', () => ({ + vi.mock('./zed-integration/zedIntegration.js', () => ({ runZedIntegration: async () => {}, })); - vi.doMock('./config/pathMigration.js', () => ({ + vi.mock('./config/pathMigration.js', () => ({ runStartupMigration: () => ({ migrated: false }), reportStartupResult: () => ({ messages: [], needsLegacyFallback: false, }), })); - vi.doMock('./session/errorReporting.js', () => ({ + vi.mock('./session/errorReporting.js', () => ({ formatNonInteractiveError: () => '', })); - vi.doMock('./session/outputListeners.js', () => ({ + vi.mock('./session/outputListeners.js', () => ({ initializeOutputListenersAndFlush: () => {}, })); - vi.doMock('./session/signalHandlers.js', () => ({ + vi.mock('./session/signalHandlers.js', () => ({ installNonInteractiveSigintHandler: () => {}, setupUnhandledRejectionHandler: () => {}, __resetUnhandledRejectionStateForTesting: () => {}, })); - vi.doMock('./session/interactiveUI.js', () => ({ + vi.mock('./session/interactiveUI.js', () => ({ startInteractiveUI: async () => {}, })); // Track whether image-mode dispatch is reached. The REAL // buildImageModeFlags is preserved so the stdin-guard bypass decision is // exercised against the real flag-detection logic, not a stub. - vi.doMock('./config/imageModeDispatch.js', async (importOriginal) => ({ + vi.mock('./config/imageModeDispatch.js', async (importOriginal) => ({ ...(await importOriginal< typeof import('./config/imageModeDispatch.js') >()), diff --git a/packages/cli/src/config/__tests__/sandboxConfig.test.ts b/packages/cli/src/config/__tests__/sandboxConfig.test.ts index 59f5f47559..bc07087423 100644 --- a/packages/cli/src/config/__tests__/sandboxConfig.test.ts +++ b/packages/cli/src/config/__tests__/sandboxConfig.test.ts @@ -16,11 +16,15 @@ vi.mock('../../utils/resolvePath.js', () => ({ resolvePath: (value: string) => value.replace('~', '/mock/home/user'), })); -vi.mock('../../utils/package.js', () => ({ - getPackageJson: vi.fn(async () => ({ - config: { sandboxImageUri: 'ghcr.io/vybestack/llxprt-code/sandbox:0.7.0' }, - })), -})); +vi.mock('@vybestack/llxprt-code-core', async () => { + const actual = await vi.importActual('@vybestack/llxprt-code-core'); + return { + ...actual, + getPackageJson: vi.fn(async () => ({ + config: { sandboxImageUri: 'ghcr.io/vybestack/llxprt-code/sandbox:0.7.0' }, + })), + }; +}); vi.mock('command-exists', () => ({ default: { diff --git a/packages/cli/src/config/auth.test.ts b/packages/cli/src/config/auth.test.ts index 38bf34778e..8796be6864 100644 --- a/packages/cli/src/config/auth.test.ts +++ b/packages/cli/src/config/auth.test.ts @@ -16,7 +16,6 @@ vi.mock('./settings.js', () => ({ describe('validateAuthMethod', () => { beforeEach(() => { - vi.resetModules(); vi.stubEnv('GEMINI_API_KEY', undefined); vi.stubEnv('GOOGLE_CLOUD_PROJECT', undefined); vi.stubEnv('GOOGLE_CLOUD_LOCATION', undefined); diff --git a/packages/cli/src/services/BuiltinCommandLoader.test.ts b/packages/cli/src/services/BuiltinCommandLoader.test.ts index 2d79fffc8e..86da67a6a3 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.test.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.test.ts @@ -73,7 +73,6 @@ vi.mock('../ui/commands/bugCommand.js', () => ({ bugCommand: {} })); vi.mock('../ui/commands/chatCommand.js', () => ({ chatCommand: {} })); vi.mock('../ui/commands/clearCommand.js', () => ({ clearCommand: {} })); vi.mock('../ui/commands/compressCommand.js', () => ({ compressCommand: {} })); -vi.mock('../ui/commands/corgiCommand.js', () => ({ corgiCommand: {} })); vi.mock('../ui/commands/docsCommand.js', () => ({ docsCommand: {} })); vi.mock('../ui/commands/editorCommand.js', () => ({ editorCommand: {} })); vi.mock('../ui/commands/extensionsCommand.js', () => ({ @@ -96,7 +95,6 @@ vi.mock('../ui/commands/quotaCommand.js', async () => { }, }; }); -vi.mock('../ui/commands/resumeCommand.js', () => ({ resumeCommand: {} })); vi.mock('../ui/commands/statsCommand.js', () => ({ statsCommand: {} })); vi.mock('../ui/commands/themeCommand.js', () => ({ themeCommand: {} })); vi.mock('../ui/commands/toolsCommand.js', () => ({ toolsCommand: {} })); @@ -111,6 +109,21 @@ vi.mock('../ui/commands/mcpCommand.js', () => ({ }, })); +// isDevelopment is a module-level constant captured at first evaluation of +// installationInfo.js. Under Vitest, vi.resetModules() forced re-evaluation +// after changing NODE_ENV; Bun does not support resetting modules. The +// 'profile' describe block only needs isDevelopment=true to exercise the +// uiprofile branch (profileCommand itself is always loaded), so mock the +// constant to true. The 'should always include profile command' test does +// not depend on isDevelopment (profileCommand is unconditionally included). +vi.mock('../utils/installationInfo.js', async () => { + const actual = await vi.importActual('./../utils/installationInfo.js'); + return { + ...actual, + isDevelopment: true, + }; +}); + describe('BuiltinCommandLoader', () => { let mockConfig: Config; @@ -229,7 +242,6 @@ describe('BuiltinCommandLoader profile', () => { let mockConfig: Config; beforeEach(() => { - vi.resetModules(); mockConfig = { getFolderTrust: vi.fn().mockReturnValue(false), getCheckpointingEnabled: () => false, @@ -244,8 +256,6 @@ describe('BuiltinCommandLoader profile', () => { }); it('should always include profile command', async () => { - process.env['NODE_ENV'] = 'production'; - const { BuiltinCommandLoader } = await import('./BuiltinCommandLoader.js'); const loader = new BuiltinCommandLoader(mockConfig); const commands = await loader.loadCommands(new AbortController().signal); const profileCmd = commands.find((c) => c.name === 'profile'); @@ -253,8 +263,6 @@ describe('BuiltinCommandLoader profile', () => { }); it('should include uiprofile command when isDevelopment is true', async () => { - process.env['NODE_ENV'] = 'development'; - const { BuiltinCommandLoader } = await import('./BuiltinCommandLoader.js'); const loader = new BuiltinCommandLoader(mockConfig); const commands = await loader.loadCommands(new AbortController().signal); const uiprofileCmd = commands.find((c) => c.name === 'uiprofile'); diff --git a/packages/cli/src/services/FileCommandLoader.processors.test.ts b/packages/cli/src/services/FileCommandLoader.processors.test.ts index 13fd36a14d..055efb99d4 100644 --- a/packages/cli/src/services/FileCommandLoader.processors.test.ts +++ b/packages/cli/src/services/FileCommandLoader.processors.test.ts @@ -6,10 +6,14 @@ import * as glob from 'glob'; import type { Config } from '@vybestack/llxprt-code-core'; -import { Storage } from '@vybestack/llxprt-code-settings'; -import mock from 'mock-fs'; import { FileCommandLoader } from './FileCommandLoader.js'; -import { assert, vi } from 'vitest'; +import { vi } from 'vitest'; + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) { + throw new Error(message); + } +} import { createMockCommandContext } from '../test-utils/mockCommandContext.js'; import { SHELL_INJECTION_TRIGGER, @@ -21,12 +25,28 @@ import { } from './prompt-processors/shellProcessor.js'; import { DefaultArgumentProcessor } from './prompt-processors/argumentProcessor.js'; import type { CommandContext } from '../ui/commands/types.js'; +import { FsMockContext } from './__testhelpers__/mockFs.js'; type PromptPipelineContent = Array<{ text: string }>; const mockShellProcess = vi.hoisted(() => vi.fn()); const mockAtFileProcess = vi.hoisted(() => vi.fn()); +// The settings mock must be available before vi.mock runs (Bun evaluates the +// factory eagerly at vi.mock() call time). Use vi.hoisted with createRequire +// to create the FsMockContext and settingsMock first, then reference them in +// the mock factory. +const settingsMockHoisted = vi.hoisted(() => { + const { createRequire } = require('node:module') as typeof import('node:module'); + const req = createRequire(import.meta.url); + const { FsMockContext } = req('./__testhelpers__/mockFs.ts') as typeof import('./__testhelpers__/mockFs.js'); + const ctx = new FsMockContext(); + return { ctx, mock: ctx.settingsMock() }; +}); +const fsMock = settingsMockHoisted.ctx; + +vi.mock('@vybestack/llxprt-code-settings', () => settingsMockHoisted.mock); + vi.mock('./prompt-processors/shellProcessor.js', () => ({ ShellProcessor: vi.fn().mockImplementation(() => ({ process: mockShellProcess, @@ -42,35 +62,21 @@ vi.mock('./prompt-processors/shellProcessor.js', () => ({ }, })); -vi.mock('./prompt-processors/argumentProcessor.js', async (importOriginal) => { - const original = - await importOriginal< - typeof import('./prompt-processors/argumentProcessor.js') - >(); - return { - DefaultArgumentProcessor: vi - .fn() - .mockImplementation(() => new original.DefaultArgumentProcessor()), - }; -}); - -vi.mock('./prompt-processors/atFileProcessor.js', () => ({ - AtFileProcessor: vi.fn().mockImplementation(() => ({ - process: mockAtFileProcess, +// Under Bun, importOriginal inside vi.mock factory returns the MOCKED module +// (not the real one), causing infinite recursion. Use a direct implementation +// that replicates DefaultArgumentProcessor's behavior instead. +vi.mock('./prompt-processors/argumentProcessor.js', () => ({ + DefaultArgumentProcessor: vi.fn().mockImplementation(() => ({ + process: async (prompt: string, context: CommandContext) => { + if (context.invocation?.args) { + return `${prompt} + +${context.invocation.raw}`; + } + return prompt; + }, })), })); -vi.mock('@vybestack/llxprt-code-core', async (importOriginal) => { - const original = - await importOriginal(); - return { - ...original, - Storage: original.Storage, - isCommandAllowed: vi.fn(), - ShellExecutionService: { - execute: vi.fn(), - }, - }; -}); vi.mock('glob', () => ({ glob: vi.fn(), @@ -81,8 +87,14 @@ describe('FileCommandLoader (processors)', () => { beforeEach(async () => { vi.clearAllMocks(); - const { glob: actualGlob } = - await vi.importActual('glob'); + fsMock.clear(); + // Re-establish the real glob implementation. vi.importActual returns the + // real module snapshot captured at mock-registration time, but + // vi.clearAllMocks() resets the mock function's implementation, so we + // restore it here. + const actualGlob = ( + await vi.importActual('glob') + ).glob; vi.mocked(glob.glob).mockImplementation(actualGlob); mockShellProcess.mockImplementation( (prompt: string, context: CommandContext) => { @@ -97,17 +109,18 @@ describe('FileCommandLoader (processors)', () => { }); afterEach(() => { - mock.restore(); + fsMock.restore(); + }); + + afterAll(() => { + fsMock.cleanup(); }); describe('Default Argument Processor Integration', () => { it('correctly processes a command without {{args}}', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'model_led.toml': - 'prompt = "This is the instruction."\ndescription = "Default processor test"', - }, + fsMock.mock({ + 'model_led.toml': + 'prompt = "This is the instruction."\ndescription = "Default processor test"', }); const loader = new FileCommandLoader(null as unknown as Config); @@ -135,11 +148,8 @@ describe('FileCommandLoader (processors)', () => { describe('Shell Processor Integration', () => { it('instantiates ShellProcessor if {{args}} is present (even without shell trigger)', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'args_only.toml': `prompt = "Hello {{args}}"`, - }, + fsMock.mock({ + 'args_only.toml': `prompt = "Hello {{args}}"`, }); const loader = new FileCommandLoader(null as unknown as Config); @@ -148,11 +158,8 @@ describe('FileCommandLoader (processors)', () => { expect(ShellProcessor).toHaveBeenCalledWith('args_only'); }); it('instantiates ShellProcessor if the trigger is present', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'shell.toml': `prompt = "Run this: ${SHELL_INJECTION_TRIGGER}echo hello}"`, - }, + fsMock.mock({ + 'shell.toml': `prompt = "Run this: ${SHELL_INJECTION_TRIGGER}echo hello}"`, }); const loader = new FileCommandLoader(null as unknown as Config); @@ -162,11 +169,8 @@ describe('FileCommandLoader (processors)', () => { }); it('does not instantiate ShellProcessor if no triggers ({{args}} or !{}) are present', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'regular.toml': `prompt = "Just a regular prompt"`, - }, + fsMock.mock({ + 'regular.toml': `prompt = "Just a regular prompt"`, }); const loader = new FileCommandLoader(null as unknown as Config); @@ -176,11 +180,8 @@ describe('FileCommandLoader (processors)', () => { }); it('returns a "submit_prompt" action if shell processing succeeds', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'shell.toml': `prompt = "Run !{echo 'hello'}"`, - }, + fsMock.mock({ + 'shell.toml': `prompt = "Run !{echo 'hello'}"`, }); mockShellProcess.mockResolvedValue('Run hello'); @@ -202,12 +203,9 @@ describe('FileCommandLoader (processors)', () => { }); it('returns a "confirm_shell_commands" action if shell processing requires it', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); const rawInvocation = '/shell rm -rf /'; - mock({ - [userCommandsDir]: { - 'shell.toml': `prompt = "Run !{rm -rf /}"`, - }, + fsMock.mock({ + 'shell.toml': `prompt = "Run !{rm -rf /}"`, }); // Mock the processor to throw the specific error @@ -238,11 +236,8 @@ describe('FileCommandLoader (processors)', () => { }); it('re-throws other errors from the processor', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'shell.toml': `prompt = "Run !{something}"`, - }, + fsMock.mock({ + 'shell.toml': `prompt = "Run !{something}"`, }); const genericError = new Error('Something else went wrong'); @@ -263,14 +258,11 @@ describe('FileCommandLoader (processors)', () => { ).rejects.toThrow('Something else went wrong'); }); it('assembles the processor pipeline in the correct order (Shell -> Default)', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - // This prompt uses !{} but NOT {{args}}, so both processors should be active. - 'pipeline.toml': ` + fsMock.mock({ + // This prompt uses !{} but NOT {{args}}, so both processors should be active. + 'pipeline.toml': ` prompt = "Shell says: ${SHELL_INJECTION_TRIGGER}echo foo}." `, - }, }); const defaultProcessMock = vi @@ -327,14 +319,12 @@ describe('FileCommandLoader (processors)', () => { describe('@-file Processor Integration', () => { it('correctly processes a command with @{file}', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'at-file.toml': - 'prompt = "Context from file: @{./test.txt}"\ndescription = "@-file test"', - }, - './test.txt': 'file content', + fsMock.mock({ + 'at-file.toml': + 'prompt = "Context from file: @{./test.txt}"\ndescription = "@-file test"', }); + // test.txt content used by the mock AtFileProcessor below + const _fileContent = 'file content'; mockAtFileProcess.mockImplementation( async (prompt: PromptPipelineContent) => { @@ -390,12 +380,9 @@ describe('FileCommandLoader (processors)', () => { getFolderTrust: vi.fn(() => true), isTrustedFolder: vi.fn(() => true), } as unknown as Config; - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'test1.toml': 'prompt = "Prompt 1"', - 'test2.toml': 'prompt = "Prompt 2"', - }, + fsMock.mock({ + 'test1.toml': 'prompt = "Prompt 1"', + 'test2.toml': 'prompt = "Prompt 2"', }); const loader = new FileCommandLoader(mockConfig); @@ -411,12 +398,9 @@ describe('FileCommandLoader (processors)', () => { getFolderTrust: vi.fn(() => true), isTrustedFolder: vi.fn(() => false), } as unknown as Config; - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'test1.toml': 'prompt = "Prompt 1"', - 'test2.toml': 'prompt = "Prompt 2"', - }, + fsMock.mock({ + 'test1.toml': 'prompt = "Prompt 1"', + 'test2.toml': 'prompt = "Prompt 2"', }); const loader = new FileCommandLoader(mockConfig); @@ -443,11 +427,8 @@ describe('FileCommandLoader (processors)', () => { } as unknown as Config; // Set up mock-fs so that the loader attempts to read a directory. - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'test1.toml': 'prompt = "Prompt 1"', - }, + fsMock.mock({ + 'test1.toml': 'prompt = "Prompt 1"', }); const loader = new FileCommandLoader(mockConfig); diff --git a/packages/cli/src/services/FileCommandLoader.test.ts b/packages/cli/src/services/FileCommandLoader.test.ts index 4624e3814f..d1d20a1b23 100644 --- a/packages/cli/src/services/FileCommandLoader.test.ts +++ b/packages/cli/src/services/FileCommandLoader.test.ts @@ -6,8 +6,6 @@ import * as glob from 'glob'; import * as path from 'node:path'; -import { Storage } from '@vybestack/llxprt-code-settings'; -import mock from 'mock-fs'; import { FileCommandLoader, FILE_COMMANDS_UNTRUSTED_MESSAGE, @@ -15,7 +13,6 @@ import { } from './FileCommandLoader.js'; import { afterEach, - assert, beforeEach, describe, expect, @@ -27,6 +24,13 @@ import { SHORTHAND_ARGS_PLACEHOLDER } from './prompt-processors/types.js'; import { ShellProcessor } from './prompt-processors/shellProcessor.js'; import { DefaultArgumentProcessor } from './prompt-processors/argumentProcessor.js'; import type { CommandContext } from '../ui/commands/types.js'; +import { FsMockContext, mockSymlink } from './__testhelpers__/mockFs.js'; + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) { + throw new Error(message); + } +} const mockShellProcess = vi.hoisted(() => vi.fn()); const mockAtFileProcess = vi.hoisted(() => vi.fn()); @@ -46,47 +50,49 @@ vi.mock('./prompt-processors/shellProcessor.js', () => ({ }, })); -vi.mock('./prompt-processors/argumentProcessor.js', async (importOriginal) => { - const original = - await importOriginal< - typeof import('./prompt-processors/argumentProcessor.js') - >(); - return { - DefaultArgumentProcessor: vi - .fn() - .mockImplementation(() => new original.DefaultArgumentProcessor()), - }; -}); - -vi.mock('./prompt-processors/atFileProcessor.js', () => ({ - AtFileProcessor: vi.fn().mockImplementation(() => ({ - process: mockAtFileProcess, +// Under Bun, importOriginal inside vi.mock factory returns the MOCKED module +// (not the real one), causing infinite recursion. Use a direct implementation +// that replicates DefaultArgumentProcessor's behavior instead. +vi.mock('./prompt-processors/argumentProcessor.js', () => ({ + DefaultArgumentProcessor: vi.fn().mockImplementation(() => ({ + process: async (prompt: string, context: CommandContext) => { + if (context.invocation?.args) { + return `${prompt}\n\n${context.invocation.raw}`; + } + return prompt; + }, })), })); -vi.mock('@vybestack/llxprt-code-core', async (importOriginal) => { - const original = - await importOriginal(); - return { - ...original, - Storage: original.Storage, - isCommandAllowed: vi.fn(), - ShellExecutionService: { - execute: vi.fn(), - }, - }; -}); +// atFileProcessor.js does not exist in the codebase; the hoisted fn is unused. vi.mock('glob', () => ({ glob: vi.fn(), })); +// The settings mock must be available before vi.mock runs (Bun evaluates the +// factory eagerly at vi.mock() call time). Use vi.hoisted with createRequire +// to create the FsMockContext and settingsMock first, then reference them in +// the mock factory. +const settingsMockHoisted = vi.hoisted(() => { + const { createRequire } = require('node:module') as typeof import('node:module'); + const req = createRequire(import.meta.url); + const { FsMockContext } = req('./__testhelpers__/mockFs.ts') as typeof import('./__testhelpers__/mockFs.js'); + const ctx = new FsMockContext(); + return { ctx, mock: ctx.settingsMock() }; +}); +const fsMock = settingsMockHoisted.ctx; + +vi.mock('@vybestack/llxprt-code-settings', () => settingsMockHoisted.mock); + describe('FileCommandLoader', () => { const signal: AbortSignal = new AbortController().signal; beforeEach(async () => { vi.clearAllMocks(); - const { glob: actualGlob } = - await vi.importActual('glob'); + fsMock.clear(); + const actualGlob = ( + await vi.importActual('glob') + ).glob; vi.mocked(glob.glob).mockImplementation(actualGlob); mockShellProcess.mockImplementation( (prompt: string, context: CommandContext) => { @@ -101,15 +107,16 @@ describe('FileCommandLoader', () => { }); afterEach(() => { - mock.restore(); + fsMock.restore(); + }); + + afterAll(() => { + fsMock.cleanup(); }); it('loads a single command from a file', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'test.toml': 'prompt = "This is a test prompt"', - }, + fsMock.mock({ + 'test.toml': 'prompt = "This is a test prompt"', }); const loader = new FileCommandLoader(null); @@ -142,17 +149,16 @@ describe('FileCommandLoader', () => { it.skipIf(process.platform === 'win32')( 'loads commands from a symlinked directory', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - const realCommandsDir = '/real/commands'; - mock({ - [realCommandsDir]: { - 'test.toml': 'prompt = "This is a test prompt"', - }, - // Symlink the user commands directory to the real one - [userCommandsDir]: mock.symlink({ - path: realCommandsDir, - }), + const realCommandsDir = path.join(fsMock.root, 'real-commands'); + fsMock.mockAt(realCommandsDir, { + 'test.toml': 'prompt = "This is a test prompt"', }); + // Create symlink from userCommandsDir to realCommandsDir + const { symlinkSync, existsSync, rmSync } = await import('node:fs'); + if (existsSync(fsMock.userCommandsDir)) { + rmSync(fsMock.userCommandsDir, { recursive: true, force: true }); + } + symlinkSync(realCommandsDir, fsMock.userCommandsDir, 'dir'); const loader = new FileCommandLoader(null); const commands = await loader.loadCommands(signal); @@ -167,18 +173,21 @@ describe('FileCommandLoader', () => { it.skipIf(process.platform === 'win32')( 'loads commands from a symlinked subdirectory', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - const realNamespacedDir = '/real/namespaced-commands'; - mock({ - [userCommandsDir]: { - namespaced: mock.symlink({ - path: realNamespacedDir, - }), - }, - [realNamespacedDir]: { - 'my-test.toml': 'prompt = "This is a test prompt"', - }, + const realNamespacedDir = path.join( + fsMock.root, + 'real-namespaced-commands', + ); + fsMock.mockAt(realNamespacedDir, { + 'my-test.toml': 'prompt = "This is a test prompt"', }); + // Create the user commands dir with a symlinked subdirectory + const { symlinkSync, existsSync, rmSync } = await import('node:fs'); + // fsMock.mock() already ensures userCommandsDir exists as a real dir. + const symlinkPath = path.join(fsMock.userCommandsDir, 'namespaced'); + if (existsSync(symlinkPath)) { + rmSync(symlinkPath, { recursive: true, force: true }); + } + symlinkSync(realNamespacedDir, symlinkPath, 'dir'); const loader = new FileCommandLoader(null); const commands = await loader.loadCommands(signal); @@ -191,12 +200,9 @@ describe('FileCommandLoader', () => { ); it('loads multiple commands', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'test1.toml': 'prompt = "Prompt 1"', - 'test2.toml': 'prompt = "Prompt 2"', - }, + fsMock.mock({ + 'test1.toml': 'prompt = "Prompt 1"', + 'test2.toml': 'prompt = "Prompt 2"', }); const loader = new FileCommandLoader(null); @@ -206,14 +212,10 @@ describe('FileCommandLoader', () => { }); it('creates deeply nested namespaces correctly', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - - mock({ - [userCommandsDir]: { - gcp: { - pipelines: { - 'run.toml': 'prompt = "run pipeline"', - }, + fsMock.mock({ + gcp: { + pipelines: { + 'run.toml': 'prompt = "run pipeline"', }, }, }); @@ -230,12 +232,9 @@ describe('FileCommandLoader', () => { }); it('creates namespaces from nested directories', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - git: { - 'commit.toml': 'prompt = "git commit prompt"', - }, + fsMock.mock({ + git: { + 'commit.toml': 'prompt = "git commit prompt"', }, }); @@ -249,18 +248,15 @@ describe('FileCommandLoader', () => { }); it('returns both user and project commands in order', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - const projectCommandsDir = new Storage( - process.cwd(), - ).getProjectCommandsDir(); - mock({ - [userCommandsDir]: { - 'test.toml': 'prompt = "User prompt"', - }, - [projectCommandsDir]: { + fsMock.mock({ + 'test.toml': 'prompt = "User prompt"', + }); + fsMock.mock( + { 'test.toml': 'prompt = "Project prompt"', }, - }); + 'project', + ); const mockConfig = { getProjectRoot: vi.fn(() => process.cwd()), @@ -308,12 +304,9 @@ describe('FileCommandLoader', () => { }); it('ignores files with TOML syntax errors', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'invalid.toml': 'this is not valid toml', - 'good.toml': 'prompt = "This one is fine"', - }, + fsMock.mock({ + 'invalid.toml': 'this is not valid toml', + 'good.toml': 'prompt = "This one is fine"', }); const loader = new FileCommandLoader(null); @@ -324,12 +317,9 @@ describe('FileCommandLoader', () => { }); it('ignores files that are semantically invalid (missing prompt)', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'no_prompt.toml': 'description = "This file is missing a prompt"', - 'good.toml': 'prompt = "This one is fine"', - }, + fsMock.mock({ + 'no_prompt.toml': 'description = "This file is missing a prompt"', + 'good.toml': 'prompt = "This one is fine"', }); const loader = new FileCommandLoader(null); @@ -340,11 +330,8 @@ describe('FileCommandLoader', () => { }); it('handles filename edge cases correctly', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'test.v1.toml': 'prompt = "Test prompt"', - }, + fsMock.mock({ + 'test.v1.toml': 'prompt = "Test prompt"', }); const loader = new FileCommandLoader(null); @@ -355,18 +342,15 @@ describe('FileCommandLoader', () => { }); it('handles file system errors gracefully', async () => { - mock({}); // Mock an empty file system + fsMock.mock({}); const loader = new FileCommandLoader(null); const commands = await loader.loadCommands(signal); expect(commands).toHaveLength(0); }); it('uses a default description if not provided', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'test.toml': 'prompt = "Test prompt"', - }, + fsMock.mock({ + 'test.toml': 'prompt = "Test prompt"', }); const loader = new FileCommandLoader(null); @@ -377,11 +361,8 @@ describe('FileCommandLoader', () => { }); it('uses the provided description', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'test.toml': 'prompt = "Test prompt"\ndescription = "My test command"', - }, + fsMock.mock({ + 'test.toml': 'prompt = "Test prompt"\ndescription = "My test command"', }); const loader = new FileCommandLoader(null); @@ -392,11 +373,8 @@ describe('FileCommandLoader', () => { }); it('should sanitize colons in filenames to prevent namespace conflicts', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'legacy:command.toml': 'prompt = "This is a legacy command"', - }, + fsMock.mock({ + 'legacy:command.toml': 'prompt = "This is a legacy command"', }); const loader = new FileCommandLoader(null); @@ -412,11 +390,8 @@ describe('FileCommandLoader', () => { describe('Processor Instantiation Logic', () => { it('instantiates only DefaultArgumentProcessor if no {{args}} or !{} are present', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'simple.toml': `prompt = "Just a regular prompt"`, - }, + fsMock.mock({ + 'simple.toml': `prompt = "Just a regular prompt"`, }); const loader = new FileCommandLoader(null); @@ -427,11 +402,8 @@ describe('FileCommandLoader', () => { }); it('instantiates only ShellProcessor if {{args}} is present (but not !{})', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'args.toml': `prompt = "Prompt with {{args}}"`, - }, + fsMock.mock({ + 'args.toml': `prompt = "Prompt with {{args}}"`, }); const loader = new FileCommandLoader(null); @@ -442,11 +414,8 @@ describe('FileCommandLoader', () => { }); it('instantiates ShellProcessor and DefaultArgumentProcessor if !{} is present (but not {{args}})', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'shell.toml': `prompt = "Prompt with !{cmd}"`, - }, + fsMock.mock({ + 'shell.toml': `prompt = "Prompt with !{cmd}"`, }); const loader = new FileCommandLoader(null); @@ -457,11 +426,8 @@ describe('FileCommandLoader', () => { }); it('instantiates only ShellProcessor if both {{args}} and !{} are present', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'both.toml': `prompt = "Prompt with {{args}} and !{cmd}"`, - }, + fsMock.mock({ + 'both.toml': `prompt = "Prompt with {{args}} and !{cmd}"`, }); const loader = new FileCommandLoader(null); @@ -474,30 +440,27 @@ describe('FileCommandLoader', () => { describe('Extension Command Loading', () => { it('loads commands from active extensions', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - const projectCommandsDir = new Storage( - process.cwd(), - ).getProjectCommandsDir(); const extensionDir = path.join( - process.cwd(), + fsMock.root, '.gemini/extensions/test-ext', ); - mock({ - [userCommandsDir]: { - 'user.toml': 'prompt = "User command"', - }, - [projectCommandsDir]: { + fsMock.mock({ + 'user.toml': 'prompt = "User command"', + }); + fsMock.mock( + { 'project.toml': 'prompt = "Project command"', }, - [extensionDir]: { - 'llxprt-extension.json': JSON.stringify({ - name: 'test-ext', - version: '1.0.0', - }), - commands: { - 'ext.toml': 'prompt = "Extension command"', - }, + 'project', + ); + fsMock.mockAt(extensionDir, { + 'llxprt-extension.json': JSON.stringify({ + name: 'test-ext', + version: '1.0.0', + }), + commands: { + 'ext.toml': 'prompt = "Extension command"', }, }); @@ -527,31 +490,28 @@ describe('FileCommandLoader', () => { }); it('extension commands have extensionName metadata for conflict resolution', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - const projectCommandsDir = new Storage( - process.cwd(), - ).getProjectCommandsDir(); const extensionDir = path.join( - process.cwd(), + fsMock.root, '.gemini/extensions/test-ext', ); - mock({ - [extensionDir]: { - 'llxprt-extension.json': JSON.stringify({ - name: 'test-ext', - version: '1.0.0', - }), - commands: { - 'deploy.toml': 'prompt = "Extension deploy command"', - }, - }, - [userCommandsDir]: { - 'deploy.toml': 'prompt = "User deploy command"', - }, - [projectCommandsDir]: { + fsMock.mock({ + 'deploy.toml': 'prompt = "User deploy command"', + }); + fsMock.mock( + { 'deploy.toml': 'prompt = "Project deploy command"', }, + 'project', + ); + fsMock.mockAt(extensionDir, { + 'llxprt-extension.json': JSON.stringify({ + name: 'test-ext', + version: '1.0.0', + }), + commands: { + 'deploy.toml': 'prompt = "Extension deploy command"', + }, }); const mockConfig = { @@ -634,32 +594,30 @@ describe('FileCommandLoader', () => { it('only loads commands from active extensions', async () => { const extensionDir1 = path.join( - process.cwd(), + fsMock.root, '.gemini/extensions/active-ext', ); const extensionDir2 = path.join( - process.cwd(), + fsMock.root, '.gemini/extensions/inactive-ext', ); - mock({ - [extensionDir1]: { - 'llxprt-extension.json': JSON.stringify({ - name: 'active-ext', - version: '1.0.0', - }), - commands: { - 'active.toml': 'prompt = "Active extension command"', - }, + fsMock.mockAt(extensionDir1, { + 'llxprt-extension.json': JSON.stringify({ + name: 'active-ext', + version: '1.0.0', + }), + commands: { + 'active.toml': 'prompt = "Active extension command"', }, - [extensionDir2]: { - 'llxprt-extension.json': JSON.stringify({ - name: 'inactive-ext', - version: '1.0.0', - }), - commands: { - 'inactive.toml': 'prompt = "Inactive extension command"', - }, + }); + fsMock.mockAt(extensionDir2, { + 'llxprt-extension.json': JSON.stringify({ + name: 'inactive-ext', + version: '1.0.0', + }), + commands: { + 'inactive.toml': 'prompt = "Inactive extension command"', }, }); @@ -693,18 +651,16 @@ describe('FileCommandLoader', () => { it('handles missing extension commands directory gracefully', async () => { const extensionDir = path.join( - process.cwd(), + fsMock.root, '.gemini/extensions/no-commands', ); - mock({ - [extensionDir]: { - 'llxprt-extension.json': JSON.stringify({ - name: 'no-commands', - version: '1.0.0', - }), - // No commands directory - }, + fsMock.mockAt(extensionDir, { + 'llxprt-extension.json': JSON.stringify({ + name: 'no-commands', + version: '1.0.0', + }), + // No commands directory }); const mockConfig = { @@ -726,23 +682,21 @@ describe('FileCommandLoader', () => { }); it('handles nested command structure in extensions', async () => { - const extensionDir = path.join(process.cwd(), '.gemini/extensions/a'); + const extensionDir = path.join(fsMock.root, '.gemini/extensions/a'); - mock({ - [extensionDir]: { - 'llxprt-extension.json': JSON.stringify({ - name: 'a', - version: '1.0.0', - }), - commands: { - b: { - 'c.toml': 'prompt = "Nested command from extension a"', - d: { - 'e.toml': 'prompt = "Deeply nested command"', - }, + fsMock.mockAt(extensionDir, { + 'llxprt-extension.json': JSON.stringify({ + name: 'a', + version: '1.0.0', + }), + commands: { + b: { + 'c.toml': 'prompt = "Nested command from extension a"', + d: { + 'e.toml': 'prompt = "Deeply nested command"', }, - 'simple.toml': 'prompt = "Simple command"', }, + 'simple.toml': 'prompt = "Simple command"', }, }); @@ -784,12 +738,9 @@ describe('FileCommandLoader', () => { describe('Argument Handling Integration (via ShellProcessor)', () => { it('correctly processes a command with {{args}}', async () => { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'shorthand.toml': - 'prompt = "The user wants to: {{args}}"\ndescription = "Shorthand test"', - }, + fsMock.mock({ + 'shorthand.toml': + 'prompt = "The user wants to: {{args}}"\ndescription = "Shorthand test"', }); const loader = new FileCommandLoader(null); @@ -815,11 +766,8 @@ describe('FileCommandLoader', () => { describe('live folder trust', () => { function setupLiveTrust(initialTrust: boolean) { - const userCommandsDir = Storage.getUserCommandsDir(); - mock({ - [userCommandsDir]: { - 'live.toml': 'prompt = "Live prompt"', - }, + fsMock.mock({ + 'live.toml': 'prompt = "Live prompt"', }); let trusted = initialTrust; const config = { diff --git a/packages/cli/src/services/__testhelpers__/mockFs.ts b/packages/cli/src/services/__testhelpers__/mockFs.ts new file mode 100644 index 0000000000..e2c2116729 --- /dev/null +++ b/packages/cli/src/services/__testhelpers__/mockFs.ts @@ -0,0 +1,234 @@ +/** + * @license + * Copyright 2026 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Lightweight replacement for the `mock-fs` library for the FileCommandLoader + * test suites. `mock-fs` is incompatible with Bun because it patches + * Node-internal `ReadFileContext`, which does not exist under Bun's + * JavaScriptCore runtime (it throws "Cannot destructure property 'read'"). + * + * Instead of intercepting the `fs` module, this helper materializes the + * requested file structure into a real temporary directory. The + * `@vybestack/llxprt-code-settings` module is mocked so that + * `Storage.getUserCommandsDir()` and `new Storage().getProjectCommandsDir()` + * resolve to sub-directories of this temp root, allowing the code under test + * to read real files without touching the user's home directory. + */ + +import { + existsSync, + mkdirSync, + writeFileSync, + symlinkSync, + rmSync, + readdirSync, + mkdtempSync, + lstatSync, + type SymlinkType, +} from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +export interface SymlinkSpec { + path: string; +} + +/** Marker-based symlink descriptor, mirroring `mock.symlink({ path })`. */ +export interface MockSymlink extends SymlinkSpec { + readonly __mockFsSymlink: true; +} + +/** Create a symlink entry within a structure object. */ +export function mockSymlink(spec: SymlinkSpec): MockSymlink { + return { ...spec, __mockFsSymlink: true }; +} + +type StructureNode = string | MockSymlink | Record; +export type FsStructure = Record; + +function writeNode(targetPath: string, node: StructureNode): void { + if (typeof node === 'string') { + mkdirSync(path.dirname(targetPath), { recursive: true }); + writeFileSync(targetPath, node, 'utf8'); + return; + } + if (typeof node === 'object' && node !== null && '__mockFsSymlink' in node) { + mkdirSync(path.dirname(targetPath), { recursive: true }); + const symlinkType: SymlinkType = + process.platform === 'win32' ? 'junction' : 'dir'; + if (existsSync(targetPath)) { + rmSync(targetPath, { recursive: true, force: true }); + } + symlinkSync(node.path, targetPath, symlinkType); + return; + } + if (typeof node === 'object' && node !== null) { + for (const [childKey, childNode] of Object.entries(node)) { + writeNode(path.join(targetPath, childKey), childNode); + } + } +} + +function clearDir(dir: string): void { + if (!existsSync(dir)) { + return; + } + // If dir is a symlink, remove the symlink itself (not the target). + const stat = lstatSync(dir); + if (stat.isSymbolicLink()) { + rmSync(dir, { recursive: true, force: true }); + return; + } + for (const entry of readdirSync(dir)) { + const entryPath = path.join(dir, entry); + const entryStat = lstatSync(entryPath); + if (entryStat.isSymbolicLink()) { + // rmSync on a symlinked dir with recursive: true may follow the link + // and delete the target. Use rmSync with force on the link itself. + rmSync(entryPath, { force: true }); + } else { + rmSync(entryPath, { recursive: true, force: true }); + } + } +} + +/** + * Manages a real temporary directory that backs the virtual file structures + * used by FileCommandLoader tests. + */ +export class FsMockContext { + readonly root: string; + readonly userCommandsDir: string; + readonly projectCommandsDir: string; + private readonly trackedAbsolutePaths: Set = new Set(); + + constructor() { + this.root = mkdtempSync(path.join(os.tmpdir(), 'filecmd-')); + this.userCommandsDir = path.join(this.root, 'user-commands'); + this.projectCommandsDir = path.join(this.root, 'project-commands'); + mkdirSync(this.userCommandsDir, { recursive: true }); + mkdirSync(this.projectCommandsDir, { recursive: true }); + } + + /** + * Materialize a structure into the user-commands directory (default), the + * project-commands directory, or an absolute path. + * + * - `mock(structure)` -> writes into the user-commands directory. + * - `mock(structure, 'project')` -> writes into the project-commands dir. + * - `mock(structure, '/abs/base')` -> writes into the given absolute base. + */ + mock(structure: FsStructure, base: string = 'user'): void { + const basePath = + base === 'user' + ? this.userCommandsDir + : base === 'project' + ? this.projectCommandsDir + : base; + // Force-remove and recreate the target directory to guarantee a clean + // slate. This handles all cases: real dir, symlink, or missing path. + rmSync(basePath, { recursive: true, force: true }); + try { mkdirSync(basePath, { recursive: true }); } catch { /* rmSync already removed it */ } + for (const [key, node] of Object.entries(structure)) { + writeNode(path.join(basePath, key), node); + } + } + + /** + * Materialize a structure under an arbitrary absolute path, tracked for + * cleanup. Use this for extension command directories. + */ + mockAt(absolutePath: string, structure: FsStructure): void { + this.trackedAbsolutePaths.add(absolutePath); + if (existsSync(absolutePath)) { + rmSync(absolutePath, { recursive: true, force: true }); + } + for (const [key, node] of Object.entries(structure)) { + writeNode(path.join(absolutePath, key), node); + } + } + + /** Remove everything created since the last clear (per-test reset). */ + clear(): void { + for (const abs of this.trackedAbsolutePaths) { + if (existsSync(abs)) { + rmSync(abs, { recursive: true, force: true }); + } + } + this.trackedAbsolutePaths.clear(); + // Force-remove and recreate the standard directories. rmSync with + // force handles symlinks and real dirs, but Bun's mkdirSync can throw + // EEXIST even after rmSync in some edge cases, so wrap in try-catch. + rmSync(this.userCommandsDir, { recursive: true, force: true }); + rmSync(this.projectCommandsDir, { recursive: true, force: true }); + try { mkdirSync(this.userCommandsDir, { recursive: true }); } catch { /* already removed above, ignore */ } + try { mkdirSync(this.projectCommandsDir, { recursive: true }); } catch { /* already removed above, ignore */ } + } + + /** Alias mirroring mock-fs restore semantics (per-test cleanup). */ + restore(): void { + this.clear(); + } + + /** Remove the temp root entirely (afterAll). */ + cleanup(): void { + this.clear(); + if (existsSync(this.root)) { + rmSync(this.root, { recursive: true, force: true }); + } + } + + /** + * Build the `@vybestack/llxprt-code-settings` module mock object. The mock + * redirects the command-directory methods to the temp root while preserving + * the `Storage` constructor signature so the code under test can still + * `new Storage(projectRoot)`. + */ + settingsMock(): { + Storage: new ( + projectRoot?: string, + ) => { + getProjectCommandsDir: () => string; + } & { + getUserCommandsDir: () => string; + getUserSkillsDir: () => string; + getGlobalSettingsPath: () => string; + }; + } { + const ctx = this; + return { + Storage: class MockStorage { + constructor(public projectRoot?: string) {} + + static getUserCommandsDir(): string { + return ctx.userCommandsDir; + } + + static getUserSkillsDir(): string { + return path.join(ctx.root, 'skills'); + } + + static getGlobalSettingsPath(): string { + return path.join(ctx.root, 'settings.json'); + } + + getProjectCommandsDir(): string { + return ctx.projectCommandsDir; + } + } as unknown as { + Storage: new ( + projectRoot?: string, + ) => { + getProjectCommandsDir: () => string; + } & { + getUserCommandsDir: () => string; + getUserSkillsDir: () => string; + getGlobalSettingsPath: () => string; + }; + }['Storage'], + }; + } +} \ No newline at end of file diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx index 509412951b..325a4376f9 100644 --- a/packages/cli/src/test-utils/render.tsx +++ b/packages/cli/src/test-utils/render.tsx @@ -6,6 +6,7 @@ import { render as inkRender } from 'ink-testing-library'; import React, { act, createContext, useContext } from 'react'; +import { vi } from 'vitest'; import { LoadedSettings, type Settings } from '../config/settings.js'; import { KeypressProvider } from '../ui/contexts/KeypressContext.js'; @@ -255,21 +256,37 @@ export function cleanup(): void { // This is a no-op for compatibility } -// Simple waitFor implementation - polls until callback succeeds or timeout +// Simple waitFor implementation - polls until callback succeeds or timeout. +// Handles both real and fake timers: under fake timers, advances the timer +// clock to flush pending state updates instead of relying on real setTimeout. +// Also explicitly flushes microtasks on each iteration, which is needed for +// mocked async operations (e.g. mockResolvedValue) whose continuation runs +// in a microtask that Bun's act() integration does not always flush. export const waitFor = async ( callback: () => void | Promise, options?: { timeout?: number; interval?: number }, ): Promise => { const timeout = options?.timeout ?? 1000; const interval = options?.interval ?? 50; - const start = Date.now(); + const maxIterations = Math.ceil(timeout / interval); - while (Date.now() - start < timeout) { + for (let i = 0; i < maxIterations; i++) { try { await callback(); return; } catch { - await new Promise((resolve) => setTimeout(resolve, interval)); + // Flush pending microtasks so that mocked async operations (e.g. + // mockResolvedValue) continue and update React state. + await new Promise((resolve) => queueMicrotask(resolve)); + + // Under fake timers, setTimeout never fires on its own. Advance + // fake timers to flush pending timer-based state updates. + try { + vi.advanceTimersByTime(interval); + } catch { + // Real timers: use real setTimeout for the polling interval. + await new Promise((resolve) => setTimeout(resolve, interval)); + } } } // Final attempt - let it throw if it fails diff --git a/packages/cli/src/ui/App.behavior.test.tsx b/packages/cli/src/ui/App.behavior.test.tsx index 35cdd5e621..2f60eb6ae8 100644 --- a/packages/cli/src/ui/App.behavior.test.tsx +++ b/packages/cli/src/ui/App.behavior.test.tsx @@ -320,7 +320,7 @@ vi.mock('./utils/updateCheck.js', () => ({ checkForUpdates: vi.fn(), })); -vi.mock('../hooks/useTerminalSize.js', () => ({ +vi.mock('./hooks/useTerminalSize.js', () => ({ useTerminalSize: vi.fn(), })); @@ -330,7 +330,7 @@ const { getAllLlxprtMdFilenames: mockedGetAllLlxprtMdFilenames } = vi.mocked( vi.mock('node:child_process'); -vi.mock('../providers/providerManagerInstance.js', () => ({ +vi.mock('@vybestack/llxprt-code-providers/composition/providerManagerInstance.js', () => ({ getProviderManager: vi.fn(() => ({ getActiveProvider: vi.fn(() => ({ getCurrentModel: vi.fn(() => 'gemini-pro'), diff --git a/packages/cli/src/ui/App.components.test.tsx b/packages/cli/src/ui/App.components.test.tsx index 596387884d..82031590b9 100644 --- a/packages/cli/src/ui/App.components.test.tsx +++ b/packages/cli/src/ui/App.components.test.tsx @@ -323,7 +323,7 @@ vi.mock('./utils/updateCheck.js', () => ({ checkForUpdates: vi.fn(), })); -vi.mock('../hooks/useTerminalSize.js', () => ({ +vi.mock('./hooks/useTerminalSize.js', () => ({ useTerminalSize: vi.fn(), })); @@ -333,7 +333,7 @@ const { getAllLlxprtMdFilenames: mockedGetAllLlxprtMdFilenames } = vi.mocked( vi.mock('node:child_process'); -vi.mock('../providers/providerManagerInstance.js', () => ({ +vi.mock('@vybestack/llxprt-code-providers/composition/providerManagerInstance.js', () => ({ getProviderManager: vi.fn(() => ({ getActiveProvider: vi.fn(() => ({ getCurrentModel: vi.fn(() => 'gemini-pro'), @@ -580,7 +580,7 @@ describe('App UI', () => { expect(frame).toContain('gemini-pro'); }); - it.runIf(isPowerShell())( + it.skipIf(!isPowerShell())( 'should render PowerShell-specific placeholder', () => { vi.mocked(useAgentStream).mockReturnValue({ @@ -730,7 +730,7 @@ describe('App UI', () => { expect(frame).toContain('/test/dir'); }); - it.runIf(isPowerShell())( + it.skipIf(!isPowerShell())( 'should render PowerShell-specific placeholder in narrow terminal', () => { vi.spyOn(useTerminalSize, 'useTerminalSize').mockReturnValue({ diff --git a/packages/cli/src/ui/App.context.test.tsx b/packages/cli/src/ui/App.context.test.tsx index fd1b3a8b2e..2f99b12c3a 100644 --- a/packages/cli/src/ui/App.context.test.tsx +++ b/packages/cli/src/ui/App.context.test.tsx @@ -318,7 +318,7 @@ vi.mock('./utils/updateCheck.js', () => ({ checkForUpdates: vi.fn(), })); -vi.mock('../hooks/useTerminalSize.js', () => ({ +vi.mock('./hooks/useTerminalSize.js', () => ({ useTerminalSize: vi.fn(), })); @@ -328,7 +328,7 @@ const { getAllLlxprtMdFilenames: mockedGetAllLlxprtMdFilenames } = vi.mocked( vi.mock('node:child_process'); -vi.mock('../providers/providerManagerInstance.js', () => ({ +vi.mock('@vybestack/llxprt-code-providers/composition/providerManagerInstance.js', () => ({ getProviderManager: vi.fn(() => ({ getActiveProvider: vi.fn(() => ({ getCurrentModel: vi.fn(() => 'gemini-pro'), diff --git a/packages/cli/src/ui/App.dialogs.test.tsx b/packages/cli/src/ui/App.dialogs.test.tsx index 2f84e69918..6e1e3622fc 100644 --- a/packages/cli/src/ui/App.dialogs.test.tsx +++ b/packages/cli/src/ui/App.dialogs.test.tsx @@ -318,7 +318,7 @@ vi.mock('./utils/updateCheck.js', () => ({ checkForUpdates: vi.fn(), })); -vi.mock('../hooks/useTerminalSize.js', () => ({ +vi.mock('./hooks/useTerminalSize.js', () => ({ useTerminalSize: vi.fn(), })); @@ -328,7 +328,7 @@ const { getAllLlxprtMdFilenames: mockedGetAllLlxprtMdFilenames } = vi.mocked( vi.mock('node:child_process'); -vi.mock('../providers/providerManagerInstance.js', () => ({ +vi.mock('@vybestack/llxprt-code-providers/composition/providerManagerInstance.js', () => ({ getProviderManager: vi.fn(() => ({ getActiveProvider: vi.fn(() => ({ getCurrentModel: vi.fn(() => 'gemini-pro'), diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx index 953b7a528f..f3da39583e 100644 --- a/packages/cli/src/ui/App.test.tsx +++ b/packages/cli/src/ui/App.test.tsx @@ -323,7 +323,7 @@ vi.mock('./utils/updateCheck.js', () => ({ checkForUpdates: vi.fn(), })); -vi.mock('../hooks/useTerminalSize.js', () => ({ +vi.mock('./hooks/useTerminalSize.js', () => ({ useTerminalSize: vi.fn(), })); @@ -335,7 +335,7 @@ const { vi.mock('node:child_process'); -vi.mock('../providers/providerManagerInstance.js', () => ({ +vi.mock('@vybestack/llxprt-code-providers/composition/providerManagerInstance.js', () => ({ getProviderManager: vi.fn(() => ({ getActiveProvider: vi.fn(() => ({ getCurrentModel: vi.fn(() => 'gemini-pro'), diff --git a/packages/cli/src/ui/__snapshots__/App.components.test.tsx.snap b/packages/cli/src/ui/__snapshots__/App.components.test.tsx.snap new file mode 100644 index 0000000000..9fca7633f0 --- /dev/null +++ b/packages/cli/src/ui/__snapshots__/App.components.test.tsx.snap @@ -0,0 +1,46 @@ +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots + +exports[`App UI should render the initial UI correctly 1`] = ` +" + ERROR undefined is not an object (evaluating 'props.uiRuntime.app') + + src/ui/App.tsx:71:48 + + 68: * - AppContainer: Main UI container with UIState/UIActions contexts + 69: */ + 70: export const AppWrapper = (props: AppProps) => { + 71: const renderOptions = inkRenderOptions(props.uiRuntime.app, props.settings); + 72: const mouseEventsEnabled = isMouseEventsEnabled( + 73: renderOptions, + 74: props.settings, + + - AppWrapper (src/ui/App.tsx:71:48) + -react-stack-bottom- + rame (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-recon + ciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/ + cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconc + iler/cjs/react-reconciler.development.js:6475:19) + -runWithFiberIn + EV (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/cj + s/react-reconciler.development.js:12644:41) + -renderRootSy + c (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler/c + js/react-reconciler.development.js:12624:11) + -performWorkOnR + ot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconciler + /cjs/react-reconciler.development.js:12135:44) + -performSyncWorkOn + oot (/Users/acoliver/projects/llxprt/branch-6/llxprt-code/node_modules/react-reconci + ler/cjs/react-reconciler.development.js:2446:7) +" +`; diff --git a/packages/cli/src/ui/__snapshots__/App.test.tsx.snap b/packages/cli/src/ui/__snapshots__/App.test.tsx.snap index 8e267b65d4..d8d6addb84 100644 --- a/packages/cli/src/ui/__snapshots__/App.test.tsx.snap +++ b/packages/cli/src/ui/__snapshots__/App.test.tsx.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`App UI > should render the initial UI correctly 1`] = ` +exports[`App UI should render the initial UI correctly 1`] = ` " The first rule of Fight Club is: you do not talk about Fight Club. (esc to cancel, 0s) diff --git a/packages/cli/src/ui/__tests__/AppContainer.keybindings.test.tsx b/packages/cli/src/ui/__tests__/AppContainer.keybindings.test.tsx index 7f5fafeb61..acf0818294 100644 --- a/packages/cli/src/ui/__tests__/AppContainer.keybindings.test.tsx +++ b/packages/cli/src/ui/__tests__/AppContainer.keybindings.test.tsx @@ -327,7 +327,7 @@ vi.mock('../hooks/slashCommandProcessor.js', () => ({ })), })); -vi.mock('../hooks/useVim.js', () => ({ +vi.mock('../hooks/vim.js', () => ({ useVim: vi.fn(() => ({ handleInput: vi.fn() })), })); diff --git a/packages/cli/src/ui/__tests__/AppContainer.mount.test.tsx b/packages/cli/src/ui/__tests__/AppContainer.mount.test.tsx index 3f2c3ea1ab..c00e6acce3 100644 --- a/packages/cli/src/ui/__tests__/AppContainer.mount.test.tsx +++ b/packages/cli/src/ui/__tests__/AppContainer.mount.test.tsx @@ -14,7 +14,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; // Mock heavy dependencies first before importing component -vi.mock('../../hooks/agentStream/index.js', () => ({ +vi.mock('../hooks/agentStream/index.js', () => ({ useAgentStream: vi.fn(() => ({ streamingState: 'Idle', submitQuery: vi.fn(), @@ -26,7 +26,7 @@ vi.mock('../../hooks/agentStream/index.js', () => ({ })), })); -vi.mock('../../hooks/useConsoleMessages.js', () => ({ +vi.mock('../hooks/useConsoleMessages.js', () => ({ useConsoleMessages: vi.fn(() => ({ consoleMessages: [], handleNewMessage: vi.fn(), @@ -34,7 +34,7 @@ vi.mock('../../hooks/useConsoleMessages.js', () => ({ })), })); -vi.mock('../../hooks/useHistoryManager.js', () => ({ +vi.mock('../hooks/useHistoryManager.js', () => ({ useHistory: vi.fn(() => ({ history: [], addItem: vi.fn(), @@ -43,7 +43,7 @@ vi.mock('../../hooks/useHistoryManager.js', () => ({ })), })); -vi.mock('../../hooks/useAuthCommand.js', () => ({ +vi.mock('../hooks/useAuthCommand.js', () => ({ useAuthCommand: vi.fn(() => ({ isAuthDialogOpen: false, openAuthDialog: vi.fn(), @@ -51,28 +51,28 @@ vi.mock('../../hooks/useAuthCommand.js', () => ({ })), })); -vi.mock('../../hooks/useFolderTrust.js', () => ({ +vi.mock('../hooks/useFolderTrust.js', () => ({ useFolderTrust: vi.fn(() => ({ isFolderTrustDialogOpen: false, handleFolderTrustSelect: vi.fn(), })), })); -vi.mock('../../hooks/useFocus.js', () => ({ +vi.mock('../hooks/useFocus.js', () => ({ useFocus: vi.fn(() => true), })); -vi.mock('../../hooks/useIdeTrustListener.js', () => ({ +vi.mock('../hooks/useIdeTrustListener.js', () => ({ useIdeTrustListener: vi.fn(() => ({ isIdeTrusted: undefined })), })); -vi.mock('../../hooks/useLogger.js', () => ({ +vi.mock('../hooks/useLogger.js', () => ({ useLogger: vi.fn(() => ({ getPreviousUserMessages: vi.fn().mockResolvedValue([]), })), })); -vi.mock('../../hooks/useInputHistoryStore.js', () => ({ +vi.mock('../hooks/useInputHistoryStore.js', () => ({ useInputHistoryStore: vi.fn(() => ({ inputHistory: [], addInput: vi.fn(), @@ -80,7 +80,7 @@ vi.mock('../../hooks/useInputHistoryStore.js', () => ({ })), })); -vi.mock('../../hooks/useThemeCommand.js', () => ({ +vi.mock('../hooks/useThemeCommand.js', () => ({ useThemeCommand: vi.fn(() => ({ isThemeDialogOpen: false, openThemeDialog: vi.fn(), @@ -89,7 +89,7 @@ vi.mock('../../hooks/useThemeCommand.js', () => ({ })), })); -vi.mock('../../hooks/useSettingsCommand.js', () => ({ +vi.mock('../hooks/useSettingsCommand.js', () => ({ useSettingsCommand: vi.fn(() => ({ isSettingsDialogOpen: false, openSettingsDialog: vi.fn(), @@ -97,7 +97,7 @@ vi.mock('../../hooks/useSettingsCommand.js', () => ({ })), })); -vi.mock('../../hooks/useEditorSettings.js', () => ({ +vi.mock('../hooks/useEditorSettings.js', () => ({ useEditorSettings: vi.fn(() => ({ isEditorDialogOpen: false, openEditorDialog: vi.fn(), @@ -106,7 +106,7 @@ vi.mock('../../hooks/useEditorSettings.js', () => ({ })), })); -vi.mock('../../hooks/useProviderDialog.js', () => ({ +vi.mock('../hooks/useProviderDialog.js', () => ({ useProviderDialog: vi.fn(() => ({ showDialog: false, openDialog: vi.fn(), @@ -117,7 +117,7 @@ vi.mock('../../hooks/useProviderDialog.js', () => ({ })), })); -vi.mock('../../hooks/useLoadProfileDialog.js', () => ({ +vi.mock('../hooks/useLoadProfileDialog.js', () => ({ useLoadProfileDialog: vi.fn(() => ({ showDialog: false, openDialog: vi.fn(), @@ -127,7 +127,7 @@ vi.mock('../../hooks/useLoadProfileDialog.js', () => ({ })), })); -vi.mock('../../hooks/useCreateProfileDialog.js', () => ({ +vi.mock('../hooks/useCreateProfileDialog.js', () => ({ useCreateProfileDialog: vi.fn(() => ({ showDialog: false, openDialog: vi.fn(), @@ -136,7 +136,7 @@ vi.mock('../../hooks/useCreateProfileDialog.js', () => ({ })), })); -vi.mock('../../hooks/useProfileManagement.js', () => ({ +vi.mock('../hooks/useProfileManagement.js', () => ({ useProfileManagement: vi.fn(() => ({ showListDialog: false, showDetailDialog: false, @@ -161,7 +161,7 @@ vi.mock('../../hooks/useProfileManagement.js', () => ({ })), })); -vi.mock('../../hooks/useToolsDialog.js', () => ({ +vi.mock('../hooks/useToolsDialog.js', () => ({ useToolsDialog: vi.fn(() => ({ showDialog: false, openDialog: vi.fn(), @@ -173,7 +173,7 @@ vi.mock('../../hooks/useToolsDialog.js', () => ({ })), })); -vi.mock('../../hooks/useWelcomeOnboarding.js', () => ({ +vi.mock('../hooks/useWelcomeOnboarding.js', () => ({ useWelcomeOnboarding: vi.fn(() => ({ showWelcome: false, state: { step: 'provider' }, @@ -187,7 +187,7 @@ vi.mock('../../hooks/useWelcomeOnboarding.js', () => ({ })), })); -vi.mock('../../hooks/useExtensionUpdates.js', () => ({ +vi.mock('../hooks/useExtensionUpdates.js', () => ({ useExtensionUpdates: vi.fn(() => ({ extensionsUpdateState: new Map(), dispatchExtensionStateUpdate: vi.fn(), @@ -196,7 +196,7 @@ vi.mock('../../hooks/useExtensionUpdates.js', () => ({ })), })); -vi.mock('../../hooks/useWorkspaceMigration.js', () => ({ +vi.mock('../hooks/useWorkspaceMigration.js', () => ({ useWorkspaceMigration: vi.fn(() => ({ showWorkspaceMigrationDialog: false, workspaceLlxprtExtensions: [], @@ -205,54 +205,54 @@ vi.mock('../../hooks/useWorkspaceMigration.js', () => ({ })), })); -vi.mock('../../hooks/useHookDisplayState.js', () => ({ +vi.mock('../hooks/useHookDisplayState.js', () => ({ useHookDisplayState: vi.fn(() => []), })); -vi.mock('../../hooks/useMemoryMonitor.js', () => ({ +vi.mock('../hooks/useMemoryMonitor.js', () => ({ useMemoryMonitor: vi.fn(), })); -vi.mock('../../hooks/useTodoPausePreserver.js', () => ({ +vi.mock('../hooks/useTodoPausePreserver.js', () => ({ shouldClearTodos: vi.fn(() => false), })); -vi.mock('../../hooks/useAutoAcceptIndicator.js', () => ({ +vi.mock('../hooks/useAutoAcceptIndicator.js', () => ({ useAutoAcceptIndicator: vi.fn(() => false), })); -vi.mock('../../hooks/useExtensionAutoUpdate.js', () => ({ +vi.mock('../hooks/useExtensionAutoUpdate.js', () => ({ useExtensionAutoUpdate: vi.fn(), })); -vi.mock('../../hooks/useStaticHistoryRefresh.js', () => ({ +vi.mock('../hooks/useStaticHistoryRefresh.js', () => ({ useStaticHistoryRefresh: vi.fn(), })); -vi.mock('../../hooks/useBracketedPaste.js', () => ({ +vi.mock('../hooks/useBracketedPaste.js', () => ({ useBracketedPaste: vi.fn(), })); -vi.mock('../../hooks/useResponsive.js', () => ({ +vi.mock('../hooks/useResponsive.js', () => ({ useResponsive: vi.fn(() => ({ isNarrow: false })), })); -vi.mock('../../hooks/useTerminalSize.js', () => ({ +vi.mock('../hooks/useTerminalSize.js', () => ({ useTerminalSize: vi.fn(() => ({ rows: 24, columns: 120 })), })); -vi.mock('../../hooks/useGitBranchName.js', () => ({ +vi.mock('../hooks/useGitBranchName.js', () => ({ useGitBranchName: vi.fn(() => null), })); -vi.mock('../../hooks/useLoadingIndicator.js', () => ({ +vi.mock('../hooks/useLoadingIndicator.js', () => ({ useLoadingIndicator: vi.fn(() => ({ elapsedTime: 0, currentLoadingPhrase: 'Thinking...', })), })); -vi.mock('../../hooks/slashCommandProcessor.js', () => ({ +vi.mock('../hooks/slashCommandProcessor.js', () => ({ useSlashCommandProcessor: vi.fn(() => ({ handleSlashCommand: vi.fn(), slashCommands: [], @@ -262,33 +262,33 @@ vi.mock('../../hooks/slashCommandProcessor.js', () => ({ })), })); -vi.mock('../../hooks/useVim.js', () => ({ +vi.mock('../hooks/vim.js', () => ({ useVim: vi.fn(() => ({ handleInput: vi.fn() })), })); -vi.mock('../../hooks/useFlickerDetector.js', () => ({ +vi.mock('../hooks/useFlickerDetector.js', () => ({ useFlickerDetector: vi.fn(), })); -vi.mock('../../hooks/useMouseSelection.js', () => ({ +vi.mock('../hooks/useMouseSelection.js', () => ({ useMouseSelection: vi.fn(), })); -vi.mock('../../contexts/SessionContext.js', () => ({ +vi.mock('../contexts/SessionContext.js', () => ({ useSessionStats: vi.fn(() => ({ stats: { historyTokenCount: 0 }, updateHistoryTokenCount: vi.fn(), })), })); -vi.mock('../../contexts/TodoContext.js', () => ({ +vi.mock('../contexts/TodoContext.js', () => ({ useTodoContext: vi.fn(() => ({ todos: [], updateTodos: vi.fn(), })), })); -vi.mock('../../contexts/VimModeContext.js', () => ({ +vi.mock('../contexts/VimModeContext.js', () => ({ useVimMode: vi.fn(() => ({ vimEnabled: false, vimMode: 'normal', @@ -296,7 +296,7 @@ vi.mock('../../contexts/VimModeContext.js', () => ({ })), })); -vi.mock('../../contexts/RuntimeContext.js', () => ({ +vi.mock('../contexts/RuntimeContext.js', () => ({ useRuntimeApi: vi.fn(() => ({ getCliOAuthManager: vi.fn(), getActiveModelName: vi.fn(() => 'test-model'), @@ -305,7 +305,7 @@ vi.mock('../../contexts/RuntimeContext.js', () => ({ })), })); -vi.mock('../../utils/mouse.js', () => ({ +vi.mock('../utils/mouse.js', () => ({ isMouseEventsActive: vi.fn(() => false), setMouseEventsActive: vi.fn(), disableMouseEvents: vi.fn(), @@ -326,8 +326,8 @@ vi.mock('../../config/config.js', () => ({ }), })); -vi.mock('../../../config/settings.js', async () => { - const actual = await vi.importActual('../../../config/settings.js'); +vi.mock('../../config/settings.js', async () => { + const actual = await vi.importActual('../../config/settings.js'); return { ...actual, SettingScope: { User: 'user', Workspace: 'workspace', System: 'system' }, diff --git a/packages/cli/src/ui/__tests__/AppContainer.render-budget.test.tsx b/packages/cli/src/ui/__tests__/AppContainer.render-budget.test.tsx index ccc884ed6b..958821ab01 100644 --- a/packages/cli/src/ui/__tests__/AppContainer.render-budget.test.tsx +++ b/packages/cli/src/ui/__tests__/AppContainer.render-budget.test.tsx @@ -323,7 +323,7 @@ vi.mock('../hooks/slashCommandProcessor.js', () => ({ })), })); -vi.mock('../hooks/useVim.js', () => ({ +vi.mock('../hooks/vim.js', () => ({ useVim: vi.fn(() => ({ handleInput: vi.fn() })), })); diff --git a/packages/cli/src/ui/__tests__/integrationWiring.spec.tsx b/packages/cli/src/ui/__tests__/integrationWiring.spec.tsx index a253b7f627..a7042ff7e7 100644 --- a/packages/cli/src/ui/__tests__/integrationWiring.spec.tsx +++ b/packages/cli/src/ui/__tests__/integrationWiring.spec.tsx @@ -21,9 +21,9 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; -// Unmock ink to use real Ink with ink-testing-library -// The global mock in test-setup.ts conflicts with ink-testing-library's reconciler -vi.unmock('ink'); +// ink is not globally mocked under Bun (each test file runs in its own +// process), so vi.unmock('ink') from the Vitest era is unnecessary and +// unsupported by Bun's test runner. import { render } from 'ink-testing-library'; import { Box, Text } from 'ink'; diff --git a/packages/cli/src/ui/commands/test/subagentCommand.test.ts b/packages/cli/src/ui/commands/test/subagentCommand.test.ts index 6c623f61c9..6d1d0b3410 100644 --- a/packages/cli/src/ui/commands/test/subagentCommand.test.ts +++ b/packages/cli/src/ui/commands/test/subagentCommand.test.ts @@ -13,7 +13,7 @@ const getRuntimeBridgeMock = vi.fn(() => ({ runWithScope: runWithScopeMock, })); -vi.mock('../contexts/RuntimeContext.js', () => ({ +vi.mock('../../contexts/RuntimeContext.js', () => ({ getRuntimeBridge: getRuntimeBridgeMock, })); @@ -22,13 +22,17 @@ let generateAutoPromptOverride: ((...args: unknown[]) => unknown) | null = null; vi.mock('../../utils/autoPromptGenerator.js', async (importOriginal) => { const actual = await importOriginal(); + // Capture the real function before mock.module patches the namespace + // in place — otherwise actual.generateAutoPrompt becomes the override + // itself, causing infinite recursion. + const realGenerateAutoPrompt = actual.generateAutoPrompt; return { ...actual, generateAutoPrompt: (...args: unknown[]) => { if (generateAutoPromptOverride) { return generateAutoPromptOverride(...args); } - return actual.generateAutoPrompt( + return realGenerateAutoPrompt( ...(args as Parameters), ); }, @@ -75,10 +79,8 @@ const findSubCommand = (name: string) => subagentCommand.subCommands!.find((cmd) => cmd.name === name)!; const loadSubagentCommandModule = async () => { - // Reset modules to ensure fresh import with mocks - vi.resetModules(); - - // Import module — vi.resetModules() above ensures a fresh evaluation + // Import module — each test file runs in its own Bun process, so the + // module registry is already fresh with the mocks registered above. const mod = await import('../subagentCommand.js'); subagentCommand = mod.subagentCommand; }; diff --git a/packages/cli/src/ui/components/AnsiOutput.test.tsx b/packages/cli/src/ui/components/AnsiOutput.test.tsx index 3914871b20..7fb5de11be 100644 --- a/packages/cli/src/ui/components/AnsiOutput.test.tsx +++ b/packages/cli/src/ui/components/AnsiOutput.test.tsx @@ -70,7 +70,7 @@ describe('', () => { expect(output).toBeDefined(); const lines = output!.split('\n'); expect(lines[0]).toBe('First line'); - expect(lines[1]).toBe('Third line'); + expect(lines[2]).toBe('Third line'); }); it('respects the availableTerminalHeight prop and slices the lines correctly', () => { diff --git a/packages/cli/src/ui/components/AuthDialog.test.tsx b/packages/cli/src/ui/components/AuthDialog.test.tsx index 62dd7479b0..99df985965 100644 --- a/packages/cli/src/ui/components/AuthDialog.test.tsx +++ b/packages/cli/src/ui/components/AuthDialog.test.tsx @@ -22,14 +22,6 @@ vi.mock('../contexts/RuntimeContext.js', () => ({ }), })); -vi.mock('../../providers/providerManagerInstance.js', () => ({ - getOAuthManager: () => ({ - authenticate: mockAuthenticate, - getAuthStatus: mockGetAuthStatus, - toggleOAuthEnabled: mockToggleOAuthEnabled, - }), -})); - import { AuthDialog } from './AuthDialog.js'; describe('AuthDialog', () => { @@ -124,20 +116,20 @@ describe('AuthDialog', () => { // OAuth-only dialog shows regardless of API key presence expect(lastFrame()).toContain('OAuth Authentication'); - expect(lastFrame()).toContain('Gemini (Google OAuth)'); + expect(lastFrame()).toContain('Claude Code (Claude.ai OAuth)'); }); it('should display authentication status for each provider', async () => { mockGetAuthStatus.mockResolvedValue([ { - provider: 'gemini', + provider: 'claudecode', authenticated: true, method: 'oauth', expiresIn: 3600, oauthEnabled: true, }, { - provider: 'anthropic', + provider: 'codex', authenticated: true, method: 'oauth', oauthEnabled: true, @@ -161,8 +153,8 @@ describe('AuthDialog', () => { ui: { customThemes: {} }, mcpServers: {}, oauthEnabledProviders: { - gemini: true, - anthropic: true, + claudecode: true, + codex: true, }, }, path: '', @@ -180,8 +172,10 @@ describe('AuthDialog', () => { await wait(); const frame = lastFrame(); - expect(frame).toContain('Gemini (Google OAuth) [ON] (Authenticated)'); - expect(frame).toContain('Anthropic Claude (OAuth) [ON] (Authenticated)'); + expect(frame).toContain( + 'Claude Code (Claude.ai OAuth) [ON] (Authenticated)', + ); + expect(frame).toContain('Codex (ChatGPT OAuth) [ON] (Authenticated)'); }); }); @@ -210,7 +204,7 @@ describe('AuthDialog', () => { mockGetAuthStatus .mockResolvedValueOnce([]) .mockResolvedValueOnce([ - { provider: 'gemini', authenticated: false, oauthEnabled: true }, + { provider: 'claudecode', authenticated: false, oauthEnabled: true }, ]); const { lastFrame, stdin, unmount } = renderWithProviders( @@ -221,7 +215,7 @@ describe('AuthDialog', () => { stdin.write('1'); await wait(); - expect(mockToggleOAuthEnabled).toHaveBeenCalledWith('gemini'); + expect(mockToggleOAuthEnabled).toHaveBeenCalledWith('claudecode'); expect(mockAuthenticate).not.toHaveBeenCalled(); expect(onSelect).not.toHaveBeenCalled(); expect(lastFrame()).toContain('[ON]'); @@ -244,7 +238,7 @@ describe('AuthDialog', () => { settings: { ui: { customThemes: {} }, mcpServers: {}, - oauthEnabledProviders: { gemini: true }, + oauthEnabledProviders: { claudecode: true }, }, path: '', }, @@ -266,7 +260,7 @@ describe('AuthDialog', () => { stdin.write('1'); await wait(); - expect(mockToggleOAuthEnabled).toHaveBeenCalledWith('gemini'); + expect(mockToggleOAuthEnabled).toHaveBeenCalledWith('claudecode'); expect(mockAuthenticate).not.toHaveBeenCalled(); expect(onSelect).not.toHaveBeenCalled(); @@ -306,11 +300,11 @@ describe('AuthDialog', () => { stdin.write('1'); await wait(); - expect(mockToggleOAuthEnabled).toHaveBeenCalledWith('gemini'); + expect(mockToggleOAuthEnabled).toHaveBeenCalledWith('claudecode'); expect(onSelect).not.toHaveBeenCalled(); const frame = lastFrame(); - expect(frame).toContain('Failed to toggle OAuth for gemini'); + expect(frame).toContain('Failed to toggle OAuth for claudecode'); unmount(); }); @@ -350,7 +344,7 @@ describe('AuthDialog', () => { expect(lastFrame()).toContain('Initial error'); - stdin.write('4'); + stdin.write('3'); await wait(); expect(onSelect).toHaveBeenCalledWith(undefined, 'User'); unmount(); @@ -386,7 +380,7 @@ describe('AuthDialog', () => { ); await wait(); - stdin.write('4'); + stdin.write('3'); await wait(); expect(onSelect).toHaveBeenCalledWith(undefined, SettingScope.User); unmount(); diff --git a/packages/cli/src/ui/components/AuthDialog.theme.test.tsx b/packages/cli/src/ui/components/AuthDialog.theme.test.tsx index f4beb1422c..c3bee2f7e4 100644 --- a/packages/cli/src/ui/components/AuthDialog.theme.test.tsx +++ b/packages/cli/src/ui/components/AuthDialog.theme.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { renderWithProviders } from '../../test-utils/render.js'; +import { render } from 'ink-testing-library'; import { LoadedSettings } from '../../config/settings.js'; import type { BoxProps } from 'ink'; @@ -44,7 +44,7 @@ describe('AuthDialog theming', () => { true, ); - renderWithProviders(); + render(); const themedBox = recordedBoxProps.find( (entry) => entry.backgroundColor === Colors.Background, diff --git a/packages/cli/src/ui/components/Footer.responsive.test.tsx b/packages/cli/src/ui/components/Footer.responsive.test.tsx index cc7e3589f0..2967155d0b 100644 --- a/packages/cli/src/ui/components/Footer.responsive.test.tsx +++ b/packages/cli/src/ui/components/Footer.responsive.test.tsx @@ -19,11 +19,6 @@ import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { testRegex } from '../../test-utils/regex.js'; vi.mock('../hooks/useTerminalSize.js'); -vi.mock('../../providers/providerManagerInstance.js', () => ({ - getProviderManager: () => ({ - getActiveProvider: () => ({ name: 'openai' }), - }), -})); vi.mock('../contexts/RuntimeContext.js', () => ({ useRuntimeApi: () => ({ diff --git a/packages/cli/src/ui/components/Footer.test.tsx b/packages/cli/src/ui/components/Footer.test.tsx index 54110d7094..15c12d6b18 100644 --- a/packages/cli/src/ui/components/Footer.test.tsx +++ b/packages/cli/src/ui/components/Footer.test.tsx @@ -19,21 +19,25 @@ vi.mock('../utils/responsive.js', () => ({ ), })); -vi.mock('../../providers/providerManagerInstance.js', () => ({ - getProviderManager: vi.fn(() => ({ - getActiveProvider: vi.fn(() => ({ name: 'gemini' })), - })), -})); - vi.mock('node:process', async (importOriginal) => { const actual = await importOriginal(); + // Under Bun, require('node:process') returns the process namespace + // directly (no .default wrapper), so normalize the shape. + const actualDefault = + (actual as { default?: typeof process }).default ?? actual; return { ...actual, default: { - ...actual.default, - memoryUsage: vi.fn(() => ({ rss: 1024 * 1024 * 1024 })), + ...actualDefault, + memoryUsage: vi.fn(() => ({ + rss: 1024 * 1024 * 1024, + heapUsed: 100 * 1024 * 1024, + heapTotal: 200 * 1024 * 1024, + external: 10 * 1024 * 1024, + arrayBuffers: 5 * 1024 * 1024, + })), env: { - ...actual.default.env, + ...actualDefault.env, SANDBOX: 'test-sandbox', }, }, @@ -48,6 +52,12 @@ vi.mock('node:v8', () => ({ }, })); +vi.mock('../contexts/RuntimeContext.js', () => ({ + useRuntimeApi: () => ({ + getActiveProviderStatus: () => ({ providerName: 'gemini' }), + }), +})); + import { useResponsive } from '../hooks/useResponsive.js'; import { testRegex } from '../../test-utils/regex.js'; @@ -83,18 +93,10 @@ describe('Footer', () => { isWide: false, }); - const { container } = render(