From 45059b927f65bb6613112387882e616c4ab61cdd Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Thu, 20 Aug 2026 12:57:16 +0300 Subject: [PATCH] Run client typecheck before vitest in npm test. Duplicate keys in strings.ts were green under vitest and red only on build. Drop the self file: dependency loop so npm test is enough. --- AGENTS.md | 11 ++- docs/phases/10-craft/57-client-typecheck.md | 16 ++-- src/HSchool.Client/package-lock.json | 7 -- src/HSchool.Client/package.json | 5 +- src/HSchool.Client/scripts/npm-test.test.ts | 81 +++++++++++++++++++++ src/HSchool.Client/scripts/run-test.mjs | 38 ++++++++++ src/HSchool.Client/tsconfig.node.json | 2 +- 7 files changed, 136 insertions(+), 24 deletions(-) create mode 100644 src/HSchool.Client/scripts/npm-test.test.ts create mode 100644 src/HSchool.Client/scripts/run-test.mjs diff --git a/AGENTS.md b/AGENTS.md index 8187ba0..11a44e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,8 +46,9 @@ dotnet test tests/HSchool.People.Tests --filter FullyQualifiedName~YearlyIntake npm --prefix src/HSchool.Client test -- src/ui/peoplePanel.test.ts ``` -Solution-wide `dotnet test` and a full client `npm test` are CI, or when the user asked. Agents -scope the run — Testing policy. +Client `npm test` runs `tsc -b` (`typecheck`) then vitest; extra args after `--` go to vitest. +`test:watch` is vitest only. Solution-wide `dotnet test` and a full client `npm test` are CI, or +when the user asked. Agents scope the run — Testing policy. ```bash npm --prefix src/HSchool.Client run build @@ -126,7 +127,7 @@ say so explicitly in the change description. ## Testing policy **What to run.** One project from the list below, filtered to the new or changed class while -iterating. Client Vitest only if `src/HSchool.Client` changed. `HSchool.AppHost.Tests` only if +iterating. Client `npm test` (typecheck + Vitest) only if `src/HSchool.Client` changed. `HSchool.AppHost.Tests` only if the change is HTTP, WebSocket, or host wiring — it boots a server. Do not `dotnet test` the solution, do not run client and .NET together "to be sure", do not re-run after a merge that only resolved a status line. Solution-wide is CI. @@ -155,7 +156,9 @@ only resolved a status line. Solution-wide is CI. - Screen logic is covered in Vitest under happy-dom (`ui/*.test.ts`): people filters and the pager, the create dialog (core stays on, map reset, submit busy), planner rejection text, and the payroll-cap message. Layout and styles are still verified by running the app. Dictionaries - and date formatting stay in `i18n/strings.test.ts` and `format/gameTime.test.ts`. + and date formatting stay in `i18n/strings.test.ts` and `format/gameTime.test.ts`. A duplicate + key in `strings.ts` is a typecheck failure (`TS1117` / `TS2300`); client `npm test` runs tsc + before vitest, so it is red without `npm run build`. ## Dependencies diff --git a/docs/phases/10-craft/57-client-typecheck.md b/docs/phases/10-craft/57-client-typecheck.md index da9e6dc..5e5a559 100644 --- a/docs/phases/10-craft/57-client-typecheck.md +++ b/docs/phases/10-craft/57-client-typecheck.md @@ -11,20 +11,20 @@ ## Задачи -- [ ] `package.json` → `"test":` сначала `tsc -b` (`typecheck`), потом `vitest run`. Без +- [x] `package.json` → `"test":` сначала `tsc -b` (`typecheck`), потом `vitest run`. Без `&&` скрывать код выхода typecheck -- [ ] Убрать петлю `"hschool-client": "file:"` из `dependencies`; `npm test` и `npm run build` +- [x] Убрать петлю `"hschool-client": "file:"` из `dependencies`; `npm test` и `npm run build` без неё -- [ ] `AGENTS.md` Commands и Testing policy: клиентский `npm test` включает typecheck -- [ ] `test:watch` по-прежнему только vitest — typecheck на каждый файл в watch не нужен +- [x] `AGENTS.md` Commands и Testing policy: клиентский `npm test` включает typecheck +- [x] `test:watch` по-прежнему только vitest — typecheck на каждый файл в watch не нужен ## Тесты, без которых фаза не закрыта -- [ ] `npm --prefix src/HSchool.Client test` гоняет tsc и vitest и проходит на текущем дереве -- [ ] Дубликат ключа в одном объекте `strings.ts` (ru или en) делает `npm test` красным без +- [x] `npm --prefix src/HSchool.Client test` гоняет tsc и vitest и проходит на текущем дереве +- [x] Дубликат ключа в одном объекте `strings.ts` (ru или en) делает `npm test` красным без `npm run build` -- [ ] В `package.json` нет зависимости `hschool-client` -- [ ] `npm --prefix src/HSchool.Client run build` проходит +- [x] В `package.json` нет зависимости `hschool-client` +- [x] `npm --prefix src/HSchool.Client run build` проходит ## Критерий готовности diff --git a/src/HSchool.Client/package-lock.json b/src/HSchool.Client/package-lock.json index 8295d2b..a3a300a 100644 --- a/src/HSchool.Client/package-lock.json +++ b/src/HSchool.Client/package-lock.json @@ -7,9 +7,6 @@ "": { "name": "hschool-client", "version": "0.1.0", - "dependencies": { - "hschool-client": "file:" - }, "devDependencies": { "@types/node": "^24.10.1", "happy-dom": "^20.11.2", @@ -597,10 +594,6 @@ "node": ">=20.0.0" } }, - "node_modules/hschool-client": { - "resolved": "", - "link": true - }, "node_modules/lightningcss": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", diff --git a/src/HSchool.Client/package.json b/src/HSchool.Client/package.json index de127c5..7698635 100644 --- a/src/HSchool.Client/package.json +++ b/src/HSchool.Client/package.json @@ -11,7 +11,7 @@ "build": "tsc -b && vite build", "preview": "vite preview", "typecheck": "tsc -b", - "test": "vitest run", + "test": "node ./scripts/run-test.mjs", "test:watch": "vitest" }, "devDependencies": { @@ -20,8 +20,5 @@ "typescript": "~5.9.3", "vite": "^8.2.1", "vitest": "^4.1.10" - }, - "dependencies": { - "hschool-client": "file:" } } diff --git a/src/HSchool.Client/scripts/npm-test.test.ts b/src/HSchool.Client/scripts/npm-test.test.ts new file mode 100644 index 0000000..15fd606 --- /dev/null +++ b/src/HSchool.Client/scripts/npm-test.test.ts @@ -0,0 +1,81 @@ +import { spawnSync } from 'node:child_process'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const clientRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const require = createRequire(import.meta.url); +const tscBin = require.resolve('typescript/bin/tsc'); + +const pkg = JSON.parse(readFileSync(path.join(clientRoot, 'package.json'), 'utf8')) as { + scripts: Record; + dependencies?: Record; +}; + +describe('npm test script', () => { + it('runs typecheck then vitest, without swallowing tsc', () => { + expect(pkg.scripts.test).toBe('node ./scripts/run-test.mjs'); + const runner = readFileSync(path.join(clientRoot, 'scripts', 'run-test.mjs'), 'utf8'); + expect(runner).toContain("'-b'"); + expect(runner).toContain("'run'"); + expect(runner).toMatch(/if \(typecheck !== 0\)/); + expect(pkg.scripts.typecheck).toBe('tsc -b'); + }); + + it('keeps watch as vitest only', () => { + expect(pkg.scripts['test:watch']).toBe('vitest'); + expect(pkg.scripts['test:watch']).not.toMatch(/typecheck|tsc/); + }); + + it('does not depend on the hschool-client file: loop', () => { + expect(pkg.dependencies?.['hschool-client']).toBeUndefined(); + }); +}); + +describe('duplicate dictionary keys', () => { + it('makes tsc fail on a strings.ts ru object without vite build', () => { + const dir = mkdtempSync(path.join(tmpdir(), 'hschool-dup-')); + try { + const original = readFileSync(path.join(clientRoot, 'src', 'i18n', 'strings.ts'), 'utf8'); + const needle = " createSchool: 'Создать школу',"; + expect(original).toContain(needle); + const duplicate = original.replace( + needle, + `${needle}\n createSchool: 'дубликат',`, + ); + writeFileSync(path.join(dir, 'strings.ts'), duplicate); + writeFileSync( + path.join(dir, 'locale.ts'), + readFileSync(path.join(clientRoot, 'src', 'i18n', 'locale.ts'), 'utf8'), + ); + writeFileSync( + path.join(dir, 'tsconfig.json'), + JSON.stringify({ + compilerOptions: { + strict: true, + noEmit: true, + target: 'ES2022', + module: 'ESNext', + moduleResolution: 'bundler', + allowImportingTsExtensions: true, + verbatimModuleSyntax: true, + lib: ['ES2022', 'DOM'], + }, + include: ['strings.ts', 'locale.ts'], + }), + ); + + const result = spawnSync(process.execPath, [tscBin, '-p', dir], { + encoding: 'utf8', + }); + const output = `${result.stdout ?? ''}${result.stderr ?? ''}`; + expect(result.status).not.toBe(0); + expect(output).toMatch(/TS1117|TS2300|TS1118/); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); diff --git a/src/HSchool.Client/scripts/run-test.mjs b/src/HSchool.Client/scripts/run-test.mjs new file mode 100644 index 0000000..3c453db --- /dev/null +++ b/src/HSchool.Client/scripts/run-test.mjs @@ -0,0 +1,38 @@ +// `tsc -b && vitest run` on Windows npm (tsc.cmd) can drop the typecheck exit code. +import { spawnSync } from 'node:child_process'; +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const clientRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const require = createRequire(import.meta.url); +const win = process.platform === 'win32'; + +function statusOf(result) { + if (result.error) { + console.error(result.error); + return 1; + } + return result.status ?? 1; +} + +const typecheck = statusOf( + spawnSync(process.execPath, [require.resolve('typescript/bin/tsc'), '-b'], { + cwd: clientRoot, + stdio: 'inherit', + }), +); +if (typecheck !== 0) { + process.exit(typecheck); +} + +const vitest = path.join(clientRoot, 'node_modules', '.bin', win ? 'vitest.cmd' : 'vitest'); +process.exit( + statusOf( + spawnSync(vitest, ['run', ...process.argv.slice(2)], { + cwd: clientRoot, + stdio: 'inherit', + shell: win, + }), + ), +); diff --git a/src/HSchool.Client/tsconfig.node.json b/src/HSchool.Client/tsconfig.node.json index 0244509..dbd52da 100644 --- a/src/HSchool.Client/tsconfig.node.json +++ b/src/HSchool.Client/tsconfig.node.json @@ -20,5 +20,5 @@ "noUncheckedSideEffectImports": true, "types": ["node"] }, - "include": ["vite.config.ts"] + "include": ["vite.config.ts", "scripts/**/*.ts"] }