CI / build + unit + e2e (pull_request) Successful in 2m38s
Production freeze reported after the v0.9.3 (#21) deploy: every API route hung and all panels stayed at LOADING. Root cause was not the #21 layout (verified healthy across all 7 tabs/viewports) but makeName's unbounded rejection loop over a finite 16x8=128 system-name pool — once a save grew past ~128 systems, any jump into an undiscovered system spun forever on Node's single thread (sql.js is synchronous), blocking the whole process. makeName and makeCrewName now fall back to numbered variants ('Kepler Drift 2') once their pools are exhausted, so they always terminate.
88 lines
3.6 KiB
TypeScript
88 lines
3.6 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
import { CREW_CHANCE, generateStartingCrew, makeCrewName, rollCrewMember, startingCrewRng } from './crew'
|
||
import { explorationRng } from './world'
|
||
|
||
describe('generateStartingCrew', () => {
|
||
it('gives a new game 2–3 members, each with exactly one good and one bad trait', () => {
|
||
for (const seed of [1, 7, 42, 999]) {
|
||
const crew = generateStartingCrew(startingCrewRng(String(seed)))
|
||
expect(crew.length).toBeGreaterThanOrEqual(2)
|
||
expect(crew.length).toBeLessThanOrEqual(3)
|
||
|
||
for (const member of crew) {
|
||
expect(member.name.trim().length).toBeGreaterThan(0)
|
||
expect(member.role.trim().length).toBeGreaterThan(0)
|
||
expect(member.traitGood.trim().length).toBeGreaterThan(0)
|
||
expect(member.traitBad.trim().length).toBeGreaterThan(0)
|
||
}
|
||
// Names are unique within the crew; roles too (drawn without replacement).
|
||
expect(new Set(crew.map((m) => m.name)).size).toBe(crew.length)
|
||
expect(new Set(crew.map((m) => m.role)).size).toBe(crew.length)
|
||
}
|
||
})
|
||
|
||
it('is deterministic under a fixed seed and differs across seeds', () => {
|
||
const a = generateStartingCrew(startingCrewRng('42'))
|
||
expect(generateStartingCrew(startingCrewRng('42'))).toEqual(a) // same save → same crew
|
||
const b = generateStartingCrew(startingCrewRng('43'))
|
||
expect(JSON.stringify(b)).not.toBe(JSON.stringify(a)) // different seed → different crew
|
||
})
|
||
})
|
||
|
||
describe('rollCrewMember', () => {
|
||
it('finds a member with the documented probability and never reuses a taken name', () => {
|
||
const rng = startingCrewRng('42')
|
||
let found = 0
|
||
for (let i = 0; i < 1000; i++) found += rollCrewMember(rng, new Set<string>()) ? 1 : 0
|
||
// 18% ± a comfortable band — catches an inverted or missing chance without flaking.
|
||
expect(found).toBeGreaterThan(90)
|
||
expect(found).toBeLessThan(270)
|
||
|
||
const taken = new Set(['Ava Vance'])
|
||
for (let i = 0; i < 50; i++) {
|
||
const member = rollCrewMember(startingCrewRng(String(i)), taken)
|
||
if (member) expect(member.name).not.toBe('Ava Vance') // never reuses a roster name
|
||
}
|
||
})
|
||
|
||
it('is deterministic per save+planet when drawn from the exploration stream', () => {
|
||
const first = rollCrewMember(explorationRng('42', 13), new Set<string>())
|
||
expect(rollCrewMember(explorationRng('42', 13), new Set<string>())).toEqual(first) // same save+planet → same result
|
||
|
||
// Across many planets both outcomes occur (someone is found, someone isn't).
|
||
let found = 0
|
||
for (let planetId = 1; planetId <= 200; planetId++) {
|
||
if (rollCrewMember(explorationRng('42', planetId), new Set<string>())) found++
|
||
}
|
||
expect(found).toBeGreaterThan(0)
|
||
expect(found).toBeLessThan(200)
|
||
})
|
||
|
||
it('keeps the crew chance documented and sane', () => {
|
||
expect(CREW_CHANCE).toBeGreaterThanOrEqual(0.15)
|
||
expect(CREW_CHANCE).toBeLessThanOrEqual(0.2)
|
||
})
|
||
})
|
||
|
||
describe('makeCrewName', () => {
|
||
it('never reuses a taken name and always terminates', () => {
|
||
const rng = startingCrewRng('7')
|
||
const taken = new Set<string>()
|
||
for (let i = 0; i < 40; i++) makeCrewName(rng, taken)
|
||
expect(taken.size).toBe(40)
|
||
})
|
||
|
||
it('terminates with numbered variants once all 352 base names are taken', () => {
|
||
const rng = startingCrewRng('7')
|
||
const taken = new Set<string>()
|
||
for (let i = 0; i < 352; i++) makeCrewName(rng, taken) // exhaust the whole pool
|
||
expect(taken.size).toBe(352)
|
||
|
||
const a = makeCrewName(rng, taken) // must return instead of spinning forever
|
||
const b = makeCrewName(rng, taken)
|
||
expect(a).not.toBe(b)
|
||
expect(taken.has(a)).toBe(true)
|
||
expect(taken.has(b)).toBe(true)
|
||
})
|
||
})
|