feat: add smoke tests, agent credit ledger, and player cashback page
Introduce admin smoke-test suite with API probes, agent credit transaction history, and player cashback records; fix SmokeTestModule DI and polish admin/player UI assets. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
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 { 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,
|
||||
) {}
|
||||
|
||||
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);
|
||||
|
||||
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 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));
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user