Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -1,54 +1,126 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { CollectionPreview, LaunchAvailability } from '../../src/lib/deployment-plan.js';
|
||||
import { zombieMapBySlug, type ZombieGame } from '../../src/lib/map-aliases.js';
|
||||
import { collectionCapacitySummary, collectionKeyCoverage, collectionMapGroups } from '../../src/lib/ui/collection-summary.js';
|
||||
import {
|
||||
collectionCapacitySummary,
|
||||
collectionKeyCoverage,
|
||||
collectionMapGroups
|
||||
} from '../../src/lib/ui/collection-summary.js';
|
||||
|
||||
function entry(game: ZombieGame, mapSlug: string, count: number, existingServers: string[] = []): CollectionPreview['entries'][number] {
|
||||
function entry(
|
||||
game: ZombieGame,
|
||||
mapSlug: string,
|
||||
count: number,
|
||||
existingServers: string[] = []
|
||||
): CollectionPreview['entries'][number] {
|
||||
const map = zombieMapBySlug(game, mapSlug)!;
|
||||
return { count, existing: existingServers.length, missing: Math.max(0, count - existingServers.length), existingServers,
|
||||
coverage: existingServers.map(name => ({ name, health: 'stopped', href: `/servers/${name}` })),
|
||||
recipe: { game, mapSlug, mapName: map.friendly, expectedMap: map.canonical, capturedAt: '', image: 'runtime:latest',
|
||||
configFile: 'server.cfg', configSource: 'Current setup', files: [], fingerprint: '' } };
|
||||
return {
|
||||
count,
|
||||
existing: existingServers.length,
|
||||
missing: Math.max(0, count - existingServers.length),
|
||||
existingServers,
|
||||
coverage: existingServers.map((name) => ({
|
||||
name,
|
||||
health: 'stopped',
|
||||
href: `/servers/${name}`
|
||||
})),
|
||||
recipe: {
|
||||
game,
|
||||
mapSlug,
|
||||
mapName: map.friendly,
|
||||
expectedMap: map.canonical,
|
||||
capturedAt: '',
|
||||
image: 'runtime:latest',
|
||||
configFile: 'server.cfg',
|
||||
configSource: 'Current setup',
|
||||
files: [],
|
||||
fingerprint: ''
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function preview(overrides: Partial<CollectionPreview> = {}): CollectionPreview {
|
||||
return { id: 'preview', expiresAt: '2030-01-01T00:00:00Z', blueprintName: 'Collection', launchConcurrency: 1,
|
||||
mode: 'fill', retainedServers: [], removals: [], entries: [], servers: [], blockers: [], ready: true, ...overrides };
|
||||
return {
|
||||
id: 'preview',
|
||||
expiresAt: '2030-01-01T00:00:00Z',
|
||||
blueprintName: 'Collection',
|
||||
launchConcurrency: 1,
|
||||
mode: 'fill',
|
||||
retainedServers: [],
|
||||
removals: [],
|
||||
entries: [],
|
||||
servers: [],
|
||||
blockers: [],
|
||||
ready: true,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
function capacity(overrides: Partial<LaunchAvailability> = {}): LaunchAvailability {
|
||||
return { requested: 5, estimatedAvailable: null, updatedAt: null, portsAvailable: null, cpuCoresAvailable: null,
|
||||
memoryBytesAvailable: null, startupSlots: null, startupLimit: null, queued: null, ...overrides };
|
||||
return {
|
||||
requested: 5,
|
||||
estimatedAvailable: null,
|
||||
updatedAt: null,
|
||||
portsAvailable: null,
|
||||
cpuCoresAvailable: null,
|
||||
memoryBytesAvailable: null,
|
||||
startupSlots: null,
|
||||
startupLimit: null,
|
||||
queued: null,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
describe('collection key coverage', () => {
|
||||
it('counts coverage separately by game without treating removals as available keys', () => {
|
||||
const plan = preview({ mode: 'exact', entries: [entry('t5', 'theater', 2), entry('t6', 'town', 1)],
|
||||
removals: [{ name: 't5_zombie_moon_1', reason: 'Outside collection' }], servers: [
|
||||
const plan = preview({
|
||||
mode: 'exact',
|
||||
entries: [entry('t5', 'theater', 2), entry('t6', 'town', 1)],
|
||||
removals: [{ name: 't5_zombie_moon_1', reason: 'Outside collection' }],
|
||||
servers: [
|
||||
{ entryIndex: 0, name: 't5_zombie_theater_1', hasSavedKey: true, port: null },
|
||||
{ entryIndex: 0, name: 't5_zombie_theater_2', hasSavedKey: false, port: null },
|
||||
{ entryIndex: 1, name: 't6_zombie_town_1', hasSavedKey: true, port: null },
|
||||
] });
|
||||
{ entryIndex: 1, name: 't6_zombie_town_1', hasSavedKey: true, port: null }
|
||||
]
|
||||
});
|
||||
expect(collectionKeyCoverage(plan)).toEqual([
|
||||
{ game: 't5', label: 'Black Ops I', required: 2, available: 1, unassigned: 0 },
|
||||
{ game: 't6', label: 'Black Ops II', required: 1, available: 1, unassigned: 0 },
|
||||
{ game: 't6', label: 'Black Ops II', required: 1, available: 1, unassigned: 0 }
|
||||
]);
|
||||
});
|
||||
|
||||
it('counts an unavailable lobby as required but never covered, and omits games without creations', () => {
|
||||
expect(collectionKeyCoverage(preview({ entries: [entry('t5', 'theater', 1), entry('t6', 'town', 1)],
|
||||
servers: [{ entryIndex: 1, name: null, hasSavedKey: true, port: null }] }))).toEqual([
|
||||
{ game: 't6', label: 'Black Ops II', required: 1, available: 0, unassigned: 1 },
|
||||
expect(
|
||||
collectionKeyCoverage(
|
||||
preview({
|
||||
entries: [entry('t5', 'theater', 1), entry('t6', 'town', 1)],
|
||||
servers: [{ entryIndex: 1, name: null, hasSavedKey: true, port: null }]
|
||||
})
|
||||
)
|
||||
).toEqual([
|
||||
{ game: 't6', label: 'Black Ops II', required: 1, available: 0, unassigned: 1 }
|
||||
]);
|
||||
expect(collectionKeyCoverage(preview({ mode: 'exact', removals: [{ name: 't5_zombie_theater_1', reason: 'Remove' }] }))).toEqual([]);
|
||||
expect(
|
||||
collectionKeyCoverage(
|
||||
preview({
|
||||
mode: 'exact',
|
||||
removals: [{ name: 't5_zombie_theater_1', reason: 'Remove' }]
|
||||
})
|
||||
)
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('distinguishes known key shortages from creations waiting for a lobby', () => {
|
||||
const [pool] = collectionKeyCoverage(preview({ entries: [entry('t5', 'theater', 3)], servers: [
|
||||
{ entryIndex: 0, name: 't5_zombie_theater_19', hasSavedKey: true, port: null },
|
||||
{ entryIndex: 0, name: 't5_zombie_theater_20', hasSavedKey: false, port: null },
|
||||
{ entryIndex: 0, name: null, hasSavedKey: false, port: null },
|
||||
] }));
|
||||
const [pool] = collectionKeyCoverage(
|
||||
preview({
|
||||
entries: [entry('t5', 'theater', 3)],
|
||||
servers: [
|
||||
{ entryIndex: 0, name: 't5_zombie_theater_19', hasSavedKey: true, port: null },
|
||||
{ entryIndex: 0, name: 't5_zombie_theater_20', hasSavedKey: false, port: null },
|
||||
{ entryIndex: 0, name: null, hasSavedKey: false, port: null }
|
||||
]
|
||||
})
|
||||
);
|
||||
expect(pool).toMatchObject({ required: 3, available: 1, unassigned: 1 });
|
||||
expect(pool.required - pool.unassigned - pool.available).toBe(1);
|
||||
});
|
||||
@@ -56,74 +128,224 @@ describe('collection key coverage', () => {
|
||||
|
||||
describe('collection map summaries', () => {
|
||||
it('preserves full existing counts in fill mode, including stopped inventory and excess lobbies', () => {
|
||||
const plan = preview({ entries: [entry('t5', 'theater', 1, ['t5_zombie_theater_1', 't5_zombie_theater_2']), entry('t5', 'moon', 2)] });
|
||||
expect(collectionMapGroups(plan)).toEqual([{ game: 't5', label: 'Black Ops I', rows: [
|
||||
{ key: 't5:theater', mapName: 'Kino Der Toten', target: 1, existing: 2, create: 0, remove: 0 },
|
||||
{ key: 't5:moon', mapName: 'Moon', target: 2, existing: 0, create: 2, remove: 0 },
|
||||
] }]);
|
||||
const plan = preview({
|
||||
entries: [
|
||||
entry('t5', 'theater', 1, ['t5_zombie_theater_1', 't5_zombie_theater_2']),
|
||||
entry('t5', 'moon', 2)
|
||||
]
|
||||
});
|
||||
expect(collectionMapGroups(plan)).toEqual([
|
||||
{
|
||||
game: 't5',
|
||||
label: 'Black Ops I',
|
||||
rows: [
|
||||
{
|
||||
key: 't5:theater',
|
||||
mapName: 'Kino Der Toten',
|
||||
target: 1,
|
||||
existing: 2,
|
||||
create: 0,
|
||||
remove: 0
|
||||
},
|
||||
{
|
||||
key: 't5:moon',
|
||||
mapName: 'Moon',
|
||||
target: 2,
|
||||
existing: 0,
|
||||
create: 2,
|
||||
remove: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('counts ordinary excess once and adds already-pending removals missing from backend coverage', () => {
|
||||
const plan = preview({ mode: 'exact', entries: [entry('t5', 'theater', 1, ['t5_zombie_theater_1', 't5_zombie_theater_2'])],
|
||||
retainedServers: ['t5_zombie_theater_1'], removals: [
|
||||
const plan = preview({
|
||||
mode: 'exact',
|
||||
entries: [entry('t5', 'theater', 1, ['t5_zombie_theater_1', 't5_zombie_theater_2'])],
|
||||
retainedServers: ['t5_zombie_theater_1'],
|
||||
removals: [
|
||||
{ name: 't5_zombie_theater_2', reason: 'Excess' },
|
||||
{ name: 't5_zombie_theater_3', reason: 'Already awaiting removal' },
|
||||
] });
|
||||
{ name: 't5_zombie_theater_3', reason: 'Already awaiting removal' }
|
||||
]
|
||||
});
|
||||
expect(collectionMapGroups(plan)[0].rows).toEqual([
|
||||
{ key: 't5:theater', mapName: 'Kino Der Toten', target: 1, existing: 3, create: 0, remove: 2 },
|
||||
{
|
||||
key: 't5:theater',
|
||||
mapName: 'Kino Der Toten',
|
||||
target: 1,
|
||||
existing: 3,
|
||||
create: 0,
|
||||
remove: 2
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('shows outside maps with target zero across both games without combining T6 variants', () => {
|
||||
const plan = preview({ mode: 'exact', entries: [entry('t6', 'town', 1, ['t6_zombie_town_1'])],
|
||||
retainedServers: ['t6_zombie_town_1'], removals: [
|
||||
const plan = preview({
|
||||
mode: 'exact',
|
||||
entries: [entry('t6', 'town', 1, ['t6_zombie_town_1'])],
|
||||
retainedServers: ['t6_zombie_town_1'],
|
||||
removals: [
|
||||
{ name: 't5_zombie_moon_1', reason: 'Outside collection' },
|
||||
{ name: 't6_zombie_farm_1', reason: 'Outside collection' },
|
||||
{ name: 't6_zombie_bus_depot_survival_1', reason: 'Outside collection' },
|
||||
] });
|
||||
{ name: 't6_zombie_bus_depot_survival_1', reason: 'Outside collection' }
|
||||
]
|
||||
});
|
||||
expect(collectionMapGroups(plan)).toEqual([
|
||||
{ game: 't5', label: 'Black Ops I', rows: [{ key: 't5:moon', mapName: 'Moon', target: 0, existing: 1, create: 0, remove: 1 }] },
|
||||
{ game: 't6', label: 'Black Ops II', rows: [
|
||||
{ key: 't6:town', mapName: 'Town Survival', target: 1, existing: 1, create: 0, remove: 0 },
|
||||
{ key: 't6:farm', mapName: 'Farm Survival', target: 0, existing: 1, create: 0, remove: 1 },
|
||||
{ key: 't6:busdepot', mapName: 'Bus Depot Survival', target: 0, existing: 1, create: 0, remove: 1 },
|
||||
] },
|
||||
{
|
||||
game: 't5',
|
||||
label: 'Black Ops I',
|
||||
rows: [
|
||||
{
|
||||
key: 't5:moon',
|
||||
mapName: 'Moon',
|
||||
target: 0,
|
||||
existing: 1,
|
||||
create: 0,
|
||||
remove: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
game: 't6',
|
||||
label: 'Black Ops II',
|
||||
rows: [
|
||||
{
|
||||
key: 't6:town',
|
||||
mapName: 'Town Survival',
|
||||
target: 1,
|
||||
existing: 1,
|
||||
create: 0,
|
||||
remove: 0
|
||||
},
|
||||
{
|
||||
key: 't6:farm',
|
||||
mapName: 'Farm Survival',
|
||||
target: 0,
|
||||
existing: 1,
|
||||
create: 0,
|
||||
remove: 1
|
||||
},
|
||||
{
|
||||
key: 't6:busdepot',
|
||||
mapName: 'Bus Depot Survival',
|
||||
target: 0,
|
||||
existing: 1,
|
||||
create: 0,
|
||||
remove: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses authoritative coverage before names and resolves friendly legacy aliases', () => {
|
||||
const plan = preview({ mode: 'exact', entries: [entry('t6', 'town', 1, ['custom-town', 't6_zombie_town_1']), entry('t5', 'theater', 1, ['t5_zombie_theater_1'])],
|
||||
removals: [{ name: 'custom-town', reason: 'Excess' }, { name: 'T5_ZOMBIE_KINO_DER_TOTEN_2', reason: 'Awaiting removal' }] });
|
||||
expect(collectionMapGroups(plan).map(group => group.rows)).toEqual([
|
||||
[{ key: 't5:theater', mapName: 'Kino Der Toten', target: 1, existing: 2, create: 0, remove: 1 }],
|
||||
[{ key: 't6:town', mapName: 'Town Survival', target: 1, existing: 2, create: 0, remove: 1 }],
|
||||
const plan = preview({
|
||||
mode: 'exact',
|
||||
entries: [
|
||||
entry('t6', 'town', 1, ['custom-town', 't6_zombie_town_1']),
|
||||
entry('t5', 'theater', 1, ['t5_zombie_theater_1'])
|
||||
],
|
||||
removals: [
|
||||
{ name: 'custom-town', reason: 'Excess' },
|
||||
{ name: 'T5_ZOMBIE_KINO_DER_TOTEN_2', reason: 'Awaiting removal' }
|
||||
]
|
||||
});
|
||||
expect(collectionMapGroups(plan).map((group) => group.rows)).toEqual([
|
||||
[
|
||||
{
|
||||
key: 't5:theater',
|
||||
mapName: 'Kino Der Toten',
|
||||
target: 1,
|
||||
existing: 2,
|
||||
create: 0,
|
||||
remove: 1
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
key: 't6:town',
|
||||
mapName: 'Town Survival',
|
||||
target: 1,
|
||||
existing: 2,
|
||||
create: 0,
|
||||
remove: 1
|
||||
}
|
||||
]
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps unknown removals visible without guessing a shared engine-map variant', () => {
|
||||
const plan = preview({ mode: 'exact', removals: [
|
||||
{ name: 't6_zombie_zm_transit_1', reason: 'Unknown variant' },
|
||||
{ name: 'custom-instance', reason: 'Outside collection' },
|
||||
] });
|
||||
const plan = preview({
|
||||
mode: 'exact',
|
||||
removals: [
|
||||
{ name: 't6_zombie_zm_transit_1', reason: 'Unknown variant' },
|
||||
{ name: 'custom-instance', reason: 'Outside collection' }
|
||||
]
|
||||
});
|
||||
expect(collectionMapGroups(plan)).toEqual([
|
||||
{ game: 't6', label: 'Black Ops II', rows: [{ key: 't6:server:t6_zombie_zm_transit_1', mapName: 't6_zombie_zm_transit_1', target: 0, existing: 1, create: 0, remove: 1 }] },
|
||||
{ game: null, label: 'Other servers', rows: [{ key: 'other:server:custom-instance', mapName: 'custom-instance', target: 0, existing: 1, create: 0, remove: 1 }] },
|
||||
{
|
||||
game: 't6',
|
||||
label: 'Black Ops II',
|
||||
rows: [
|
||||
{
|
||||
key: 't6:server:t6_zombie_zm_transit_1',
|
||||
mapName: 't6_zombie_zm_transit_1',
|
||||
target: 0,
|
||||
existing: 1,
|
||||
create: 0,
|
||||
remove: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
game: null,
|
||||
label: 'Other servers',
|
||||
rows: [
|
||||
{
|
||||
key: 'other:server:custom-instance',
|
||||
mapName: 'custom-instance',
|
||||
target: 0,
|
||||
existing: 1,
|
||||
create: 0,
|
||||
remove: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collection capacity summary', () => {
|
||||
it('keeps unknown and insufficient capacity queueable', () => {
|
||||
expect(collectionCapacitySummary()).toBe('Capacity estimate unavailable; launches can wait for capacity.');
|
||||
expect(collectionCapacitySummary(capacity())).toBe('Capacity estimate unavailable; launches can wait for capacity.');
|
||||
expect(collectionCapacitySummary(capacity({ estimatedAvailable: 0 }))).toBe('Estimated capacity for 0 of 5 servers; launches can wait for capacity.');
|
||||
expect(collectionCapacitySummary(capacity({ estimatedAvailable: 2 }))).toBe('Estimated capacity for 2 of 5 servers; launches can wait for capacity.');
|
||||
expect(collectionCapacitySummary(capacity({ requested: 1, estimatedAvailable: 0 }))).toBe('Estimated capacity for 0 of 1 server; launches can wait for capacity.');
|
||||
expect(collectionCapacitySummary()).toBe(
|
||||
'Capacity estimate unavailable; launches can wait for capacity.'
|
||||
);
|
||||
expect(collectionCapacitySummary(capacity())).toBe(
|
||||
'Capacity estimate unavailable; launches can wait for capacity.'
|
||||
);
|
||||
expect(collectionCapacitySummary(capacity({ estimatedAvailable: 0 }))).toBe(
|
||||
'Estimated capacity for 0 of 5 servers; launches can wait for capacity.'
|
||||
);
|
||||
expect(collectionCapacitySummary(capacity({ estimatedAvailable: 2 }))).toBe(
|
||||
'Estimated capacity for 2 of 5 servers; launches can wait for capacity.'
|
||||
);
|
||||
expect(collectionCapacitySummary(capacity({ requested: 1, estimatedAvailable: 0 }))).toBe(
|
||||
'Estimated capacity for 0 of 1 server; launches can wait for capacity.'
|
||||
);
|
||||
});
|
||||
|
||||
it('uses requested counts when enough capacity exists and handles plans without creations', () => {
|
||||
expect(collectionCapacitySummary(capacity({ estimatedAvailable: 20 }))).toBe('Estimated capacity for all 5 servers.');
|
||||
expect(collectionCapacitySummary(capacity({ requested: 1, estimatedAvailable: 1 }))).toBe('Estimated capacity for 1 server.');
|
||||
expect(collectionCapacitySummary(capacity({ requested: 0 }))).toBe('No new server capacity needed.');
|
||||
expect(collectionCapacitySummary(capacity({ estimatedAvailable: 20 }))).toBe(
|
||||
'Estimated capacity for all 5 servers.'
|
||||
);
|
||||
expect(collectionCapacitySummary(capacity({ requested: 1, estimatedAvailable: 1 }))).toBe(
|
||||
'Estimated capacity for 1 server.'
|
||||
);
|
||||
expect(collectionCapacitySummary(capacity({ requested: 0 }))).toBe(
|
||||
'No new server capacity needed.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user