@cs2dak/contract 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/analysis.ts +222 -0
- package/src/cohort.ts +59 -0
- package/src/contract.test.ts +559 -0
- package/src/demo-package.ts +38 -0
- package/src/index.ts +11 -599
- package/src/leaderboard.ts +102 -0
- package/src/player.ts +96 -0
- package/src/qa.ts +21 -0
- package/src/scoring.ts +79 -0
- package/src/series.ts +77 -0
- package/src/team.ts +72 -0
- package/src/upstream.ts +90 -0
- package/src/workspace.ts +167 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 合同 schema 冒烟测试 — 验证每个文件的 z.parse 可通过,以及关键约束(enum、literal、nullable)生效。
|
|
3
|
+
* 这是拆分后的第一道安全网;测试失败意味着上游 cs2-demo-format 合同或本包结构发生破坏性变更。
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
// qa
|
|
8
|
+
qaIssueSchema,
|
|
9
|
+
qaReportSchema,
|
|
10
|
+
// scoring
|
|
11
|
+
rrIndicatorsSchema,
|
|
12
|
+
// upstream re-exports
|
|
13
|
+
teamKeySchema,
|
|
14
|
+
sideSchema,
|
|
15
|
+
economyTypeSchema,
|
|
16
|
+
// demo-package
|
|
17
|
+
demoPackageSchema,
|
|
18
|
+
// analysis
|
|
19
|
+
playerRoundFactSchema,
|
|
20
|
+
playerScoreboardRowSchema,
|
|
21
|
+
timelineEventSchema,
|
|
22
|
+
economyPointSchema,
|
|
23
|
+
heatmapPointSchema,
|
|
24
|
+
mapViewSchema,
|
|
25
|
+
analysisProvenanceSchema,
|
|
26
|
+
analysisBundleSchema,
|
|
27
|
+
// cohort
|
|
28
|
+
seasonCohortBundleSchema,
|
|
29
|
+
// workspace
|
|
30
|
+
workspaceTabSchema,
|
|
31
|
+
matchWorkspaceModelSchema,
|
|
32
|
+
} from "./index.js";
|
|
33
|
+
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
// 复用 fixtures
|
|
36
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
const qaReport = {
|
|
39
|
+
ok: true,
|
|
40
|
+
summary: { issueCount: 0, errorCount: 0, warningCount: 0 },
|
|
41
|
+
issues: [],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const minimalIndicators = {
|
|
45
|
+
steamId64: "76561198000000001",
|
|
46
|
+
totalRounds: 21,
|
|
47
|
+
kills: 10, deaths: 8, assists: 2,
|
|
48
|
+
kpr: 0.48, dpr: 0.38, apr: 0.10,
|
|
49
|
+
adr: 72.5, hsPercent: 40, kast: 66.7,
|
|
50
|
+
survivalRate: 0.62,
|
|
51
|
+
twoKillRounds: 3, threeKillRounds: 1, fourKillRounds: 0, fiveKillRounds: 0,
|
|
52
|
+
multiKillRate: 0.19,
|
|
53
|
+
firstKillCount: 2, firstDeathCount: 1,
|
|
54
|
+
firstKillRate: 0.095, firstDeathRate: 0.048,
|
|
55
|
+
openingDuelRate: 0.14, openingDuelWinRate: 0.67,
|
|
56
|
+
tradeKillCount: 1, tradeDeathCount: 2,
|
|
57
|
+
tradeKillRate: 0.048, tradeDeathRate: 0.095,
|
|
58
|
+
clutchAttempts: 3, clutchWins: 1, clutchWinRate: 0.33, clutchFrequency: 0.14,
|
|
59
|
+
clutchScore: 1.2, clutchScoreRate: 0.057,
|
|
60
|
+
vsOne: { count: 2, won: 1 }, vsTwo: { count: 1, won: 0 },
|
|
61
|
+
vsThree: { count: 0, won: 0 }, vsFour: { count: 0, won: 0 }, vsFive: { count: 0, won: 0 },
|
|
62
|
+
awpKills: 2, awpKillsPerRound: 0.095, awpKillRate: 0.2,
|
|
63
|
+
sniperKills: 2, sniperKillRate: 0.2,
|
|
64
|
+
awpMultiKillRate: null, awpDuelWinRate: null,
|
|
65
|
+
utilityDamage: 150, utilityDamagePerRound: 7.1,
|
|
66
|
+
flashAssistCount: 3, flashAssistPerRound: 0.14,
|
|
67
|
+
blindDurationTotal: 8.5, blindDurationPerRound: 0.4,
|
|
68
|
+
enemyFlashDurationSeconds: null, enemyFlashDurationPerRound: null,
|
|
69
|
+
teamFlashDurationSeconds: null, teamFlashDurationPerRound: null,
|
|
70
|
+
grenadeCount: 12, grenadeCountPerRound: 0.57,
|
|
71
|
+
ecoRoundCount: 2, forceRoundCount: 3, fullBuyRoundCount: 14, pistolRoundCount: 2,
|
|
72
|
+
avgEquipmentValue: 3800,
|
|
73
|
+
combatDeathCount: null, bombDeathCount: null,
|
|
74
|
+
wallbangKillCount: null,
|
|
75
|
+
roundSwingTotal: null, roundSwingPerKill: null,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const minimalProvenance = {
|
|
79
|
+
analysisVersion: "cs2-demo-analysis-kit/1.0",
|
|
80
|
+
sourceSchemaVersion: "cs2-demo-format/2.0" as const,
|
|
81
|
+
sourceDemoHash: null,
|
|
82
|
+
exporter: { name: "cs2dak", version: "0.2.1" },
|
|
83
|
+
parser: { name: "demoparser2", version: "0.1.0" },
|
|
84
|
+
ratingVersions: { rr: "1.0.0", valueAccounts: "1.0.0" },
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const minimalScoreboardRow = {
|
|
88
|
+
steamId64: "76561198000000001",
|
|
89
|
+
name: "Player A",
|
|
90
|
+
teamKey: "teamA" as const,
|
|
91
|
+
kills: 10, deaths: 8, assists: 2,
|
|
92
|
+
adr: 72.5, kast: 66.7, headshotPercent: 40,
|
|
93
|
+
entryKills: 2, tradeKills: 1, awpKills: 2, utilityDamage: 150,
|
|
94
|
+
combatDeathCount: null, bombDeathCount: null,
|
|
95
|
+
wallbangKillCount: null, noScopeKillCount: null,
|
|
96
|
+
throughSmokeKillCount: null, collateralKillCount: null,
|
|
97
|
+
bombPlantCount: null, bombDefuseCount: null,
|
|
98
|
+
confidence: 0.85,
|
|
99
|
+
fieldAvailability: {
|
|
100
|
+
playerStats: "available" as const,
|
|
101
|
+
economy: "available" as const,
|
|
102
|
+
rounds: "available" as const,
|
|
103
|
+
richKills: "partial" as const,
|
|
104
|
+
damages: "available" as const,
|
|
105
|
+
bombs: "missing" as const,
|
|
106
|
+
},
|
|
107
|
+
ratingSeed: 1.1,
|
|
108
|
+
rr: 1.05, rrPercentile: 55,
|
|
109
|
+
accountRR: 1.1, accountRRRaw: 1.08,
|
|
110
|
+
accountCombatContextFactor: 1.0,
|
|
111
|
+
accountBreakdown: { combat: 0.4, trade: 0.1, clutch: 0.05, objective: 0.05, utility: 0.1 },
|
|
112
|
+
accountContextStatus: { buyDelta: "available" as const, manState: "available" as const },
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const minimalBundle = {
|
|
116
|
+
version: "cs2-demo-analysis-kit/1.0" as const,
|
|
117
|
+
sourceSchemaVersion: "cs2-demo-format/2.0" as const,
|
|
118
|
+
provenance: minimalProvenance,
|
|
119
|
+
mapName: "de_ancient",
|
|
120
|
+
tickrate: 64,
|
|
121
|
+
teams: {
|
|
122
|
+
teamA: { name: "Team A", score: 13 },
|
|
123
|
+
teamB: { name: "Team B", score: 8 },
|
|
124
|
+
},
|
|
125
|
+
scoreboard: [],
|
|
126
|
+
playerWeaponHighlights: [],
|
|
127
|
+
playerIndicators: [],
|
|
128
|
+
playerRoundFacts: [],
|
|
129
|
+
timeline: [],
|
|
130
|
+
economy: [],
|
|
131
|
+
heatmap: [],
|
|
132
|
+
qa: qaReport,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const minimalWorkspaceModel = {
|
|
136
|
+
version: "cs2-demo-analysis-kit/workspace-0.1" as const,
|
|
137
|
+
sourceSchemaVersion: "cs2-demo-format/2.0" as const,
|
|
138
|
+
title: "Team A vs Team B",
|
|
139
|
+
subtitle: "de_ancient · 10 名选手 · 21 回合",
|
|
140
|
+
scoreline: "13:8",
|
|
141
|
+
mapName: "de_ancient",
|
|
142
|
+
teams: { teamA: { name: "Team A", score: 13 }, teamB: { name: "Team B", score: 8 } },
|
|
143
|
+
tabs: [{ key: "overview" as const, label: "总览" }],
|
|
144
|
+
overview: { kpis: [], story: [] },
|
|
145
|
+
scoreboard: [],
|
|
146
|
+
rounds: [],
|
|
147
|
+
players: [],
|
|
148
|
+
economy: [],
|
|
149
|
+
map: {
|
|
150
|
+
view: { name: "de_ancient", radarImageUrl: null, calibrated: false },
|
|
151
|
+
modes: [],
|
|
152
|
+
points: [],
|
|
153
|
+
status: { hasRadar: false, hasPositionData: false, message: null },
|
|
154
|
+
},
|
|
155
|
+
replay: {
|
|
156
|
+
available: false,
|
|
157
|
+
sampleRate: null,
|
|
158
|
+
tickrate: null,
|
|
159
|
+
rounds: [],
|
|
160
|
+
capabilities: { hasDefuseKit: false, hasBombPosition: false },
|
|
161
|
+
},
|
|
162
|
+
adminQa: qaReport,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
166
|
+
// qa.ts
|
|
167
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
describe("qa schemas", () => {
|
|
170
|
+
it("qaIssueSchema parses valid issue", () => {
|
|
171
|
+
expect(() =>
|
|
172
|
+
qaIssueSchema.parse({ severity: "warning", code: "MISSING_ECONOMY", message: "Economy data missing" })
|
|
173
|
+
).not.toThrow();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("qaIssueSchema rejects invalid severity", () => {
|
|
177
|
+
expect(qaIssueSchema.safeParse({ severity: "critical", code: "X", message: "Y" }).success).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("qaIssueSchema rejects empty code or message", () => {
|
|
181
|
+
expect(qaIssueSchema.safeParse({ severity: "error", code: "", message: "Y" }).success).toBe(false);
|
|
182
|
+
expect(qaIssueSchema.safeParse({ severity: "error", code: "X", message: "" }).success).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("qaReportSchema parses valid report", () => {
|
|
186
|
+
expect(() => qaReportSchema.parse(qaReport)).not.toThrow();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("qaReportSchema rejects negative counts", () => {
|
|
190
|
+
expect(
|
|
191
|
+
qaReportSchema.safeParse({ ok: true, summary: { issueCount: -1, errorCount: 0, warningCount: 0 }, issues: [] }).success
|
|
192
|
+
).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
197
|
+
// upstream.ts — re-exports from cs2-demo-format
|
|
198
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
describe("upstream re-exports", () => {
|
|
201
|
+
it("teamKeySchema accepts teamA / teamB only", () => {
|
|
202
|
+
expect(teamKeySchema.parse("teamA")).toBe("teamA");
|
|
203
|
+
expect(teamKeySchema.parse("teamB")).toBe("teamB");
|
|
204
|
+
expect(teamKeySchema.safeParse("teamC").success).toBe(false);
|
|
205
|
+
expect(teamKeySchema.safeParse("").success).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("sideSchema accepts ct / t only", () => {
|
|
209
|
+
expect(sideSchema.parse("ct")).toBe("ct");
|
|
210
|
+
expect(sideSchema.parse("t")).toBe("t");
|
|
211
|
+
expect(sideSchema.safeParse("CT").success).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("economyTypeSchema accepts known economy types", () => {
|
|
215
|
+
for (const val of ["pistol", "eco", "semi", "force", "full"]) {
|
|
216
|
+
expect(() => economyTypeSchema.parse(val)).not.toThrow();
|
|
217
|
+
}
|
|
218
|
+
expect(economyTypeSchema.safeParse("full-buy").success).toBe(false);
|
|
219
|
+
expect(economyTypeSchema.safeParse("rich").success).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
224
|
+
// scoring.ts — rrIndicatorsSchema
|
|
225
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
describe("rrIndicatorsSchema", () => {
|
|
228
|
+
it("parses a complete valid indicators object", () => {
|
|
229
|
+
expect(() => rrIndicatorsSchema.parse(minimalIndicators)).not.toThrow();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("accepts null for nullable rate fields", () => {
|
|
233
|
+
const result = rrIndicatorsSchema.parse(minimalIndicators);
|
|
234
|
+
expect(result.awpMultiKillRate).toBeNull();
|
|
235
|
+
expect(result.awpDuelWinRate).toBeNull();
|
|
236
|
+
expect(result.enemyFlashDurationSeconds).toBeNull();
|
|
237
|
+
expect(result.roundSwingTotal).toBeNull();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("rejects negative nonnegative counts", () => {
|
|
241
|
+
expect(rrIndicatorsSchema.safeParse({ ...minimalIndicators, kills: -1 }).success).toBe(false);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
246
|
+
// demo-package.ts — demoPackageSchema
|
|
247
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
describe("demoPackageSchema", () => {
|
|
250
|
+
it("applies defaults for optional array fields", () => {
|
|
251
|
+
const pkg = demoPackageSchema.parse({
|
|
252
|
+
manifest: {
|
|
253
|
+
schemaVersion: "cs2-demo-format/2.0",
|
|
254
|
+
exporter: { name: "cs2dak", version: "0.2.1" },
|
|
255
|
+
parser: { name: "demoparser2", version: "0.1.0" },
|
|
256
|
+
demo: { hash: null, sourceFileName: null },
|
|
257
|
+
mapName: "de_ancient",
|
|
258
|
+
tickrate: 64,
|
|
259
|
+
exportedAt: "2024-01-01T00:00:00Z",
|
|
260
|
+
files: {
|
|
261
|
+
match: "match.json",
|
|
262
|
+
players: "players.json",
|
|
263
|
+
rounds: "rounds.json",
|
|
264
|
+
playerStats: "player_stats.json",
|
|
265
|
+
playerEconomies: "player_economies.json",
|
|
266
|
+
kills: "kills.json",
|
|
267
|
+
damages: "damages.json",
|
|
268
|
+
blinds: "blinds.json",
|
|
269
|
+
bombs: "bombs.json",
|
|
270
|
+
grenades: "grenades.json",
|
|
271
|
+
clutches: "clutches.json",
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
match: {
|
|
275
|
+
mapName: "de_ancient",
|
|
276
|
+
tickrate: 64,
|
|
277
|
+
durationSeconds: 1800,
|
|
278
|
+
serverName: null,
|
|
279
|
+
source: "valve",
|
|
280
|
+
teamA: { teamKey: "teamA", name: "Team A", score: 13 },
|
|
281
|
+
teamB: { teamKey: "teamB", name: "Team B", score: 8 },
|
|
282
|
+
},
|
|
283
|
+
players: [],
|
|
284
|
+
rounds: [],
|
|
285
|
+
});
|
|
286
|
+
expect(pkg.playerEconomies).toEqual([]);
|
|
287
|
+
expect(pkg.playerStats).toEqual([]);
|
|
288
|
+
expect(pkg.kills).toEqual([]);
|
|
289
|
+
expect(pkg.damages).toEqual([]);
|
|
290
|
+
expect(pkg.blinds).toEqual([]);
|
|
291
|
+
expect(pkg.bombs).toEqual([]);
|
|
292
|
+
expect(pkg.grenades).toEqual([]);
|
|
293
|
+
expect(pkg.clutches).toEqual([]);
|
|
294
|
+
expect(pkg.shots).toBeUndefined();
|
|
295
|
+
expect(pkg.positions1s).toBeUndefined();
|
|
296
|
+
expect(pkg.replay).toBeUndefined();
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
301
|
+
// analysis.ts
|
|
302
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
describe("playerRoundFactSchema", () => {
|
|
305
|
+
const fact = {
|
|
306
|
+
roundNumber: 1,
|
|
307
|
+
steamId64: "76561198000000001",
|
|
308
|
+
name: "Player A",
|
|
309
|
+
teamKey: "teamA",
|
|
310
|
+
side: "ct",
|
|
311
|
+
survived: true,
|
|
312
|
+
kills: 2, deaths: 0, assists: 1,
|
|
313
|
+
damage: 180, utilityDamage: 20,
|
|
314
|
+
flashAssists: 1, tradeKills: 0, tradedDeaths: 0,
|
|
315
|
+
openingDuel: "won",
|
|
316
|
+
kastTags: ["kill", "survive"],
|
|
317
|
+
equipmentValue: 5400,
|
|
318
|
+
economyType: "full",
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
it("parses valid round fact", () => {
|
|
322
|
+
expect(() => playerRoundFactSchema.parse(fact)).not.toThrow();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("accepts null for nullable fields", () => {
|
|
326
|
+
const result = playerRoundFactSchema.parse({ ...fact, equipmentValue: null, economyType: null });
|
|
327
|
+
expect(result.equipmentValue).toBeNull();
|
|
328
|
+
expect(result.economyType).toBeNull();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("rejects zero or negative roundNumber", () => {
|
|
332
|
+
expect(playerRoundFactSchema.safeParse({ ...fact, roundNumber: 0 }).success).toBe(false);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("rejects invalid openingDuel value", () => {
|
|
336
|
+
expect(playerRoundFactSchema.safeParse({ ...fact, openingDuel: "draw" }).success).toBe(false);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("rejects invalid kastTag", () => {
|
|
340
|
+
expect(playerRoundFactSchema.safeParse({ ...fact, kastTags: ["kill", "bomb"] }).success).toBe(false);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe("timelineEventSchema", () => {
|
|
345
|
+
const event = {
|
|
346
|
+
id: "ev-1",
|
|
347
|
+
roundNumber: 1,
|
|
348
|
+
tick: 1000,
|
|
349
|
+
timeSeconds: 15.5,
|
|
350
|
+
clockPhase: "round",
|
|
351
|
+
clockSeconds: 15.5,
|
|
352
|
+
clockLabel: "0:45",
|
|
353
|
+
type: "kill",
|
|
354
|
+
label: "Player A killed Player B",
|
|
355
|
+
teamKey: "teamA",
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
it("parses valid timeline event", () => {
|
|
359
|
+
expect(() => timelineEventSchema.parse(event)).not.toThrow();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("accepts null teamKey", () => {
|
|
363
|
+
expect(timelineEventSchema.parse({ ...event, teamKey: null }).teamKey).toBeNull();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("rejects invalid clockPhase", () => {
|
|
367
|
+
expect(timelineEventSchema.safeParse({ ...event, clockPhase: "overtime" }).success).toBe(false);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("rejects invalid event type", () => {
|
|
371
|
+
expect(timelineEventSchema.safeParse({ ...event, type: "defuse" }).success).toBe(false);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe("economyPointSchema", () => {
|
|
376
|
+
it("parses valid economy point", () => {
|
|
377
|
+
expect(() =>
|
|
378
|
+
economyPointSchema.parse({
|
|
379
|
+
roundNumber: 5,
|
|
380
|
+
teamA: 4200, teamB: 1800,
|
|
381
|
+
advantage: 2400,
|
|
382
|
+
teamAEconomy: "full",
|
|
383
|
+
teamBEconomy: "eco",
|
|
384
|
+
winnerTeamKey: "teamA",
|
|
385
|
+
})
|
|
386
|
+
).not.toThrow();
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
describe("heatmapPointSchema", () => {
|
|
391
|
+
it("parses valid kill heatmap point", () => {
|
|
392
|
+
expect(() =>
|
|
393
|
+
heatmapPointSchema.parse({
|
|
394
|
+
x: 100, y: 200, z: 50,
|
|
395
|
+
roundNumber: 3,
|
|
396
|
+
teamKey: "teamA",
|
|
397
|
+
steamId64: "76561198000000001",
|
|
398
|
+
side: "ct",
|
|
399
|
+
kind: "kill",
|
|
400
|
+
grenadeType: null,
|
|
401
|
+
})
|
|
402
|
+
).not.toThrow();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("accepts null for optional context fields", () => {
|
|
406
|
+
const result = heatmapPointSchema.parse({
|
|
407
|
+
x: 0, y: 0, z: 0,
|
|
408
|
+
roundNumber: 1,
|
|
409
|
+
teamKey: null, steamId64: null, side: null,
|
|
410
|
+
kind: "grenade",
|
|
411
|
+
grenadeType: "smoke",
|
|
412
|
+
});
|
|
413
|
+
expect(result.teamKey).toBeNull();
|
|
414
|
+
expect(result.steamId64).toBeNull();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("rejects invalid kind", () => {
|
|
418
|
+
expect(
|
|
419
|
+
heatmapPointSchema.safeParse({
|
|
420
|
+
x: 0, y: 0, z: 0, roundNumber: 1,
|
|
421
|
+
teamKey: null, steamId64: null, side: null,
|
|
422
|
+
kind: "bomb", grenadeType: null,
|
|
423
|
+
}).success
|
|
424
|
+
).toBe(false);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe("mapViewSchema", () => {
|
|
429
|
+
it("parses with null radarImageUrl", () => {
|
|
430
|
+
const result = mapViewSchema.parse({ name: "de_ancient", radarImageUrl: null, calibrated: false });
|
|
431
|
+
expect(result.radarImageUrl).toBeNull();
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
describe("analysisProvenanceSchema", () => {
|
|
436
|
+
it("enforces sourceSchemaVersion literal", () => {
|
|
437
|
+
expect(() => analysisProvenanceSchema.parse(minimalProvenance)).not.toThrow();
|
|
438
|
+
expect(
|
|
439
|
+
analysisProvenanceSchema.safeParse({ ...minimalProvenance, sourceSchemaVersion: "cs2-demo-format/1.0" }).success
|
|
440
|
+
).toBe(false);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
describe("analysisBundleSchema", () => {
|
|
445
|
+
it("parses a minimal empty bundle", () => {
|
|
446
|
+
expect(() => analysisBundleSchema.parse(minimalBundle)).not.toThrow();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("enforces version literal", () => {
|
|
450
|
+
expect(
|
|
451
|
+
analysisBundleSchema.safeParse({ ...minimalBundle, version: "cs2-demo-analysis-kit/2.0" }).success
|
|
452
|
+
).toBe(false);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("enforces sourceSchemaVersion literal", () => {
|
|
456
|
+
expect(
|
|
457
|
+
analysisBundleSchema.safeParse({ ...minimalBundle, sourceSchemaVersion: "cs2-demo-format/1.0" }).success
|
|
458
|
+
).toBe(false);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
describe("playerScoreboardRowSchema", () => {
|
|
463
|
+
it("parses valid scoreboard row", () => {
|
|
464
|
+
expect(() => playerScoreboardRowSchema.parse(minimalScoreboardRow)).not.toThrow();
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("accepts null for optional stat fields", () => {
|
|
468
|
+
const result = playerScoreboardRowSchema.parse(minimalScoreboardRow);
|
|
469
|
+
expect(result.combatDeathCount).toBeNull();
|
|
470
|
+
expect(result.bombDeathCount).toBeNull();
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("rejects confidence outside [0,1]", () => {
|
|
474
|
+
expect(playerScoreboardRowSchema.safeParse({ ...minimalScoreboardRow, confidence: 1.1 }).success).toBe(false);
|
|
475
|
+
expect(playerScoreboardRowSchema.safeParse({ ...minimalScoreboardRow, confidence: -0.1 }).success).toBe(false);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("rejects invalid fieldAvailability richKills value", () => {
|
|
479
|
+
expect(
|
|
480
|
+
playerScoreboardRowSchema.safeParse({
|
|
481
|
+
...minimalScoreboardRow,
|
|
482
|
+
fieldAvailability: { ...minimalScoreboardRow.fieldAvailability, richKills: "unknown" },
|
|
483
|
+
}).success
|
|
484
|
+
).toBe(false);
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
489
|
+
// cohort.ts
|
|
490
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
491
|
+
|
|
492
|
+
describe("seasonCohortBundleSchema", () => {
|
|
493
|
+
const bundle = {
|
|
494
|
+
version: "cs2-demo-analysis-kit/cohort-1.0" as const,
|
|
495
|
+
matchCount: 3,
|
|
496
|
+
players: [],
|
|
497
|
+
weightsVersion: "value-accounts-v2-lite@0.1.0",
|
|
498
|
+
provenance: {
|
|
499
|
+
cohortVersion: "cs2-demo-analysis-kit/cohort-1.0" as const,
|
|
500
|
+
sourceSchemaVersion: "cs2-demo-format/2.0" as const,
|
|
501
|
+
matches: [{ matchId: "abc123", sourceDemoHash: null }],
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
it("parses a valid cohort bundle", () => {
|
|
506
|
+
expect(() => seasonCohortBundleSchema.parse(bundle)).not.toThrow();
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("enforces cohort version literal", () => {
|
|
510
|
+
expect(
|
|
511
|
+
seasonCohortBundleSchema.safeParse({ ...bundle, version: "cs2-demo-analysis-kit/cohort-2.0" }).success
|
|
512
|
+
).toBe(false);
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
517
|
+
// workspace.ts
|
|
518
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
519
|
+
|
|
520
|
+
describe("workspaceTabSchema", () => {
|
|
521
|
+
it("accepts all valid tab keys", () => {
|
|
522
|
+
for (const key of ["overview", "rounds", "players", "economy", "map", "replay"] as const) {
|
|
523
|
+
expect(() => workspaceTabSchema.parse({ key, label: "Label" })).not.toThrow();
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("rejects unknown tab key", () => {
|
|
528
|
+
expect(workspaceTabSchema.safeParse({ key: "stats", label: "Stats" }).success).toBe(false);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
describe("matchWorkspaceModelSchema", () => {
|
|
533
|
+
it("parses a minimal workspace model", () => {
|
|
534
|
+
expect(() => matchWorkspaceModelSchema.parse(minimalWorkspaceModel)).not.toThrow();
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it("enforces workspace version literal", () => {
|
|
538
|
+
expect(
|
|
539
|
+
matchWorkspaceModelSchema.safeParse({ ...minimalWorkspaceModel, version: "cs2-demo-analysis-kit/workspace-1.0" }).success
|
|
540
|
+
).toBe(false);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("enforces sourceSchemaVersion literal", () => {
|
|
544
|
+
expect(
|
|
545
|
+
matchWorkspaceModelSchema.safeParse({ ...minimalWorkspaceModel, sourceSchemaVersion: "cs2-demo-format/3.0" }).success
|
|
546
|
+
).toBe(false);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("replay nullable fields accept null when unavailable", () => {
|
|
550
|
+
const result = matchWorkspaceModelSchema.parse(minimalWorkspaceModel);
|
|
551
|
+
expect(result.replay.sampleRate).toBeNull();
|
|
552
|
+
expect(result.replay.tickrate).toBeNull();
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("map status message accepts null", () => {
|
|
556
|
+
const result = matchWorkspaceModelSchema.parse(minimalWorkspaceModel);
|
|
557
|
+
expect(result.map.status.message).toBeNull();
|
|
558
|
+
});
|
|
559
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
manifestSchema,
|
|
4
|
+
matchSchema,
|
|
5
|
+
playersSchema,
|
|
6
|
+
roundsSchema,
|
|
7
|
+
playerEconomiesSchema,
|
|
8
|
+
playerStatsSchema,
|
|
9
|
+
killsSchema,
|
|
10
|
+
damagesSchema,
|
|
11
|
+
blindsSchema,
|
|
12
|
+
bombsSchema,
|
|
13
|
+
grenadesSchema,
|
|
14
|
+
clutchesSchema,
|
|
15
|
+
shotsSchema,
|
|
16
|
+
positionsSchema,
|
|
17
|
+
replaySchema,
|
|
18
|
+
} from "cs2-demo-format";
|
|
19
|
+
|
|
20
|
+
export const demoPackageSchema = z.object({
|
|
21
|
+
manifest: manifestSchema,
|
|
22
|
+
match: matchSchema,
|
|
23
|
+
players: playersSchema,
|
|
24
|
+
rounds: roundsSchema,
|
|
25
|
+
playerEconomies: playerEconomiesSchema.default([]),
|
|
26
|
+
playerStats: playerStatsSchema.default([]),
|
|
27
|
+
kills: killsSchema.default([]),
|
|
28
|
+
damages: damagesSchema.default([]),
|
|
29
|
+
blinds: blindsSchema.default([]),
|
|
30
|
+
bombs: bombsSchema.default([]),
|
|
31
|
+
grenades: grenadesSchema.default([]),
|
|
32
|
+
clutches: clutchesSchema.default([]),
|
|
33
|
+
shots: shotsSchema.optional(),
|
|
34
|
+
positions1s: positionsSchema.optional(),
|
|
35
|
+
replay: replaySchema.optional(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type DemoPackage = z.infer<typeof demoPackageSchema>;
|