diff --git a/AGENTS.md b/AGENTS.md index a7b782b8..5f6dd064 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,7 @@ - 类型检查:Node/CLI/Electron 主进程相关改动运行 `pnpm run type-check:node`;前端/Vue 相关改动运行 `pnpm run type-check:web`;跨端或发布前改动运行 `pnpm run type-check:all` - Lint:优先对修改文件运行 `pnpm exec eslint `;需要全量修复时再运行 `pnpm lint` - Format:优先对修改文件运行 `pnpm exec prettier --write `;大范围格式化才运行 `pnpm format` -- 单元/集成测试:当前没有统一全量 `test` 脚本,优先运行相关测试文件,例如 `pnpm exec tsx --test path/to/file.test.ts` +- 单元/集成测试:日常默认运行 `pnpm test` 或 `pnpm run test:unit`;优先运行相关测试文件时用 `pnpm test -- path/to/file.test.ts` - 文档:修改公开文档或 VitePress 配置后运行 `pnpm docs:build`;只改 `.docs/` 私有任务文档时不需要构建公开文档站 - E2E/Smoke:`pnpm run test:e2e:launcher`、`pnpm run test:e2e:smoke` 和真实 LLM/真实 Electron 测试只在相关功能需要时运行,默认不作为每次改动的必跑项 - 最后检查:提交前运行 `git diff --check`,确认没有空白错误 diff --git a/apps/desktop/main/paths.ts b/apps/desktop/main/paths.ts index 2c69ed33..4168ffc3 100644 --- a/apps/desktop/main/paths.ts +++ b/apps/desktop/main/paths.ts @@ -35,6 +35,7 @@ import { isSubPath, writeMigrationLog, } from './utils/pathUtils' +import { shouldMarkUnifiedDirMigrationDone } from './utils/unifiedDirMigration' import { createPendingDataDirMigration, isDirectoryEmptyOrMissing, @@ -568,10 +569,6 @@ export function removeLegacyDir(): boolean { const SYSTEM_SUBDIRS = ['ai', 'settings', 'cache', 'logs', 'temp', 'nlp'] -export function shouldMarkUnifiedDirMigrationDone(failedDirs: string[]): boolean { - return failedDirs.length === 0 -} - /** * 检测是否需要从 Electron 旧目录结构迁移到新的双根目录结构 * diff --git a/apps/desktop/main/paths.test.ts b/apps/desktop/main/utils/unifiedDirMigration.test.ts similarity index 53% rename from apps/desktop/main/paths.test.ts rename to apps/desktop/main/utils/unifiedDirMigration.test.ts index 46cb93bd..da71cd82 100644 --- a/apps/desktop/main/paths.test.ts +++ b/apps/desktop/main/utils/unifiedDirMigration.test.ts @@ -1,9 +1,9 @@ import { describe, it } from 'node:test' import assert from 'node:assert/strict' -import { shouldMarkUnifiedDirMigrationDone } from './paths' +import { shouldMarkUnifiedDirMigrationDone } from './unifiedDirMigration' -describe('desktop path migration', () => { - it('marks unified directory migration done only when no directory failed', () => { +describe('desktop unified directory migration', () => { + it('marks migration done only when no directory failed', () => { assert.equal(shouldMarkUnifiedDirMigrationDone([]), true) assert.equal(shouldMarkUnifiedDirMigrationDone(['settings']), false) }) diff --git a/apps/desktop/main/utils/unifiedDirMigration.ts b/apps/desktop/main/utils/unifiedDirMigration.ts new file mode 100644 index 00000000..91274fd8 --- /dev/null +++ b/apps/desktop/main/utils/unifiedDirMigration.ts @@ -0,0 +1,3 @@ +export function shouldMarkUnifiedDirMigrationDone(failedDirs: string[]): boolean { + return failedDirs.length === 0 +} diff --git a/package.json b/package.json index af2a9bcb..e2866af5 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,8 @@ "type-check:node": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p apps/desktop/tsconfig.node.json", "type-check:all": "pnpm run type-check:web && pnpm run type-check:node", "type-check": "vue-tsc --noEmit -p tsconfig.web.json", + "test": "node scripts/run-tests.mjs", + "test:unit": "node scripts/run-tests.mjs", "test:auth": "tsx --test apps/cli/src/http/auth.test.ts src/services/utils/http.test.ts", "test:agent-context": "node --experimental-strip-types --test apps/desktop/main/ai/context/sessionLog.test.mjs", "test:e2e:launcher": "node --test tests/e2e/helpers/app-launcher.test.js", diff --git a/scripts/run-tests.mjs b/scripts/run-tests.mjs new file mode 100644 index 00000000..b26eed13 --- /dev/null +++ b/scripts/run-tests.mjs @@ -0,0 +1,93 @@ +#!/usr/bin/env node + +import { readdirSync, statSync } from 'node:fs' +import { join, relative, sep } from 'node:path' +import { spawnSync } from 'node:child_process' +import { fileURLToPath } from 'node:url' + +const SKIP_DIRS = new Set([ + '.git', + '.docs', + 'node_modules', + 'dist', + 'dist-web', + 'out', + 'build', + 'coverage', + '.vitepress', +]) + +const TEST_FILE_RE = /\.(?:test|spec)\.(?:ts|tsx|js|jsx|mjs|mts|cjs|cts)$/ + +function normalizePath(filePath) { + return filePath.split(sep).join('/') +} + +export function filterDefaultTestFiles(files) { + return files + .map(normalizePath) + .filter((file) => TEST_FILE_RE.test(file)) + .filter((file) => !file.startsWith('tests/e2e/')) + .filter((file) => !file.includes('/smoke/')) + .filter((file) => !file.includes('.smoke.test.')) + .filter((file) => !file.includes('.e2e.test.')) +} + +function collectFiles(rootDir, dir = rootDir) { + const entries = readdirSync(dir, { withFileTypes: true }) + const files = [] + + for (const entry of entries) { + if (entry.isDirectory()) { + if (SKIP_DIRS.has(entry.name)) continue + files.push(...collectFiles(rootDir, join(dir, entry.name))) + continue + } + + if (!entry.isFile()) continue + + const filePath = join(dir, entry.name) + files.push(normalizePath(relative(rootDir, filePath))) + } + + return files +} + +export function collectDefaultTestFiles(rootDir = process.cwd()) { + if (!statSync(rootDir).isDirectory()) { + throw new Error(`Test root is not a directory: ${rootDir}`) + } + return filterDefaultTestFiles(collectFiles(rootDir)).sort() +} + +export function buildNodeTestArgs(testArgs) { + return ['--import', 'tsx', '--test', ...testArgs] +} + +function run() { + const explicitArgs = process.argv.slice(2) + const testArgs = explicitArgs.length > 0 ? explicitArgs : collectDefaultTestFiles() + + if (testArgs.length === 0) { + console.error('No test files found.') + process.exit(1) + } + + if (explicitArgs.length === 0) { + console.log(`Running ${testArgs.length} default test files.`) + } + + const result = spawnSync(process.execPath, buildNodeTestArgs(testArgs), { + stdio: 'inherit', + }) + + if (result.error) { + throw result.error + } + + process.exit(result.status ?? 1) +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + run() +} diff --git a/scripts/run-tests.test.mjs b/scripts/run-tests.test.mjs new file mode 100644 index 00000000..2847ab01 --- /dev/null +++ b/scripts/run-tests.test.mjs @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { buildNodeTestArgs, filterDefaultTestFiles } from './run-tests.mjs' + +test('default test collection excludes e2e, smoke, and real external tests', () => { + const files = [ + 'apps/cli/src/ai/chat-command.test.ts', + 'tests/chart-runtime/agent-chart-flow.test.mts', + 'tests/chart-runtime/render-chart.integration.test.ts', + 'tests/e2e/helpers/app-launcher.test.js', + 'tests/e2e/smoke/chart-runtime.smoke.test.js', + 'tests/chart-runtime/real-llm-chart-flow.e2e.test.ts', + 'tests/e2e/helpers/app-launcher.js', + ] + + assert.deepEqual(filterDefaultTestFiles(files), [ + 'apps/cli/src/ai/chat-command.test.ts', + 'tests/chart-runtime/agent-chart-flow.test.mts', + 'tests/chart-runtime/render-chart.integration.test.ts', + ]) +}) + +test('explicit test arguments are passed through without default exclusions', () => { + assert.deepEqual(buildNodeTestArgs(['tests/e2e/helpers/app-launcher.test.js']), [ + '--import', + 'tsx', + '--test', + 'tests/e2e/helpers/app-launcher.test.js', + ]) +}) diff --git a/src/pages/home/onboardingFlow.test.mjs b/src/pages/home/onboardingFlow.test.ts similarity index 88% rename from src/pages/home/onboardingFlow.test.mjs rename to src/pages/home/onboardingFlow.test.ts index bf3b0e05..eb00d307 100644 --- a/src/pages/home/onboardingFlow.test.mjs +++ b/src/pages/home/onboardingFlow.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { resolveLanguageBootstrap } from './onboardingFlow.ts' +import { resolveLanguageBootstrap } from './onboardingFlow' test('新用户没有保存语言时应先打开语言选择,不继续后续弹窗流程', () => { assert.deepEqual(resolveLanguageBootstrap(''), {