import { describe, it, expect } from 'vitest'; import { standardStages, floatStages, validateProfile, toV2Request, stageSummary, DEFAULT_FLIGHT_PARAMETERS } from '$domain'; import type { ProfileStage } from '$domain'; const params = { ...DEFAULT_FLIGHT_PARAMETERS, ascent_rate: 5, descent_rate: 6, burst_altitude: 30000 }; describe('presets', () => { it('expresses the standard flight as ascent → descent stages', () => { const stages = standardStages(params); expect(stages.map((s) => s.name)).toEqual(['ascent', 'descent']); expect(stages[0].solver).toMatchObject({ type: 'constant_rate', rate: 5 }); expect(stages[0].advanceWhen[0]).toMatchObject({ type: 'altitude', op: '>=', limit: 30000 }); // Descent ends on the ground, not at an altitude. expect(stages[1].solver).toMatchObject({ type: 'parachute_descent', sea_level_rate: 6 }); expect(stages[1].advanceWhen[0].type).toBe('terrain_contact'); }); it('floats by holding altitude on the wind solver', () => { const stages = floatStages(params); expect(stages[1].solver.type).toBe('wind'); expect(stages[1].advanceWhen[0].type).toBe('time'); }); }); describe('validateProfile', () => { it('accepts the standard preset', () => { expect(validateProfile(standardStages(params))).toEqual([]); }); it('rejects an empty profile', () => { expect(validateProfile([])).toEqual(['profileBuilder.errNoStages']); }); it('rejects a stage that can never end', () => { const stages = standardStages(params); stages[0].advanceWhen = []; expect(validateProfile(stages)).toContain('profileBuilder.errNoExit'); }); it('rejects a fallback pointing at a stage that does not exist', () => { const stages = standardStages(params); stages[0].abortIf = [ { type: 'time', op: '>=', limit: 100, action: 'fallback', fallback: 'nowhere' } ]; expect(validateProfile(stages)).toContain('profileBuilder.errMissingFallback'); stages[0].abortIf[0].fallback = 'descent'; expect(validateProfile(stages)).not.toContain('profileBuilder.errMissingFallback'); }); it('rejects a scalar constraint missing its operator or limit', () => { const stages = standardStages(params); stages[0].advanceWhen = [{ type: 'altitude', action: 'stop' }]; expect(validateProfile(stages)).toContain('profileBuilder.errIncompleteConstraint'); }); it('rejects duplicate and blank stage names', () => { const dup: ProfileStage[] = [...standardStages(params)]; dup[1] = { ...dup[1], name: 'ascent' }; expect(validateProfile(dup)).toContain('profileBuilder.errDuplicateName'); const blank = standardStages(params); blank[0].name = ' '; expect(validateProfile(blank)).toContain('profileBuilder.errUnnamedStage'); }); }); describe('toV2Request', () => { it('maps stages to the predictor contract', () => { const req = toV2Request(standardStages(params), params, '2026-01-01T12:00:00.000Z', { source: 'gfs-0p50-3h' }); expect(req.launch).toEqual({ time: '2026-01-01T12:00:00.000Z', latitude: params.launch_latitude, longitude: params.launch_longitude, altitude: params.launch_altitude }); expect(req.direction).toBe('forward'); expect(req.source).toBe('gfs-0p50-3h'); expect(req.profile[0].model).toEqual({ type: 'constant_rate', rate: 5, include_wind: true }); expect(req.profile[1].model).toEqual({ type: 'parachute_descent', sea_level_rate: 6, include_wind: true }); }); it('folds advance and abort constraints into one list, keeping their actions', () => { const stages = standardStages(params); stages[0].abortIf = [ { type: 'time', op: '>=', limit: 7200, action: 'fallback', fallback: 'descent' } ]; const req = toV2Request(stages, params, '2026-01-01T12:00:00.000Z'); expect(req.profile[0].constraints).toHaveLength(2); expect(req.profile[0].constraints[1]).toEqual({ type: 'time', op: '>=', limit: 7200, action: 'fallback', fallback: 'descent' }); }); it('omits absent optional fields rather than sending nulls', () => { const req = toV2Request(standardStages(params), params, '2026-01-01T12:00:00.000Z'); expect(req.profile[1].constraints[0]).toEqual({ type: 'terrain_contact', action: 'stop' }); expect('source' in req).toBe(false); }); it('maps curve points to piecewise segments', () => { const stages: ProfileStage[] = [ { name: 'ascent', solver: { type: 'piecewise', include_wind: true, segments: [{ order: 0, time_constraint: 0, alt_constraint: 0, rate: 5 }] }, advanceWhen: [{ type: 'terrain_contact', action: 'stop' }], abortIf: [] } ]; const req = toV2Request(stages, params, '2026-01-01T12:00:00.000Z'); expect(req.profile[0].model.segments).toEqual([ { reference: 'profile_start', time: 0, altitude: 0, rate: 5 } ]); }); }); describe('stageSummary', () => { it('summarises a stage in one line', () => { const [ascent, descent] = standardStages(params); expect(stageSummary(ascent)).toBe('ascent · 5 m/s → 30000 m'); expect(stageSummary(descent)).toBe('descent · 6 m/s → ground'); }); });