84 lines
3.4 KiB
TypeScript
84 lines
3.4 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import test from 'node:test';
|
|
import { fileURLToPath } from 'node:url';
|
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
import { FakeFlysim, type FakeScenario } from '../src/fake/simulator';
|
|
|
|
const here = fileURLToPath(new URL('.', import.meta.url));
|
|
const schema = JSON.parse(readFileSync(join(here, '../src/schema.json'), 'utf8')) as object;
|
|
|
|
const ajv = new Ajv2020({ strict: true });
|
|
const validate = ajv.compile(schema);
|
|
|
|
const SCENARIOS: FakeScenario[] = ['boot', 'running', 'stuck', 'milestone', 'shop', 'center'];
|
|
const SNAPSHOTS_PER_SCENARIO = 100;
|
|
|
|
for (const scenario of SCENARIOS) {
|
|
test(`fake flysim (${scenario}) produces schema-valid FeedHeaders for ${SNAPSHOTS_PER_SCENARIO} snapshots`, () => {
|
|
const sim = new FakeFlysim({ scenario, seed: 0xc0ffee });
|
|
|
|
for (let i = 0; i < SNAPSHOTS_PER_SCENARIO; i++) {
|
|
const { header } = sim.tick(33);
|
|
const valid = validate(header);
|
|
assert.ok(valid, `snapshot ${i} for scenario "${scenario}" failed schema validation: ${ajv.errorsText(validate.errors)}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
test('schema rejects a header with an unknown property', () => {
|
|
const sim = new FakeFlysim({ scenario: 'running', seed: 1 });
|
|
const { header } = sim.tick(33);
|
|
const invalid = { ...header, extraField: 'nope' };
|
|
assert.equal(validate(invalid), false);
|
|
});
|
|
|
|
test('schema rejects a bad status enum value', () => {
|
|
const sim = new FakeFlysim({ scenario: 'running', seed: 1 });
|
|
const { header } = sim.tick(33);
|
|
const invalid = { ...header, status: 'sleeping' };
|
|
assert.equal(validate(invalid), false);
|
|
});
|
|
|
|
test('milestone.total is the ladder length, emitted as 38 and optional in the schema', () => {
|
|
const sim = new FakeFlysim({ scenario: 'milestone', seed: 7 });
|
|
const { header } = sim.tick(33);
|
|
|
|
assert.equal(header.milestone.total, 38, 'the fake ladder is the real one\'s length');
|
|
assert.ok(header.milestone.rank >= 0);
|
|
assert.ok(
|
|
header.milestone.rank < (header.milestone.total as number),
|
|
'rank is 0..total-1',
|
|
);
|
|
|
|
// Optional, so a producer that predates it stays valid...
|
|
const { total, ...withoutTotal } = header.milestone;
|
|
assert.equal(validate({ ...header, milestone: withoutTotal }), true);
|
|
|
|
// ...but when present it is an integer of at least 1, and `rank` no longer has
|
|
// the old hard ceiling of 15, which the 38-rung ladder passes.
|
|
for (const bad of [0, -1, 1.5, '38', null]) {
|
|
assert.equal(
|
|
validate({ ...header, milestone: { ...header.milestone, total: bad } }),
|
|
false,
|
|
`total: ${JSON.stringify(bad)}`,
|
|
);
|
|
}
|
|
assert.equal(validate({ ...header, milestone: { ...header.milestone, rank: 37 } }), true);
|
|
});
|
|
|
|
test('the milestone scenario climbs the whole ladder and never reports a rank past it', () => {
|
|
const sim = new FakeFlysim({ scenario: 'milestone', seed: 3 });
|
|
let top = 0;
|
|
// One rung per 90 simulated seconds, so 37 climbs need about an hour. Ticked at
|
|
// 1 s rather than the wire's 33 ms to keep this under a second of real time; the
|
|
// scenario steps at most one rung per tick, so anything below 90 s is exact.
|
|
for (let i = 0; i < 4_000; i++) {
|
|
const { header } = sim.tick(1_000);
|
|
const { rank, total } = header.milestone;
|
|
assert.ok(total !== undefined && rank < total, `rank ${rank} of ${total}`);
|
|
top = Math.max(top, rank);
|
|
}
|
|
assert.equal(top, 37, 'the milestone scenario should reach the top rung');
|
|
});
|