文件
tennis-training-hub/server/match.test.ts
2026-04-07 11:00:03 +08:00

96 行
3.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { buildParticipantSettlement, deriveSuggestedMatchState } from "./match";
describe("deriveSuggestedMatchState", () => {
it("aggregates point, game and set events into a suggested scoreboard", () => {
const result = deriveSuggestedMatchState([
{ eventIndex: 1, eventType: "point", source: "camera_a", winnerSlot: "player_a", confidence: 0.6, payload: { rallyCount: 4, isWinner: true } },
{ eventIndex: 2, eventType: "point", source: "camera_b", winnerSlot: "player_a", confidence: 0.7, payload: { rallyCount: 7, isAce: true, firstServeIn: true, firstServeAttempt: true } },
{ eventIndex: 3, eventType: "game", source: "camera_a", winnerSlot: "player_a", confidence: 0.65 },
{ eventIndex: 4, eventType: "set", source: "camera_a", winnerSlot: "player_a", confidence: 0.8 },
]);
expect(result.score.sets.player_a).toBe(1);
expect(result.score.games.player_a).toBe(1);
expect(result.score.points.player_a).toBe(0);
expect(result.score.winnerSlot).toBe("player_a");
expect(result.metrics.players.player_a.pointsWon).toBe(2);
expect(result.metrics.players.player_a.aces).toBe(1);
expect(result.metrics.players.player_a.firstServePct).toBe(100);
expect(result.metrics.longestRally).toBe(7);
expect(result.sourceCount).toBe(2);
});
it("prefers later reviewed score snapshots while keeping metric snapshots", () => {
const result = deriveSuggestedMatchState([
{
eventIndex: 1,
eventType: "score_suggestion",
source: "system",
confidence: 0.78,
payload: {
score: {
sets: { player_a: 1, player_b: 0 },
games: { player_a: 6, player_b: 4 },
points: { player_a: 0, player_b: 0 },
},
metrics: {
player_a: { winners: 14, pointsWon: 32 },
player_b: { winners: 10, pointsWon: 25 },
totalRallies: 18,
},
},
},
{
eventIndex: 2,
eventType: "review_adjustment",
source: "admin",
confidence: 1,
payload: {
score: {
sets: { player_a: 2, player_b: 0 },
games: { player_a: 12, player_b: 6 },
points: { player_a: 68, player_b: 53 },
winnerSlot: "player_a",
},
},
},
]);
expect(result.score.sets.player_a).toBe(2);
expect(result.score.games.player_b).toBe(6);
expect(result.score.points.player_a).toBe(68);
expect(result.score.winnerSlot).toBe("player_a");
expect(result.metrics.players.player_a.winners).toBe(14);
expect(result.metrics.totalRallies).toBe(18);
});
});
describe("buildParticipantSettlement", () => {
it("projects scoreboard and metrics into per-player settlement rows", () => {
const settlement = buildParticipantSettlement(
{
sets: { player_a: 2, player_b: 1 },
games: { player_a: 13, player_b: 11 },
points: { player_a: 74, player_b: 68 },
winnerSlot: "player_a",
},
{
players: {
player_a: { aces: 6, firstServeIn: 32, firstServeAttempts: 40, pointsWon: 74 },
player_b: { aces: 4, firstServeIn: 28, firstServeAttempts: 41, pointsWon: 68 },
},
totalRallies: 22,
longestRally: 11,
},
);
expect(settlement.winnerSlot).toBe("player_a");
expect(settlement.players.player_a.isWinner).toBe(true);
expect(settlement.players.player_a.gamesWon).toBe(13);
expect(settlement.players.player_a.stats.firstServePct).toBe(80);
expect(settlement.players.player_b.stats.firstServePct).toBeCloseTo(68.3, 1);
expect(settlement.summary.longestRally).toBe(11);
});
});