|
| 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 | +}); |
0 commit comments