Skip to content

Commit bf69f97

Browse files
committed
Add tests
1 parent 16f0553 commit bf69f97

File tree

5 files changed

+388
-64
lines changed

5 files changed

+388
-64
lines changed

src/data/derived.test.ts

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import { describe, it, expect } from "vitest";
2+
import {
3+
crossModuleName,
4+
declarationKey,
5+
allDeclarations,
6+
buildAllGameContexts,
7+
getGameContext,
8+
} from "./derived";
9+
import type { ParsedSchemas } from "./schemas";
10+
import type { Declaration } from "./types";
11+
import type { GameId } from "../games-list";
12+
import { parsedSchemas, buildTestContext } from "./test-helpers";
13+
14+
const ctx = buildTestContext();
15+
16+
// ==================== crossModuleName ====================
17+
18+
describe("crossModuleName", () => {
19+
it("maps C_ prefix to C (client → server)", () => {
20+
expect(crossModuleName("C_AK47")).toBe("CAK47");
21+
expect(crossModuleName("C_BaseEntity")).toBe("CBaseEntity");
22+
expect(crossModuleName("C_Fish")).toBe("CFish");
23+
});
24+
25+
it("maps C prefix to C_ (server → client)", () => {
26+
expect(crossModuleName("CAK47")).toBe("C_AK47");
27+
expect(crossModuleName("CBaseEntity")).toBe("C_BaseEntity");
28+
expect(crossModuleName("CFish")).toBe("C_Fish");
29+
});
30+
31+
it("returns null for names without C/C_ prefix", () => {
32+
expect(crossModuleName("BaseEntity")).toBeNull();
33+
expect(crossModuleName("sky3dparams_t")).toBeNull();
34+
expect(crossModuleName("")).toBeNull();
35+
});
36+
37+
it("handles single-letter C_ name", () => {
38+
expect(crossModuleName("C_X")).toBe("CX");
39+
expect(crossModuleName("CX")).toBe("C_X");
40+
});
41+
42+
it("handles C_ alone (edge case)", () => {
43+
expect(crossModuleName("C_")).toBe("C");
44+
});
45+
46+
it("is case sensitive — lowercase c is not mapped", () => {
47+
expect(crossModuleName("c_BaseEntity")).toBeNull();
48+
expect(crossModuleName("cBaseEntity")).toBeNull();
49+
});
50+
});
51+
52+
// ==================== declarationKey ====================
53+
54+
describe("declarationKey", () => {
55+
it("joins module and name with slash", () => {
56+
expect(declarationKey("client", "C_BaseEntity")).toBe("client/C_BaseEntity");
57+
expect(declarationKey("server", "CBaseEntity")).toBe("server/CBaseEntity");
58+
});
59+
});
60+
61+
// ==================== allDeclarations ====================
62+
63+
describe("allDeclarations", () => {
64+
it("yields all declarations across modules", () => {
65+
const all = [...allDeclarations(parsedSchemas.declarations)];
66+
expect(all.length).toBeGreaterThan(0);
67+
68+
const modules = new Set(all.map((d) => d.module));
69+
expect(modules.has("client")).toBe(true);
70+
expect(modules.has("server")).toBe(true);
71+
});
72+
73+
it("yields nothing for empty map", () => {
74+
const empty = new Map<string, Map<string, Declaration>>();
75+
expect([...allDeclarations(empty)]).toEqual([]);
76+
});
77+
});
78+
79+
// ==================== buildAllGameContexts with test-schemas.json ====================
80+
81+
describe("references", () => {
82+
it("records parent class references", () => {
83+
// C_RectLight extends C_BarnLight
84+
const refs = ctx.references.get(declarationKey("client", "C_BarnLight"));
85+
expect(refs).toBeDefined();
86+
const match = refs!.find((r) => r.declarationName === "C_RectLight" && r.relation === "class");
87+
expect(match).toBeDefined();
88+
expect(match!.declarationModule).toBe("client");
89+
});
90+
91+
it("records field type references", () => {
92+
// CFuncWater has field m_BuoyancyHelper of type client/CBuoyancyHelper
93+
const refs = ctx.references.get(declarationKey("client", "CBuoyancyHelper"));
94+
expect(refs).toBeDefined();
95+
const match = refs!.find(
96+
(r) => r.declarationName === "CFuncWater" && r.fieldName === "m_BuoyancyHelper",
97+
);
98+
expect(match).toBeDefined();
99+
expect(match!.relation).toBe("field");
100+
});
101+
102+
it("does not include self-references", () => {
103+
for (const [key, entries] of ctx.references) {
104+
for (const entry of entries) {
105+
const entryKey = declarationKey(entry.declarationModule, entry.declarationName);
106+
if (entry.relation === "field") {
107+
expect(entryKey).not.toBe(key);
108+
}
109+
}
110+
}
111+
});
112+
113+
it("records references across modules", () => {
114+
// C_Hostage has fields of type entity2/GameTime_t
115+
const refs = ctx.references.get(declarationKey("entity2", "GameTime_t"));
116+
expect(refs).toBeDefined();
117+
const match = refs!.find(
118+
(r) => r.declarationName === "C_Hostage" && r.declarationModule === "client",
119+
);
120+
expect(match).toBeDefined();
121+
});
122+
});
123+
124+
describe("cross-module lookup", () => {
125+
it("maps C_ client classes to server counterparts bidirectionally", () => {
126+
// C_PathParticleRope (client) <-> CPathParticleRope (server)
127+
const clientDecl = ctx.declarations.get("client")?.get("C_PathParticleRope");
128+
const serverDecl = ctx.declarations.get("server")?.get("CPathParticleRope");
129+
expect(clientDecl).toBeDefined();
130+
expect(serverDecl).toBeDefined();
131+
132+
expect(ctx.crossModuleLookup.get(declarationKey("client", "C_PathParticleRope"))).toBe(
133+
serverDecl,
134+
);
135+
expect(ctx.crossModuleLookup.get(declarationKey("server", "CPathParticleRope"))).toBe(
136+
clientDecl,
137+
);
138+
});
139+
140+
it("maps multiple C_ pairs", () => {
141+
// C_SoundAreaEntitySphere <-> CSoundAreaEntitySphere
142+
expect(ctx.crossModuleLookup.get(declarationKey("client", "C_SoundAreaEntitySphere"))).toBe(
143+
ctx.declarations.get("server")?.get("CSoundAreaEntitySphere"),
144+
);
145+
expect(ctx.crossModuleLookup.get(declarationKey("server", "CSoundAreaEntitySphere"))).toBe(
146+
ctx.declarations.get("client")?.get("C_SoundAreaEntitySphere"),
147+
);
148+
149+
// C_SoundEventSphereEntity <-> CSoundEventSphereEntity
150+
expect(ctx.crossModuleLookup.get(declarationKey("client", "C_SoundEventSphereEntity"))).toBe(
151+
ctx.declarations.get("server")?.get("CSoundEventSphereEntity"),
152+
);
153+
expect(ctx.crossModuleLookup.get(declarationKey("server", "CSoundEventSphereEntity"))).toBe(
154+
ctx.declarations.get("client")?.get("C_SoundEventSphereEntity"),
155+
);
156+
});
157+
158+
it("maps same-named classes across modules", () => {
159+
// CFuncWater exists in both client and server with the same name
160+
const clientDecl = ctx.declarations.get("client")?.get("CFuncWater");
161+
const serverDecl = ctx.declarations.get("server")?.get("CFuncWater");
162+
expect(clientDecl).toBeDefined();
163+
expect(serverDecl).toBeDefined();
164+
165+
expect(ctx.crossModuleLookup.get(declarationKey("client", "CFuncWater"))).toBe(serverDecl);
166+
expect(ctx.crossModuleLookup.get(declarationKey("server", "CFuncWater"))).toBe(clientDecl);
167+
});
168+
169+
it("maps same-named classes without C prefix (sky3dparams_t)", () => {
170+
const clientDecl = ctx.declarations.get("client")?.get("sky3dparams_t");
171+
const serverDecl = ctx.declarations.get("server")?.get("sky3dparams_t");
172+
expect(clientDecl).toBeDefined();
173+
expect(serverDecl).toBeDefined();
174+
175+
expect(ctx.crossModuleLookup.get(declarationKey("client", "sky3dparams_t"))).toBe(serverDecl);
176+
expect(ctx.crossModuleLookup.get(declarationKey("server", "sky3dparams_t"))).toBe(clientDecl);
177+
});
178+
179+
it("does not map client-only classes", () => {
180+
// C_Fish is only in client, no server counterpart CFish
181+
expect(ctx.declarations.get("server")?.has("CFish")).toBeFalsy();
182+
expect(ctx.crossModuleLookup.get(declarationKey("client", "C_Fish"))).toBeUndefined();
183+
});
184+
185+
it("does not map server-only classes", () => {
186+
// CPointHurt is only in server, no client counterpart C_PointHurt
187+
expect(ctx.declarations.get("client")?.has("C_PointHurt")).toBeFalsy();
188+
expect(ctx.crossModuleLookup.get(declarationKey("server", "CPointHurt"))).toBeUndefined();
189+
});
190+
191+
it("does not map classes from non-client/server modules", () => {
192+
expect(ctx.crossModuleLookup.get(declarationKey("navlib", "CNavVolumeSphere"))).toBeUndefined();
193+
expect(
194+
ctx.crossModuleLookup.get(declarationKey("particles", "C_OP_RenderTreeShake")),
195+
).toBeUndefined();
196+
});
197+
});
198+
199+
describe("context structure", () => {
200+
it("stores declarations grouped by module", () => {
201+
expect(ctx.declarations.has("client")).toBe(true);
202+
expect(ctx.declarations.has("server")).toBe(true);
203+
expect(ctx.declarations.get("client")?.has("C_BaseEntity")).toBe(true);
204+
});
205+
206+
it("stores metadata from parsed schemas", () => {
207+
expect(ctx.metadata).toEqual({ revision: 0, versionDate: "test", versionTime: "test" });
208+
});
209+
210+
it("stores error as null when no error", () => {
211+
expect(ctx.error).toBeNull();
212+
});
213+
214+
it("stores error message when provided", () => {
215+
const loaded = new Map<GameId, ParsedSchemas>();
216+
loaded.set("cs2", parsedSchemas);
217+
const errors = new Map<GameId, string>();
218+
errors.set("cs2", "something went wrong");
219+
buildAllGameContexts(loaded, errors);
220+
221+
const ctx = getGameContext("cs2");
222+
expect(ctx.error).toBe("something went wrong");
223+
});
224+
225+
it("creates contexts for games without loaded data", () => {
226+
const loaded = new Map<GameId, ParsedSchemas>();
227+
loaded.set("cs2", parsedSchemas);
228+
buildAllGameContexts(loaded, new Map());
229+
230+
const dota = getGameContext("dota2");
231+
expect(dota).toBeDefined();
232+
expect(dota.declarations.size).toBe(0);
233+
expect(dota.crossModuleLookup.size).toBe(0);
234+
});
235+
});
236+
237+
describe("otherGamesLookup", () => {
238+
it("maps declarations from other games by name", () => {
239+
const loaded = new Map<GameId, ParsedSchemas>();
240+
loaded.set("cs2", parsedSchemas);
241+
loaded.set("dota2", parsedSchemas);
242+
buildAllGameContexts(loaded, new Map());
243+
244+
const cs2 = getGameContext("cs2");
245+
const dota2Lookup = cs2.otherGamesLookup.get("dota2");
246+
expect(dota2Lookup).toBeDefined();
247+
expect(dota2Lookup!.has("C_BaseEntity")).toBe(true);
248+
});
249+
250+
it("does not include same game in lookup", () => {
251+
expect(ctx.otherGamesLookup.has("cs2")).toBe(false);
252+
});
253+
});

src/data/derived.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ function buildReferences(
100100
}
101101

102102
// Client classes use C_ prefix (e.g. C_BaseEntity), server uses C (e.g. CBaseEntity)
103-
function crossModuleName(name: string): string | null {
103+
export function crossModuleName(name: string): string | null {
104104
if (name.startsWith("C_")) return "C" + name.slice(2);
105105
if (name.startsWith("C") && name[1] !== "_") return "C_" + name.slice(1);
106106
return null;

0 commit comments

Comments
 (0)