Add market watch and match hub workflows

这个提交包含在:
cryptocommuniums-afk
2026-04-07 11:00:03 +08:00
父节点 495da60212
当前提交 32ffad1545
修改 39 个文件,包含 6974 行新增330 行删除

查看文件

@@ -3,6 +3,7 @@ import { appRouter } from "./routers";
import { COOKIE_NAME } from "../shared/const";
import type { TrpcContext } from "./_core/context";
import * as db from "./db";
import * as matchStore from "./matchStore";
import * as trainingAutomation from "./trainingAutomation";
import { ENV } from "./_core/env";
import { sdk } from "./_core/sdk";
@@ -831,14 +832,28 @@ describe("leaderboard.get", () => {
await expect(caller.leaderboard.get()).rejects.toThrow();
});
it("accepts sortBy parameter", async () => {
it("accepts training sortBy parameter", async () => {
const user = createTestUser();
const { ctx } = createMockContext(user);
const caller = appRouter.createCaller(ctx);
for (const sortBy of ["ntrpRating", "totalMinutes", "totalSessions", "totalShots"] as const) {
try {
await caller.leaderboard.get({ sortBy, limit: 10 });
await caller.leaderboard.get({ scope: "training", sortBy, limit: 10 });
} catch (e: any) {
expect(e.message).not.toContain("invalid_enum_value");
}
}
});
it("accepts competitive sortBy parameter", async () => {
const user = createTestUser();
const { ctx } = createMockContext(user);
const caller = appRouter.createCaller(ctx);
for (const sortBy of ["wins", "winRate", "setsWon", "pointsWon", "matches"] as const) {
try {
await caller.leaderboard.get({ scope: "competitive", sortBy, limit: 10 });
} catch (e: any) {
expect(e.message).not.toContain("invalid_enum_value");
}
@@ -856,6 +871,94 @@ describe("leaderboard.get", () => {
});
});
describe("match router", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("requires authentication for listing matches", async () => {
const { ctx } = createMockContext(null);
const caller = appRouter.createCaller(ctx);
await expect(caller.match.list()).rejects.toThrow();
});
it("rejects creating a match that does not include the current user when not admin", async () => {
const user = createTestUser({ id: 99, name: "Viewer" });
const { ctx } = createMockContext(user);
const caller = appRouter.createCaller(ctx);
vi.spyOn(db, "getUserById")
.mockResolvedValueOnce(createTestUser({ id: 1, name: "PlayerA" }))
.mockResolvedValueOnce(createTestUser({ id: 2, name: "PlayerB" }));
await expect(caller.match.create({
title: "League Match",
matchMode: "competitive",
playerAUserId: 1,
playerBUserId: 2,
durationMinutes: 90,
})).rejects.toThrow("只能创建包含自己的比赛");
});
it("rejects reading a match when the user is not an admin or participant", async () => {
const user = createTestUser({ id: 77, name: "Outside" });
const { ctx } = createMockContext(user);
const caller = appRouter.createCaller(ctx);
vi.spyOn(matchStore, "getMatchDetail").mockResolvedValueOnce({
id: 12,
createdByUserId: 1,
matchMode: "daily",
workflowStatus: "review_pending",
title: "Morning Match",
courtName: "Court 1",
notes: null,
durationMinutes: 90,
scheduledAt: new Date(),
startedAt: null,
endedAt: null,
suggestionStatus: "ready",
suggestionTaskId: "task-1",
suggestedScore: null,
suggestedMetrics: null,
finalScore: null,
finalMetrics: null,
reviewNotes: null,
reviewSubmittedAt: null,
reviewedByUserId: null,
reviewedAt: null,
finalizedByUserId: null,
finalizedAt: null,
createdAt: new Date(),
updatedAt: new Date(),
participants: [
{ userId: 1, playerSlot: "player_a" },
{ userId: 2, playerSlot: "player_b" },
] as any,
events: [],
eventCount: 0,
} as any);
await expect(caller.match.get({ matchId: 12 })).rejects.toThrow("当前账号不能访问这场比赛");
});
it("requires admin permission for review submission", async () => {
const user = createTestUser({ id: 7, role: "user" });
const { ctx } = createMockContext(user);
const caller = appRouter.createCaller(ctx);
await expect(caller.match.reviewSubmit({
matchId: 1,
reviewNotes: "ok",
finalScore: {
sets: { player_a: 2, player_b: 0 },
games: { player_a: 12, player_b: 6 },
points: { player_a: 60, player_b: 42 },
},
})).rejects.toThrow();
});
});
// ===== BADGE DEFINITIONS UNIT TESTS =====
describe("BADGE_DEFINITIONS via badge.definitions endpoint", () => {