통합 테스트를 실행 권위별로 분리한다
This commit is contained in:
@@ -73,7 +73,7 @@ router 결과를 별도로 비교합니다.
|
||||
`turnCommandFullLifecycle.integration.test.ts`, 실제 PostgreSQL flush/reload는
|
||||
`turnCommandFullLifecyclePersistence.integration.test.ts`가 대표 fixture로 검증합니다.
|
||||
|
||||
기본 integration:
|
||||
기본 Core canonical/projection 계약:
|
||||
|
||||
```sh
|
||||
pnpm test:integration
|
||||
@@ -85,11 +85,21 @@ PostgreSQL·Redis 조건부 경계:
|
||||
pnpm test:integration:conditional
|
||||
```
|
||||
|
||||
live Ref 명령·전투 차등 감사:
|
||||
|
||||
```sh
|
||||
pnpm test:integration:reference
|
||||
```
|
||||
|
||||
Ref runtime은 기본 Core acceptance의 필수 의존성이 아닙니다. 계승 계약 변경과
|
||||
정기 감사에서만 live 비교를 실행하고, 독립한 Core 영역은 versioned Core fixture와
|
||||
불변조건을 권위값으로 사용합니다.
|
||||
|
||||
개별 suite와 test name은 package script 뒤에 전달합니다.
|
||||
|
||||
```sh
|
||||
pnpm --filter @sammo-ts/game-engine test inputEventAtomicity.test.ts
|
||||
pnpm --filter @sammo-ts/integration-tests test:integration
|
||||
pnpm --filter @sammo-ts/integration-tests test:integration:reference -- -t 'test name'
|
||||
```
|
||||
|
||||
실제 script 인자는 해당 package의 `package.json`을 확인해 주세요.
|
||||
|
||||
@@ -2,18 +2,39 @@
|
||||
|
||||
## 실행 명령
|
||||
|
||||
외부 서비스 없이 실행 가능한 integration:
|
||||
빠른 기본 검증은 Core의 canonical snapshot, comparator, log/message projection과
|
||||
logical clock 계약만 실행합니다. Ref runtime이나 DB가 없어도 되며 skip을 성공으로
|
||||
세지 않습니다.
|
||||
|
||||
```sh
|
||||
pnpm test:integration
|
||||
```
|
||||
|
||||
전용 PostgreSQL·Redis를 사용하는 조건부 suite:
|
||||
실제 경계는 목적별 lane으로 분리합니다.
|
||||
|
||||
```sh
|
||||
# 고유 PostgreSQL schema와 Redis namespace
|
||||
pnpm test:integration:conditional
|
||||
|
||||
# 필요할 때 실행하는 Ref 호환성 감사
|
||||
pnpm test:integration:reference
|
||||
|
||||
# public/che schema와 Redis를 초기화하는 전용 stack 전용 lifecycle
|
||||
INTEGRATION_LIFECYCLE_DISPOSABLE=1 pnpm test:integration:lifecycle
|
||||
```
|
||||
|
||||
`tools/integration-tests/test-lanes.tsv`는 모든 `test/**/*.test.ts`를 `core`,
|
||||
`conditional`, `reference`, `lifecycle` 중 정확히 하나에 배정합니다. 기본 runner는
|
||||
실행 전에 실제 파일 집합과 registry를 대조하므로 새 파일의 미등록이나 중복 등록을
|
||||
허용하지 않습니다. Ref 차등과 lifecycle을 기본 명령에서 분리한 것은 test 삭제나
|
||||
silent skip이 아니라 실행 비용·외부 상태·판정 권위를 분리한 것입니다.
|
||||
|
||||
Ref는 매 변경의 제품 실행 의존성이 아닙니다. 계승 계약을 새로 이관하거나 전투·RNG·
|
||||
명령 의미가 바뀌는 변경, 정기 호환성 감사에서는 `test:integration:reference`를
|
||||
실행합니다. 일반 Core 변경은 Core 자체의 versioned fixture, canonical projection과
|
||||
불변조건을 기본 acceptance로 사용합니다. 확인된 의도적 제품 차이는 Core 기대값을
|
||||
갱신하고 문서화하며, Ref 결과에 자동으로 되맞추지 않습니다.
|
||||
|
||||
조건부 runner는 환경 변수 존재 여부만으로 안전성을 보장하지 않습니다.
|
||||
대상 host, port, database와 Redis prefix가 해당 worktree 전용인지 확인해
|
||||
주세요. Runner는 실행 ID가 포함된 game schema를 생성하고 성공, 테스트 실패,
|
||||
@@ -73,8 +94,9 @@ runtime role을 삭제하고 PID와 명령행 및 daemon 종료를 확인한 뒤
|
||||
- 관리자 시간 가속·연기의 durable action, checkpoint, Redis 부분 재시도와
|
||||
경매 timer race
|
||||
|
||||
실제 포함 suite는 `tools/run-conditional-integration.sh`, 각 package의
|
||||
`package.json`, `*.integration.test.ts`를 기준으로 확인합니다. DB 조건부
|
||||
실제 포함 suite는 `tools/integration-tests/test-lanes.tsv`,
|
||||
`tools/run-conditional-integration.sh`, 각 package의 `package.json`,
|
||||
`*.integration.test.ts`를 기준으로 확인합니다. DB 조건부
|
||||
환경 변수는 `tools/conditional-integration-registry.tsv`에서 명시적으로
|
||||
관리합니다. 새 `*_DATABASE_URL` gate가 registry에 없거나 registry 항목이
|
||||
더 이상 테스트에 존재하지 않으면 runner가 테스트 실행 전에 실패합니다.
|
||||
@@ -135,8 +157,8 @@ PostgreSQL·Redis 경계를 증명하지 않습니다. Full suite 실패는 변
|
||||
Ref 호환성 판정은 [차등 검증](architecture/turn-state-differential-testing.md),
|
||||
UI는 [프론트엔드 호환 검증](frontend-legacy-parity.md)을 함께 사용합니다.
|
||||
|
||||
전투 아이템 전체 차등 검증은 공격자·방어자 각각 145개 fixture를 모두 Core와
|
||||
Ref에서 실행하고 event, RNG, outcome, 전체 log bucket을 항목별로 비교합니다.
|
||||
Ref harness는 fixture마다 새 PHP process를 띄우지 않고 `--jsonl` 입력을 한
|
||||
process에서 일괄 처리합니다. `ITEM_PARITY_FILTER`는 특정 항목을 진단할 때만
|
||||
사용하며, 전체 검증의 fixture 수나 assertion을 줄이는 최적화로 사용하지 않습니다.
|
||||
Ref 감사 lane의 전투 아이템 전체 차등 검증은 공격자·방어자 각각 145개 fixture를
|
||||
모두 Core와 Ref에서 실행하고 event, RNG, outcome, 전체 log bucket을 항목별로
|
||||
비교합니다. Ref harness는 fixture마다 새 PHP process를 띄우지 않고 `--jsonl`
|
||||
batch를 한 process에서 처리합니다. `ITEM_PARITY_FILTER`는 특정 항목 진단에만
|
||||
사용하며 전체 감사의 fixture 수나 assertion을 줄이는 최적화로 사용하지 않습니다.
|
||||
|
||||
@@ -143,8 +143,15 @@ Fastify transport, Prisma transaction, lease와 worker 경계를 검증합니다
|
||||
```sh
|
||||
pnpm test:integration
|
||||
pnpm test:integration:conditional
|
||||
pnpm test:integration:reference
|
||||
INTEGRATION_LIFECYCLE_DISPOSABLE=1 pnpm test:integration:lifecycle
|
||||
```
|
||||
|
||||
기본 integration은 Core 계약의 빠른 feedback lane입니다. 실제 PostgreSQL/Redis,
|
||||
live Ref 호환성 감사, schema·PM2 전체 lifecycle은 각각 별도 명령으로 실행하며
|
||||
기본 명령의 skip으로 포함된 것처럼 보고하지 않습니다. 모든 integration test file의
|
||||
lane 소유권은 `tools/integration-tests/test-lanes.tsv`에서 exact-set으로 관리합니다.
|
||||
|
||||
조건부 suite는 worktree별 PostgreSQL·Redis instance를 준비합니다. Test가
|
||||
schema truncate와 Redis 정리를 수행하므로 공유 개발 instance를 사용하지
|
||||
않습니다.
|
||||
|
||||
+3
-1
@@ -16,8 +16,10 @@
|
||||
"test:bootstrap": "pnpm install --offline --frozen-lockfile && pnpm test:prepare",
|
||||
"test:prepare": "pnpm --filter @sammo-ts/infra prisma:generate && pnpm --filter @sammo-ts/common build && pnpm --filter @sammo-ts/logic build && pnpm --filter @sammo-ts/infra build && pnpm --filter @sammo-ts/game-engine build && pnpm --filter @sammo-ts/game-api build && pnpm --filter @sammo-ts/gateway-api build",
|
||||
"test:integration": "pnpm --filter @sammo-ts/integration-tests test:integration",
|
||||
"test:integration:reference": "pnpm --filter @sammo-ts/integration-tests test:integration:reference",
|
||||
"test:integration:lifecycle": "pnpm --filter @sammo-ts/integration-tests test:integration:lifecycle",
|
||||
"test:integration:conditional": "./tools/run-conditional-integration.sh",
|
||||
"test:conditional-integration-registry": "node --test tools/check-conditional-integration-files.test.mjs",
|
||||
"test:conditional-integration-registry": "node --test tools/check-conditional-integration-files.test.mjs && node tools/integration-tests/run-test-lane.mjs check",
|
||||
"build": "turbo build --ui=stream --output-logs=errors-only",
|
||||
"build-verbose": "turbo build --ui=stream --output-logs=full",
|
||||
"typecheck": "turbo typecheck",
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test:integration": "vitest run --config vitest.config.ts",
|
||||
"test:integration": "node run-test-lane.mjs core",
|
||||
"test:integration:reference": "node run-test-lane.mjs reference",
|
||||
"test:integration:lifecycle": "node run-test-lane.mjs lifecycle",
|
||||
"typecheck": "pnpm -w tsc7 -b tools/integration-tests/tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const packageRoot = path.dirname(fileURLToPath(import.meta.url));
|
||||
const registryPath = path.join(packageRoot, 'test-lanes.tsv');
|
||||
const testRoot = path.join(packageRoot, 'test');
|
||||
const supportedLanes = new Set(['core', 'reference', 'lifecycle', 'conditional']);
|
||||
const supportedCommands = new Set(['core', 'reference', 'lifecycle', 'check']);
|
||||
|
||||
const usage = () => {
|
||||
process.stderr.write('usage: node run-test-lane.mjs <core|reference|lifecycle> [vitest arguments...]\n');
|
||||
process.exit(64);
|
||||
};
|
||||
|
||||
const [lane, ...rawVitestArguments] = process.argv.slice(2);
|
||||
const vitestArguments = rawVitestArguments[0] === '--' ? rawVitestArguments.slice(1) : rawVitestArguments;
|
||||
if (!lane || !supportedCommands.has(lane)) {
|
||||
usage();
|
||||
}
|
||||
|
||||
const rows = fs
|
||||
.readFileSync(registryPath, 'utf8')
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line !== '' && !line.startsWith('#'))
|
||||
.map((line, index) => {
|
||||
const fields = line.split('\t');
|
||||
if (fields.length !== 2) {
|
||||
throw new Error(`test-lanes.tsv row ${index + 1} must have exactly two tab-separated fields`);
|
||||
}
|
||||
const [file, registeredLane] = fields;
|
||||
if (!file || !registeredLane || !supportedLanes.has(registeredLane)) {
|
||||
throw new Error(`test-lanes.tsv row ${index + 1} is invalid`);
|
||||
}
|
||||
return { file, lane: registeredLane };
|
||||
});
|
||||
|
||||
const duplicateFiles = rows.map(({ file }) => file).filter((file, index, files) => files.indexOf(file) !== index);
|
||||
if (duplicateFiles.length > 0) {
|
||||
throw new Error(`duplicate integration test lane entries: ${[...new Set(duplicateFiles)].join(', ')}`);
|
||||
}
|
||||
|
||||
const discoveredFiles = fs
|
||||
.readdirSync(testRoot, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith('.test.ts'))
|
||||
.map((entry) => `test/${entry.name}`)
|
||||
.sort();
|
||||
const registeredFiles = rows.map(({ file }) => file).sort();
|
||||
if (JSON.stringify(discoveredFiles) !== JSON.stringify(registeredFiles)) {
|
||||
const registered = new Set(registeredFiles);
|
||||
const discovered = new Set(discoveredFiles);
|
||||
const missing = discoveredFiles.filter((file) => !registered.has(file));
|
||||
const stale = registeredFiles.filter((file) => !discovered.has(file));
|
||||
throw new Error(
|
||||
`integration test lane registry mismatch; missing=[${missing.join(', ')}] stale=[${stale.join(', ')}]`
|
||||
);
|
||||
}
|
||||
|
||||
if (lane === 'check') {
|
||||
process.stdout.write(`integration test lane registry is valid (${registeredFiles.length} files)\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (lane === 'lifecycle' && process.env.INTEGRATION_LIFECYCLE_DISPOSABLE !== '1') {
|
||||
throw new Error(
|
||||
'lifecycle tests truncate public/che schemas and Redis; set INTEGRATION_LIFECYCLE_DISPOSABLE=1 only for a dedicated stack'
|
||||
);
|
||||
}
|
||||
|
||||
const files = rows.filter((row) => row.lane === lane).map(({ file }) => file);
|
||||
const environment = {
|
||||
...process.env,
|
||||
...(lane === 'reference' ? { TURN_DIFFERENTIAL_REFERENCE: '1' } : {}),
|
||||
};
|
||||
const result = spawnSync(
|
||||
'pnpm',
|
||||
['exec', 'vitest', 'run', '--config', 'vitest.config.ts', ...files, ...vitestArguments],
|
||||
{
|
||||
cwd: packageRoot,
|
||||
env: environment,
|
||||
stdio: 'inherit',
|
||||
}
|
||||
);
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
process.exit(result.status ?? 1);
|
||||
@@ -0,0 +1,30 @@
|
||||
# Test file Lane
|
||||
test/auctionFlow.test.ts lifecycle
|
||||
test/battleDifferential.test.ts reference
|
||||
test/coreCommandTraceClock.test.ts core
|
||||
test/initialization.test.ts lifecycle
|
||||
test/instantDiplomacyCoreReference.integration.test.ts reference
|
||||
test/instantDiplomacyReference.integration.test.ts reference
|
||||
test/liveSortiePersistence.integration.test.ts conditional
|
||||
test/monthlyDisasterCoreReference.integration.test.ts reference
|
||||
test/monthlyNationBettingApiLifecycle.integration.test.ts conditional
|
||||
test/npcPossessionSelectionReference.integration.test.ts conditional
|
||||
test/orchestrator.e2e.test.ts lifecycle
|
||||
test/reservedTurnDaemonApiRace.integration.test.ts conditional
|
||||
test/scenario29NationBettingLifecycle.integration.test.ts conditional
|
||||
test/tournamentLifecycle.test.ts lifecycle
|
||||
test/troopStaticEvent.integration.test.ts conditional
|
||||
test/turnCommandCoreReference.integration.test.ts reference
|
||||
test/turnCommandFullLifecycle.integration.test.ts reference
|
||||
test/turnCommandFullLifecyclePersistence.integration.test.ts conditional
|
||||
test/turnCommandGeneralMatrix.integration.test.ts reference
|
||||
test/turnCommandNationMatrix.integration.test.ts reference
|
||||
test/turnCommandReference.integration.test.ts reference
|
||||
test/turnCommandRiskDurabilityMatrix.integration.test.ts conditional
|
||||
test/turnLogProjection.test.ts core
|
||||
test/turnMessageProjection.test.ts core
|
||||
test/turnSnapshotCanonicalCoverage.test.ts core
|
||||
test/turnSnapshotComparator.test.ts core
|
||||
test/turnSnapshotCoreDatabase.integration.test.ts conditional
|
||||
test/turnSnapshotReference.integration.test.ts reference
|
||||
test/turnTraceFiles.integration.test.ts conditional
|
||||
|
@@ -1402,6 +1402,13 @@ describeWithReference('ref ↔ core2026 battle differential', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const executions: Array<{
|
||||
entry: (typeof cases)[number];
|
||||
base: BattleSimRequestPayload & { startYear: number };
|
||||
coreEvents: WarBattleTraceEvent[];
|
||||
coreRng: TracingRng | null;
|
||||
coreOutcome: WarBattleOutcome | null;
|
||||
}> = [];
|
||||
for (const entry of cases) {
|
||||
const base = readJson<BattleSimRequestPayload & { startYear: number }>(
|
||||
path.resolve(process.cwd(), 'fixtures/battle/basic-infantry.json')
|
||||
@@ -1441,13 +1448,16 @@ describeWithReference('ref ↔ core2026 battle differential', () => {
|
||||
},
|
||||
}
|
||||
);
|
||||
executions.push({ entry, base, coreEvents, coreRng, coreOutcome });
|
||||
}
|
||||
|
||||
const references = runReferenceTraceBatch(
|
||||
workspaceRoot!,
|
||||
executions.map(({ base }) => JSON.stringify(base))
|
||||
);
|
||||
executions.forEach(({ entry, coreEvents, coreRng, coreOutcome }, index) => {
|
||||
try {
|
||||
assertTraceParity(
|
||||
coreEvents,
|
||||
runReferenceTrace(workspaceRoot!, JSON.stringify(base)),
|
||||
coreRng,
|
||||
coreOutcome
|
||||
);
|
||||
assertTraceParity(coreEvents, references[index]!, coreRng, coreOutcome);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`${entry.kind}/${entry.key}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
@@ -1456,7 +1466,7 @@ describeWithReference('ref ↔ core2026 battle differential', () => {
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('loads every scenario item with the scenario slot', async () => {
|
||||
|
||||
@@ -571,6 +571,7 @@ run_redis_only_tests() {
|
||||
export PATH
|
||||
cd "$workspace_root"
|
||||
node "$script_dir/check-conditional-integration-files.mjs"
|
||||
node "$workspace_root/tools/integration-tests/run-test-lane.mjs" check
|
||||
validate_marker_registry
|
||||
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
Reference in New Issue
Block a user