chore: simplify backend verification
This commit is contained in:
@@ -21,7 +21,7 @@ npm ci
|
|||||||
npm run verify
|
npm run verify
|
||||||
```
|
```
|
||||||
|
|
||||||
Gate non-mutating и последовательно проверяет runtime, hygiene, ESLint, application/test TypeScript, risk-based Jest suite и build. `npm run lint:fix` и `npm run format` изменяют files и выполняются только явно; `npm run test:cov` — диагностика без общего threshold. `npm run test:e2e` не входит в passing gate до WL-P2-05 trigger из remediation registry.
|
Gate non-mutating и последовательно проверяет ESLint, application/test TypeScript, risk-based Jest suite и build. `npm run lint:fix` и `npm run format` изменяют files и выполняются только явно; `npm run test:cov` — диагностика без общего threshold. `npm run test:e2e` не входит в passing gate до WL-P2-05 trigger из remediation registry. `npm run test:openapi-contract` — отдельный contract gate для изменений обеих сторон API contract.
|
||||||
Актуальные scripts проверять в `package.json`/`npm run`.
|
Актуальные scripts проверять в `package.json`/`npm run`.
|
||||||
|
|
||||||
## Enforceable boundaries
|
## Enforceable boundaries
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ engine останавливает migration до запуска Jest. Точны
|
|||||||
Из workspace runtime OpenAPI можно read-only сверить с frontend snapshot:
|
Из workspace runtime OpenAPI можно read-only сверить с frontend snapshot:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash specs/scripts/verify-runtime-openapi.sh
|
npm run test:openapi-contract
|
||||||
```
|
```
|
||||||
|
|
||||||
Полный multi-repository change проверяется из workspace через
|
Полный multi-repository change проверяется из workspace через
|
||||||
`bash specs/scripts/verify-workspace-change.sh <change>`.
|
native OpenSpec validation and the canonical gates of each affected repository.
|
||||||
|
|||||||
+2
-3
@@ -9,7 +9,6 @@
|
|||||||
"node": "22.x"
|
"node": "22.x"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"runtime:check": "node scripts/verify-node-version.mjs",
|
|
||||||
"build": "prisma generate && nest build",
|
"build": "prisma generate && nest build",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
@@ -19,7 +18,6 @@
|
|||||||
"lint": "npm run lint:check",
|
"lint": "npm run lint:check",
|
||||||
"lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\" --max-warnings=0",
|
"lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\" --max-warnings=0",
|
||||||
"lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"hygiene:check": "node scripts/repository-hygiene-check.mjs",
|
|
||||||
"typecheck:app": "tsc --project tsconfig.typecheck.app.json",
|
"typecheck:app": "tsc --project tsconfig.typecheck.app.json",
|
||||||
"typecheck:test": "tsc --project tsconfig.typecheck.test.json",
|
"typecheck:test": "tsc --project tsconfig.typecheck.test.json",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
@@ -27,7 +25,8 @@
|
|||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:e2e": "DATABASE_URL=file:/tmp/wealth-ledger-e2e.db prisma migrate deploy && NODE_ENV=development NODE_OPTIONS=--experimental-vm-modules jest --config ./test/jest-e2e.json",
|
"test:e2e": "DATABASE_URL=file:/tmp/wealth-ledger-e2e.db prisma migrate deploy && NODE_ENV=development NODE_OPTIONS=--experimental-vm-modules jest --config ./test/jest-e2e.json",
|
||||||
"verify": "npm run runtime:check && npm run hygiene:check && npm run lint && npm run typecheck:app && npm run typecheck:test && npm test -- --runInBand && npm run build"
|
"test:openapi-contract": "NODE_OPTIONS=--experimental-vm-modules jest --config ./test/jest-e2e.json --runInBand openapi-contract.e2e-spec.ts",
|
||||||
|
"verify": "npm run lint && npm run typecheck:app && npm run typecheck:test && npm test -- --runInBand && npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "^1.14.4",
|
"@grpc/grpc-js": "^1.14.4",
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
import { execFileSync } from 'node:child_process';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
import { dirname, resolve } from 'node:path';
|
|
||||||
|
|
||||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
||||||
|
|
||||||
function git(args) {
|
|
||||||
return execFileSync('git', args, { cwd: root, encoding: 'utf8' }).trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function isIgnored(file) {
|
|
||||||
try {
|
|
||||||
execFileSync('git', ['check-ignore', '--no-index', '-q', file], {
|
|
||||||
cwd: root,
|
|
||||||
stdio: 'ignore',
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
if (error.status === 1) return false;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function assert(condition, message) {
|
|
||||||
if (!condition) throw new Error(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const trackedDatabases = git([
|
|
||||||
'ls-files',
|
|
||||||
'--',
|
|
||||||
'dev.db',
|
|
||||||
'dev.db-*',
|
|
||||||
'prisma/*.db',
|
|
||||||
'prisma/*.db-*',
|
|
||||||
])
|
|
||||||
.split('\n')
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
assert(
|
|
||||||
trackedDatabases.length === 0,
|
|
||||||
`Runtime SQLite files in supported locations must not be tracked; found: ${trackedDatabases.join(', ')}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const file of [
|
|
||||||
'dev.db',
|
|
||||||
'dev.db-wal',
|
|
||||||
'dev.db-shm',
|
|
||||||
'prisma/dev.db',
|
|
||||||
'prisma/dev.db-wal',
|
|
||||||
'prisma/dev.db-shm',
|
|
||||||
]) {
|
|
||||||
assert(isIgnored(file), `Runtime SQLite file must be ignored: ${file}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(
|
|
||||||
!isIgnored('test/fixtures/reference.db'),
|
|
||||||
'Intentional fixture must remain versionable',
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('Backend repository hygiene verified');
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { readFileSync } from 'node:fs';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
import { dirname, resolve } from 'node:path';
|
|
||||||
|
|
||||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
||||||
const packageJson = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'));
|
|
||||||
const nvmVersion = readFileSync(resolve(root, '.nvmrc'), 'utf8').trim();
|
|
||||||
const expectedMajor = nvmVersion.replace(/^v/, '').split('.')[0];
|
|
||||||
const expectedEngine = `${expectedMajor}.x`;
|
|
||||||
|
|
||||||
if (packageJson.engines?.node !== expectedEngine) {
|
|
||||||
throw new Error(
|
|
||||||
`Node runtime contracts disagree: .nvmrc=${nvmVersion}, package.json engines.node=${packageJson.engines?.node ?? 'missing'}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const actualVersion = process.versions.node;
|
|
||||||
const actualMajor = actualVersion.split('.')[0];
|
|
||||||
if (actualMajor !== expectedMajor) {
|
|
||||||
console.error(
|
|
||||||
`Unsupported Node.js runtime: expected ${expectedEngine}, actual ${actualVersion}. Activate .nvmrc before running repository checks.`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Node.js runtime verified: ${actualVersion} (${expectedEngine})`);
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import { Test } from '@nestjs/testing';
|
||||||
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { AppModule } from './../src/app.module';
|
||||||
|
import { configureApp } from './../src/main';
|
||||||
|
|
||||||
|
function canonicalize(value: unknown): unknown {
|
||||||
|
if (Array.isArray(value)) return value.map(canonicalize);
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(value)
|
||||||
|
.sort(([left], [right]) => left.localeCompare(right))
|
||||||
|
.map(([key, nestedValue]) => [key, canonicalize(nestedValue)]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('OpenAPI contract (integration)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
app = module.createNestApplication();
|
||||||
|
configureApp(app);
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => app.close());
|
||||||
|
|
||||||
|
it('matches the committed frontend OpenAPI snapshot', async () => {
|
||||||
|
const runtimeDocument = SwaggerModule.createDocument(
|
||||||
|
app,
|
||||||
|
new DocumentBuilder()
|
||||||
|
.setTitle('WealthLedger API')
|
||||||
|
.setVersion('v1')
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
const snapshot = JSON.parse(
|
||||||
|
await readFile(
|
||||||
|
resolve(__dirname, '../../frontend/src/shared/api/spec/openapi.json'),
|
||||||
|
'utf8',
|
||||||
|
),
|
||||||
|
) as unknown;
|
||||||
|
|
||||||
|
expect(canonicalize(runtimeDocument)).toEqual(canonicalize(snapshot));
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user