From c82dae3e3a3b3fe7687c70a54c07617aafb020f8 Mon Sep 17 00:00:00 2001 From: Kefei Qian Date: Tue, 14 Jul 2026 17:10:27 +0800 Subject: [PATCH] fix(tui): make CI tests portable Use runner-local home paths in cwd rendering tests and set simulated packaged-backend race winners to user-only permissions so Linux CI exercises the intended reusable-cache path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4c127c9c-ac00-4a90-be0f-4357720a6a8d --- tui/src/__tests__/App.test.tsx | 7 +++- .../__tests__/components/HomeScreen.test.tsx | 40 +++++++++++-------- .../__tests__/materializeBackend.test.ts | 6 +++ 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/tui/src/__tests__/App.test.tsx b/tui/src/__tests__/App.test.tsx index 5d98256e..183b3946 100644 --- a/tui/src/__tests__/App.test.tsx +++ b/tui/src/__tests__/App.test.tsx @@ -1,4 +1,6 @@ import { createStore } from 'jotai'; +import os from 'node:os'; +import path from 'node:path'; import { describe, expect, it, vi } from 'vitest'; import { App } from '@/App.tsx'; import { @@ -13,7 +15,8 @@ import { helpVisibleAtom } from '@state/ui/help/index.ts'; import { flushInput } from '@test/flushInput.ts'; import { renderWithJotai } from '@test/renderWithJotai.tsx'; -const workspaceCwd = 'C:\\Users\\kefeiqian\\Projects\\dummy-react-app'; +const workspaceCwd = path.join(os.homedir(), 'Projects', 'dummy-react-app'); +const displayCwd = `~${path.sep}${path.join('Projects', 'dummy-react-app')}`; function renderApp({ columns, rows }: { columns?: number; rows?: number } = {}) { const store = createStore(); @@ -58,7 +61,7 @@ describe('App', () => { expect(output).toContain('KQode'); expect(output).toContain('v0.1.0'); - expect(output).toContain('~\\Projects\\dummy-react-app'); + expect(output).toContain(displayCwd); expect(output).not.toContain('Preview mode: local Rust backend only'); }); diff --git a/tui/src/__tests__/components/HomeScreen.test.tsx b/tui/src/__tests__/components/HomeScreen.test.tsx index 55567f40..b8ef6f6a 100644 --- a/tui/src/__tests__/components/HomeScreen.test.tsx +++ b/tui/src/__tests__/components/HomeScreen.test.tsx @@ -1,5 +1,7 @@ import { render } from 'ink-testing-library'; import { createStore } from 'jotai'; +import os from 'node:os'; +import path from 'node:path'; import { describe, expect, it } from 'vitest'; import { App } from '@/App.tsx'; import { BodyPane } from '@components/BodyPane.tsx'; @@ -17,7 +19,9 @@ import { flushInput } from '@test/flushInput.ts'; import { renderWithJotai } from '@test/renderWithJotai.tsx'; import { theme } from '@theme/themeConfig.ts'; -const workspaceCwd = 'C:\\Users\\kefeiqian\\Projects\\KQode'; +const workspaceCwd = path.join(os.homedir(), 'Projects', 'KQode'); +const displayCwd = `~${path.sep}${path.join('Projects', 'KQode')}`; +const projectsKQode = path.join('Projects', 'KQode'); type RenderHomeScreenOptions = { productVersion?: string; @@ -65,7 +69,7 @@ describe('HomeScreen', () => { expect(output).not.toContain('Preview mode: local Rust backend only'); expect(output).not.toContain('uses AI'); expect(output).not.toContain('Check for mistakes'); - expect(output).toContain('~\\Projects\\KQode'); + expect(output).toContain(displayCwd); expect(output.split('\n')).toContain('>'); expect(output).not.toContain('Ask KQode...'); expect(output).toContain('/ commands'); @@ -76,15 +80,14 @@ describe('HomeScreen', () => { }); it('displays the copied dummy React workspace cwd rather than the TUI package path', () => { - const copiedWorkspace = - 'C:\\Users\\kefeiqian\\Projects\\KQode\\target\\kqode-test-workspaces\\workspace'; + const copiedWorkspace = path.join(workspaceCwd, 'target', 'kqode-test-workspaces', 'workspace'); const { lastFrame } = renderHomeScreen({ workspaceCwd: copiedWorkspace, columns: 120, rows: 20 }); const output = lastFrame() ?? ''; - expect(output).toContain('~\\Projects\\KQode\\target\\kqode-test-workspaces\\workspace'); - expect(output).toContain('target\\kqode-test-workspaces\\workspace'); - expect(output).not.toContain('~\\Projects\\KQode\\tui'); + expect(output).toContain(path.join(displayCwd, 'target', 'kqode-test-workspaces', 'workspace')); + expect(output).toContain(path.join('target', 'kqode-test-workspaces', 'workspace')); + expect(output).not.toContain(path.join(displayCwd, 'tui')); }); it('centralizes Dracula theme tokens including error red', () => { @@ -146,7 +149,7 @@ describe('HomeScreen', () => { })); const { lastFrame } = renderHomeScreen({ bodyEntries: entries, columns: 80, rows: 16 }); const outputRows = (lastFrame() ?? '').split('\n'); - const cwdRow = outputRows.findIndex((row) => row.includes('Projects\\KQode')); + const cwdRow = outputRows.findIndex((row) => row.includes(projectsKQode)); expect(cwdRow).toBe(11); expect(outputRows.at(cwdRow - 1)).toBe(''); @@ -217,14 +220,19 @@ describe('HomeScreen', () => { const output = lastFrame() ?? ''; - expect(formatDisplayCwd(workspaceCwd, 'C:\\Users\\kefeiqian')).toBe('~\\Projects\\KQode'); - expect(output).toContain('~\\Projects\\KQode [⎇ feat/first-ink-tui-jsonrpc-backend*+%]'); + expect(formatDisplayCwd(workspaceCwd)).toBe(displayCwd); + expect(output).toContain(`${displayCwd} [⎇ feat/first-ink-tui-jsonrpc-backend*+%]`); expect(output).not.toContain('cwd '); }); it('soft-wraps a long cwd without truncating it', () => { - const longWorkspace = - 'C:\\Users\\kefeiqian\\Projects\\KQode\\target\\kqode-test-workspaces\\workspace\\dummy-react-app'; + const longWorkspace = path.join( + workspaceCwd, + 'target', + 'kqode-test-workspaces', + 'workspace', + 'dummy-react-app' + ); const { lastFrame } = renderHomeScreen({ workspaceCwd: longWorkspace, columns: 60, rows: 15 }); const output = lastFrame() ?? ''; @@ -238,7 +246,7 @@ describe('HomeScreen', () => { expect(output).not.toContain('...'); expect(output).not.toContain('Ask KQode...'); expect(flattened).toContain( - '~\\Projects\\KQode\\target\\kqode-test-workspaces\\workspace\\dummy-react-app' + path.join(displayCwd, 'target', 'kqode-test-workspaces', 'workspace', 'dummy-react-app') ); expect(output.split('\n')).toHaveLength(15); expect(output.split('\n').at(-1)).toContain('/ commands | @ mention | ? help'); @@ -251,7 +259,7 @@ describe('HomeScreen', () => { })); const { lastFrame } = renderHomeScreen({ bodyEntries: entries, columns: 80, rows: 15 }); const outputRows = (lastFrame() ?? '').split('\n'); - const cwdRow = outputRows.findIndex((row) => row.includes('Projects\\KQode')); + const cwdRow = outputRows.findIndex((row) => row.includes(projectsKQode)); expect(outputRows.at(cwdRow - 1)).toBe(''); expect(outputRows.at(cwdRow - 2)).toContain('entry 10'); @@ -262,13 +270,13 @@ describe('HomeScreen', () => { it('hides the cwd line while the command palette is open, keeping the status row pinned', async () => { const { lastFrame, stdin } = renderHomeScreen({ columns: 80, rows: 16 }); await flushInput(); - expect(lastFrame() ?? '').toContain('Projects\\KQode'); + expect(lastFrame() ?? '').toContain(projectsKQode); stdin.write('/'); await flushInput(); const openOutput = lastFrame() ?? ''; - expect(openOutput).not.toContain('Projects\\KQode'); + expect(openOutput).not.toContain(projectsKQode); expect(openOutput).toContain('/clear'); expect(openOutput).toContain('/exit'); expect(openOutput).toContain('/help'); diff --git a/tui/src/backend/packaged/__tests__/materializeBackend.test.ts b/tui/src/backend/packaged/__tests__/materializeBackend.test.ts index d2a9e1cf..c1857989 100644 --- a/tui/src/backend/packaged/__tests__/materializeBackend.test.ts +++ b/tui/src/backend/packaged/__tests__/materializeBackend.test.ts @@ -218,6 +218,9 @@ describe('materializePackagedBackend', () => { const losingWrite = (binaryPath: string, runtimeDir: string): void => { fs.mkdirSync(runtimeDir, { recursive: true }); fs.writeFileSync(binaryPath, bytes); + if (!isWindows) { + fs.chmodSync(binaryPath, 0o700); + } throw new Error('EBUSY: binary is locked by the winning instance'); }; @@ -257,6 +260,9 @@ describe('materializePackagedBackend', () => { const write = (binaryPath: string, runtimeDir: string): void => { fs.mkdirSync(runtimeDir, { recursive: true }); fs.writeFileSync(binaryPath, bytes); + if (!isWindows) { + fs.chmodSync(binaryPath, 0o700); + } }; // ...but the immediate post-write read-back observes the atomic-replace gap // once (ENOENT), which must fall back to reusing the valid cache.