- 代理层级默认授信比例与停用冻结/禁登全局默认 - 结算预览去重、比分校验、串关当场判负与市场类型校验 - 站内信 Banner/公告自动通知开关;开发环境动态 API 端口 - 扩充 RBAC/结算/认证/返现单元测试与 agent skills
177 lines
5.4 KiB
TypeScript
177 lines
5.4 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { appForbidden } from '../../../shared/common/app-error';
|
|
import { PrismaService } from '../../../shared/prisma/prisma.service';
|
|
import { AgentsService } from '../../agent/agents.service';
|
|
import { BetsService } from '../../betting/bets.service';
|
|
import { SettlementService } from '../../settlement/settlement.service';
|
|
import { WalletService } from '../../ledger/wallet.service';
|
|
import { SMOKE_SUITE_META, SMOKE_TEST_CASES, type SmokeTestCaseDef } from './smoke-test.cases';
|
|
import { BET_FLOW_PROBE_COUNT, createBetFlowProbes } from './smoke-test.bet-flow-probes';
|
|
import { CONFIG_PROBE_COUNT, createConfigProbes } from './smoke-test.config-probes';
|
|
import { createDatabaseProbes, DATABASE_PROBE_COUNT } from './smoke-test.db-probes';
|
|
import {
|
|
beginSmokeSteps,
|
|
drainSmokeSteps,
|
|
formatStepsForResult,
|
|
} from './smoke-test.helpers';
|
|
import type {
|
|
SmokeTestCaseResult,
|
|
SmokeTestRunSummary,
|
|
SmokeTestSuiteInfo,
|
|
} from './smoke-test.types';
|
|
|
|
@Injectable()
|
|
export class SmokeTestService {
|
|
private lastRun: SmokeTestRunSummary | null = null;
|
|
|
|
constructor(
|
|
private prisma: PrismaService,
|
|
private wallet: WalletService,
|
|
private bets: BetsService,
|
|
private settlement: SettlementService,
|
|
private agents: AgentsService,
|
|
) {}
|
|
|
|
isAllowed(): boolean {
|
|
if (process.env.ALLOW_SMOKE_TESTS === 'true') return true;
|
|
return process.env.NODE_ENV !== 'production';
|
|
}
|
|
|
|
assertAllowed() {
|
|
if (!this.isAllowed()) {
|
|
throw appForbidden('SMOKE_TESTS_FORBIDDEN');
|
|
}
|
|
}
|
|
|
|
listSuites(): SmokeTestSuiteInfo[] {
|
|
const counts = new Map<string, number>();
|
|
for (const c of SMOKE_TEST_CASES) {
|
|
counts.set(c.suite, (counts.get(c.suite) ?? 0) + 1);
|
|
}
|
|
counts.set('database', DATABASE_PROBE_COUNT);
|
|
counts.set('bet-flow', BET_FLOW_PROBE_COUNT);
|
|
counts.set('config', CONFIG_PROBE_COUNT);
|
|
|
|
return [...counts.entries()].map(([id, caseCount]) => ({
|
|
id,
|
|
name: SMOKE_SUITE_META[id]?.name ?? id,
|
|
description: SMOKE_SUITE_META[id]?.description ?? '',
|
|
caseCount,
|
|
}));
|
|
}
|
|
|
|
listCases(suites?: string[]) {
|
|
const allow = suites?.length ? new Set(suites) : null;
|
|
return SMOKE_TEST_CASES.filter((c) => !allow || allow.has(c.suite)).map(
|
|
({ id, suite, name, description, uatRef }) => ({ id, suite, name, description, uatRef }),
|
|
);
|
|
}
|
|
|
|
getLastRun() {
|
|
return this.lastRun;
|
|
}
|
|
|
|
async run(suites?: string[], operatorId?: bigint): Promise<SmokeTestRunSummary> {
|
|
const started = Date.now();
|
|
const runId = `SMOKE-${started}-${operatorId?.toString() ?? '0'}`;
|
|
const allow = suites?.length ? new Set(suites) : null;
|
|
|
|
const staticCases = SMOKE_TEST_CASES.filter((c) => !allow || allow.has(c.suite));
|
|
const runDb = !allow || allow.has('database');
|
|
const runBetFlow = !allow || allow.has('bet-flow');
|
|
const runConfig = !allow || allow.has('config');
|
|
|
|
const results: SmokeTestCaseResult[] = [];
|
|
|
|
for (const testCase of staticCases) {
|
|
results.push(await this.executeCase(testCase));
|
|
}
|
|
|
|
if (runDb) {
|
|
for (const probe of createDatabaseProbes(this.prisma)) {
|
|
results.push(await this.executeCase(probe));
|
|
}
|
|
}
|
|
|
|
if (runBetFlow) {
|
|
for (const probe of createBetFlowProbes({
|
|
prisma: this.prisma,
|
|
wallet: this.wallet,
|
|
bets: this.bets,
|
|
settlement: this.settlement,
|
|
agents: this.agents,
|
|
})) {
|
|
results.push(await this.executeCase(probe));
|
|
}
|
|
}
|
|
|
|
if (runConfig) {
|
|
for (const probe of createConfigProbes({
|
|
prisma: this.prisma,
|
|
agents: this.agents,
|
|
})) {
|
|
results.push(await this.executeCase(probe));
|
|
}
|
|
}
|
|
|
|
const finished = Date.now();
|
|
const passed = results.filter((r) => r.status === 'PASS').length;
|
|
const failed = results.filter((r) => r.status === 'FAIL').length;
|
|
const skipped = results.filter((r) => r.status === 'SKIP').length;
|
|
|
|
const summary: SmokeTestRunSummary = {
|
|
runId,
|
|
startedAt: new Date(started).toISOString(),
|
|
finishedAt: new Date(finished).toISOString(),
|
|
durationMs: finished - started,
|
|
total: results.length,
|
|
passed,
|
|
failed,
|
|
skipped,
|
|
suites: [...new Set(results.map((r) => r.suite))],
|
|
results,
|
|
};
|
|
|
|
this.lastRun = summary;
|
|
return summary;
|
|
}
|
|
|
|
private async executeCase(testCase: SmokeTestCaseDef): Promise<SmokeTestCaseResult> {
|
|
const t0 = performance.now();
|
|
const base = {
|
|
id: testCase.id,
|
|
suite: testCase.suite,
|
|
name: testCase.name,
|
|
description: testCase.description,
|
|
uatRef: testCase.uatRef,
|
|
};
|
|
|
|
beginSmokeSteps();
|
|
try {
|
|
await testCase.run();
|
|
const steps = drainSmokeSteps();
|
|
const durationMs = Math.max(0.01, Math.round((performance.now() - t0) * 100) / 100);
|
|
return {
|
|
...base,
|
|
status: 'PASS',
|
|
durationMs,
|
|
stepCount: steps.length,
|
|
message: steps.length ? `${steps.length} steps passed` : 'OK',
|
|
details: formatStepsForResult(steps),
|
|
};
|
|
} catch (err) {
|
|
const steps = drainSmokeSteps();
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
const durationMs = Math.max(0.01, Math.round((performance.now() - t0) * 100) / 100);
|
|
return {
|
|
...base,
|
|
status: 'FAIL',
|
|
durationMs,
|
|
stepCount: steps.length,
|
|
error: message,
|
|
details: formatStepsForResult(steps),
|
|
};
|
|
}
|
|
}
|
|
}
|