Files
electric-cherry/tests/unit/ui-collection-summary.test.ts

352 lines
12 KiB
TypeScript

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';
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: ''
}
};
}
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
};
}
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
};
}
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: [
{ 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 }
]
});
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 }
]);
});
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({
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 }
]
})
);
expect(pool).toMatchObject({ required: 3, available: 1, unassigned: 1 });
expect(pool.required - pool.unassigned - pool.available).toBe(1);
});
});
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
}
]
}
]);
});
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: [
{ name: 't5_zombie_theater_2', reason: 'Excess' },
{ 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
}
]);
});
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: [
{ 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' }
]
});
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
}
]
}
]);
});
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
}
]
]);
});
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' }
]
});
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
}
]
}
]);
});
});
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.'
);
});
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.'
);
});
});