@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.
@@ -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>;