import { describe, it, expect } from 'vitest'; import { ServiceCache, type RawServicesMessage } from '../src/services.js'; /** * A small hand-crafted services message that mirrors the shape we'd * get from KRPC.GetServices() on a real KSP install. Just enough * services/procedures/enums to exercise the cache. */ const SAMPLE_RAW: RawServicesMessage = { services: [ { name: 'SpaceCenter', procedures: [ { name: 'GetUT', parameters: [], returnType: { code: 1, service: '', name: '', types: [] }, // DOUBLE returnIsNullable: false, }, { name: 'GetBodies', parameters: [], returnType: { code: 301, // LIST service: '', name: '', types: [{ code: 100, service: 'SpaceCenter', name: 'CelestialBody', types: [] }], }, returnIsNullable: false, }, { name: 'CelestialBody.GetName', parameters: [ { name: 'self', type: { code: 100, service: 'SpaceCenter', name: 'CelestialBody', types: [] }, nullable: false, }, ], returnType: { code: 8, service: '', name: '', types: [] }, // STRING returnIsNullable: false, }, { name: 'CelestialBody.GetParent', parameters: [ { name: 'self', type: { code: 100, service: 'SpaceCenter', name: 'CelestialBody', types: [] }, nullable: false, }, ], // Nullable CelestialBody (i.e. Sun has no parent). returnType: { code: 100, service: 'SpaceCenter', name: 'CelestialBody', types: [] }, returnIsNullable: true, }, ], classes: [ { name: 'CelestialBody' }, { name: 'Vessel' }, { name: 'Orbit' }, ], enumerations: [ { name: 'VesselType', values: [ { name: 'Ship', value: 0 }, { name: 'Station', value: 1 }, { name: 'Probe', value: 3 }, { name: 'Debris', value: 8 }, ], }, { name: 'VesselSituation', values: [ { name: 'PreLaunch', value: 0 }, { name: 'Orbiting', value: 1 }, { name: 'Escaping', value: 2 }, { name: 'Landed', value: 4 }, { name: 'Splashed', value: 5 }, ], }, ], }, { name: 'KRPC', procedures: [ { name: 'GetStatus', parameters: [], returnType: { code: 203, service: '', name: '', types: [] }, // STATUS returnIsNullable: false, }, ], classes: [], enumerations: [], }, ], }; describe('ServiceCache', () => { it('builds from a raw services message', () => { const cache = new ServiceCache(SAMPLE_RAW); expect(cache.procedureCount()).toBe(5); expect(cache.serviceNames()).toEqual(['KRPC', 'SpaceCenter']); }); it('looks up a top-level procedure', () => { const cache = new ServiceCache(SAMPLE_RAW); const r = cache.lookup('SpaceCenter', 'GetUT'); expect(r.found).toBe(true); if (!r.found) throw new Error('unreachable'); expect(r.info.service).toBe('SpaceCenter'); expect(r.info.name).toBe('GetUT'); expect(r.info.returnType.code).toBe(1); // DOUBLE expect(r.info.returnIsNullable).toBe(false); expect(r.info.parameters).toHaveLength(0); }); it('looks up a class-prefixed procedure', () => { const cache = new ServiceCache(SAMPLE_RAW); const r = cache.lookup('SpaceCenter', 'CelestialBody.GetName'); expect(r.found).toBe(true); if (!r.found) throw new Error('unreachable'); expect(r.info.parameters).toHaveLength(1); expect(r.info.parameters[0]?.name).toBe('self'); expect(r.info.parameters[0]?.type.code).toBe(100); // CLASS expect(r.info.parameters[0]?.type.name).toBe('CelestialBody'); expect(r.info.returnType.code).toBe(8); // STRING }); it('returns not-found for an unknown procedure', () => { const cache = new ServiceCache(SAMPLE_RAW); expect(cache.lookup('SpaceCenter', 'GetNothing').found).toBe(false); expect(cache.lookup('Nope', 'X').found).toBe(false); }); it('returns not-found for an unknown service', () => { const cache = new ServiceCache(SAMPLE_RAW); expect(cache.lookup('KerbalAlarmClock', 'GetAlarms').found).toBe(false); }); it('records returnIsNullable for nullable CLASS returns', () => { const cache = new ServiceCache(SAMPLE_RAW); const r = cache.lookup('SpaceCenter', 'CelestialBody.GetParent'); expect(r.found).toBe(true); if (!r.found) throw new Error('unreachable'); expect(r.info.returnIsNullable).toBe(true); expect(r.info.returnType.code).toBe(100); expect(r.info.returnType.name).toBe('CelestialBody'); }); it('resolves enum values by name', () => { const cache = new ServiceCache(SAMPLE_RAW); expect(cache.getEnumValue('SpaceCenter', 'VesselType', 'Ship')).toBe(0); expect(cache.getEnumValue('SpaceCenter', 'VesselType', 'Station')).toBe(1); expect(cache.getEnumValue('SpaceCenter', 'VesselType', 'Probe')).toBe(3); expect(cache.getEnumValue('SpaceCenter', 'VesselSituation', 'Orbiting')).toBe(1); }); it('throws for unknown enum or enum value', () => { const cache = new ServiceCache(SAMPLE_RAW); expect(() => cache.getEnumValue('SpaceCenter', 'NoSuch', 'X')).toThrow(/unknown enum/); expect(() => cache.getEnumValue('SpaceCenter', 'VesselType', 'Hovercraft')).toThrow( /unknown value/, ); }); it('resolves enum value names by code', () => { const cache = new ServiceCache(SAMPLE_RAW); expect(cache.getEnumName('SpaceCenter', 'VesselType', 0)).toBe('Ship'); expect(cache.getEnumName('SpaceCenter', 'VesselType', 3)).toBe('Probe'); expect(cache.getEnumName('SpaceCenter', 'VesselType', 999)).toBeNull(); }); it('lists enum value names', () => { const cache = new ServiceCache(SAMPLE_RAW); const names = cache.getEnumNames('SpaceCenter', 'VesselType'); expect(names).toEqual(['Debris', 'Probe', 'Ship', 'Station']); // sorted }); it('lists procedures in a service', () => { const cache = new ServiceCache(SAMPLE_RAW); const procs = cache.proceduresInService('SpaceCenter'); expect(procs).toContain('SpaceCenter.GetUT'); expect(procs).toContain('SpaceCenter.CelestialBody.GetName'); expect(procs).toContain('SpaceCenter.CelestialBody.GetParent'); }); it('preserves nested list type info', () => { const cache = new ServiceCache(SAMPLE_RAW); const r = cache.lookup('SpaceCenter', 'GetBodies'); expect(r.found).toBe(true); if (!r.found) throw new Error('unreachable'); expect(r.info.returnType.code).toBe(301); // LIST expect(r.info.returnType.types).toHaveLength(1); expect(r.info.returnType.types[0]?.code).toBe(100); expect(r.info.returnType.types[0]?.name).toBe('CelestialBody'); }); });