Add T6 Zombies fleet support with Plutainer configuration
This commit is contained in:
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs';
|
||||
import { readFile, rm } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
||||
import { T6_ZOMBIE_MAPS } from '../../src/lib/map-aliases.js';
|
||||
|
||||
vi.mock('../../src/lib/server/integrations/komodo.js', () => ({
|
||||
komodoCreateDeployment: vi.fn(async () => ({ ok: true })),
|
||||
@@ -57,6 +58,120 @@ async function promotedConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
describe('T6 Zombies provisioning', () => {
|
||||
it('keeps T6 storage and config overrides separate from T5 settings', async () => {
|
||||
vi.stubEnv('T6_SERVERS_ROOT', join(fixture.root, 'custom-t6'));
|
||||
vi.stubEnv('T6_CONFIG_TARGET_FILE', 'custom_zm.cfg');
|
||||
vi.stubEnv('T6_REL_DEST_SCRIPT', 'custom/scripts/zm');
|
||||
vi.stubEnv('PLUTAINER_T6_GAMEFILES_VOLUME', 'bo2-files');
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const baseline = createScript({ name: 'T6 baseline', kind: 'baseline', scopeType: 'game', scopeValue: 't6', content: Buffer.from('sv_voice 1\n') });
|
||||
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(baseline.version.id, baseline.script.id);
|
||||
const { createServer } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { komodoCreateDeployment } = await import('../../src/lib/server/integrations/komodo.js');
|
||||
const { destOf, configFileOf } = await import('../../src/lib/server/config.js');
|
||||
const created = await createServer({ ...input, game: 't6', mapSlug: 'town' });
|
||||
if (!created.ok) throw new Error(created.message);
|
||||
const spec = vi.mocked(komodoCreateDeployment).mock.calls[0][0];
|
||||
expect(spec.volumes).toContain('bo2-files:/home/plutainer/gamefiles:ro');
|
||||
expect(spec.environment).toContainEqual({ variable: 'PLUTAINER_CONFIG_FILE', value: 'custom_zm.cfg' });
|
||||
expect(existsSync(join(fixture.root, 'custom-t6', created.name, 'configs/custom_zm.cfg'))).toBe(true);
|
||||
expect(destOf(created.name)).toBe(`${join(fixture.root, 'custom-t6')}/${created.name}/custom/scripts/zm`);
|
||||
expect(configFileOf(serverName)).toBe('dedicated_sp.cfg');
|
||||
expect(destOf(serverName)).toBe(`${fixture.t5}/${serverName}/runtime/plutonium/storage/t5/raw/scripts/sp`);
|
||||
});
|
||||
|
||||
it.each(T6_ZOMBIE_MAPS)('provisions $friendly with the T6 mount, config, and ruleset', async (map) => {
|
||||
const { createServer } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { getServer } = await import('../../src/lib/server/fleet/servers.js');
|
||||
const { komodoCreateDeployment } = await import('../../src/lib/server/integrations/komodo.js');
|
||||
const name = `t6_zombie_${map.slug}_1`;
|
||||
expect(await createServer({ ...input, game: 't6', mapSlug: map.slug })).toMatchObject({ ok: true, name });
|
||||
const spec = vi.mocked(komodoCreateDeployment).mock.calls[0][0];
|
||||
const env = Object.fromEntries(spec.environment.map(({ variable, value }) => [variable, value]));
|
||||
expect(env).toMatchObject({ PLUTAINER_GAME: 't6zm', PLUTAINER_CONFIG_FILE: 'dedicated_zm.cfg',
|
||||
PLUTAINER_RCON_PASSWORD: 'test-rcon', PLUTAINER_MAP_ROTATE: 'false',
|
||||
PLUTAINER_EXTRA_ARGS: `+exec electric_cherry_map.cfg +set tws_server_id ${name} +map_rotate` });
|
||||
expect(env.PLUTAINER_SKIP_SEED).toBeUndefined();
|
||||
expect(spec.volumes).toEqual([`${fixture.t6}/${name}:/home/plutainer/app`, 't6_game_files:/home/plutainer/gamefiles:ro']);
|
||||
expect(getServer(name)).toMatchObject({ game: 't6', game_id: 't6zm', map: map.canonical,
|
||||
map_slug: map.slug, config_file: 'dedicated_zm.cfg' });
|
||||
expect(await readFile(join(fixture.t6, name, 'configs/electric_cherry_map.cfg'), 'utf8'))
|
||||
.toContain(`set sv_maprotation "execgts ${map.gameSettings} map ${map.canonical}"`);
|
||||
expect(existsSync(join(fixture.t5, name))).toBe(false);
|
||||
expect(existsSync(join(fixture.t6, name, 'configs/dedicated_zm.cfg'))).toBe(false);
|
||||
});
|
||||
|
||||
it('persists T6 queued work, reuses its key, and allocates ports across both games', async () => {
|
||||
const { queueCreateServer, processCreations } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { saveServerKey, validServerId } = await import('../../src/lib/server/fleet/server-keys.js');
|
||||
const { closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
const { komodoInspectHealthy } = await import('../../src/lib/server/integrations/komodo.js');
|
||||
const { rconStatusResult } = await import('../../src/lib/server/integrations/rcon.js');
|
||||
const { getDeployment } = await import('../../src/lib/server/fleet/deployments.js');
|
||||
saveServerKey('t6_zombie_town_1', 't6-key');
|
||||
expect(validServerId('t6_zombie_town_20')).toBe(true);
|
||||
expect(validServerId('t5_zombie_town_1')).toBe(false);
|
||||
expect(validServerId('t6_zombie_theater_1')).toBe(false);
|
||||
const t5 = queueCreateServer(input);
|
||||
const t6 = queueCreateServer({ game: 't6', gameMode: 'zombie', mapSlug: 'town' });
|
||||
if (!t5.ok || !t6.ok) throw new Error('queue failed');
|
||||
expect(t6.port).not.toBe(t5.port);
|
||||
closeDb();
|
||||
vi.mocked(komodoInspectHealthy).mockResolvedValue({ ok: true, state: 'running' });
|
||||
vi.mocked(rconStatusResult).mockImplementation(async (server) => ({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: '',
|
||||
status: { players: 0, map: server.startsWith('t6_') ? 'zm_transit' : 'zombie_cosmodrome', gametype: null, roster: [] } }));
|
||||
await processCreations();
|
||||
expect(getDeployment(t6.deploymentId)?.deployment).toMatchObject({ state: 'done' });
|
||||
const content = await readFile(join(fixture.t6, t6.name, 'configs/electric_cherry_map.cfg'), 'utf8');
|
||||
expect(content).toContain('execgts zm_standard_town.cfg map zm_transit');
|
||||
});
|
||||
|
||||
it('installs only matching promoted artifacts into T6 paths and removes only its directory on destruction', async () => {
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
for (const game of ['t5', 't6']) {
|
||||
for (const kind of ['script', 'baseline'] as const) {
|
||||
const { script, version } = createScript({ name: `${game}-${kind}`, kind, scopeType: 'game', scopeValue: game,
|
||||
targetRelPath: kind === 'script' ? `${game}.gsc` : undefined,
|
||||
content: Buffer.from(kind === 'script' ? 'main(){}' : 'sv_voice 1\nsv_maprotation "map wrong"\n') });
|
||||
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(version.id, script.id);
|
||||
}
|
||||
}
|
||||
const { createServer, destroyServer, processDestructions } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { getServerKey } = await import('../../src/lib/server/fleet/server-keys.js');
|
||||
const { scanServerDrift } = await import('../../src/lib/server/fleet/drift.js');
|
||||
await createServer(input);
|
||||
const t6 = await createServer({ ...input, game: 't6', mapSlug: 'tomb' });
|
||||
if (!t6.ok) throw new Error(t6.message);
|
||||
expect(existsSync(join(fixture.t6, t6.name, 'runtime/plutonium/storage/t6/scripts/zm/t6.gsc'))).toBe(true);
|
||||
expect(existsSync(join(fixture.t6, t6.name, 'runtime/plutonium/storage/t6/scripts/zm/t5.gsc'))).toBe(false);
|
||||
expect(await readFile(join(fixture.t6, t6.name, 'configs/dedicated_zm.cfg'), 'utf8'))
|
||||
.toContain('execgts zm_classic_tomb.cfg map zm_tomb');
|
||||
expect(await scanServerDrift(t6.name)).toEqual([]);
|
||||
expect(destroyServer(t6.name).ok).toBe(true);
|
||||
const { komodoInspectHealthy } = await import('../../src/lib/server/integrations/komodo.js');
|
||||
vi.mocked(komodoInspectHealthy).mockResolvedValueOnce({ ok: false, reason: 'gone' });
|
||||
await processDestructions();
|
||||
expect(existsSync(join(fixture.t6, t6.name))).toBe(false);
|
||||
expect(existsSync(join(fixture.t5, serverName))).toBe(true);
|
||||
expect(getServerKey(t6.name)).toBe('test-key');
|
||||
});
|
||||
|
||||
it('validates game/map combinations at the API boundary', async () => {
|
||||
const { POST } = await import('../../src/routes/api/servers/+server.js');
|
||||
for (const game of ['t7', null, 6]) {
|
||||
const res = await POST({ request: new Request('http://localhost/api/servers', {
|
||||
method: 'POST', body: JSON.stringify({ ...input, game })
|
||||
}) });
|
||||
expect(res.status).toBe(400);
|
||||
}
|
||||
const { queueCreateServer } = await import('../../src/lib/server/fleet/create.js');
|
||||
expect(queueCreateServer({ ...input, game: 't6' })).toMatchObject({ ok: false, kind: 'unknown-map' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('server creation map selection', () => {
|
||||
it('returns a queued creation before provisioning and reserves its name and port', async () => {
|
||||
const { POST } = await import('../../src/routes/api/servers/+server.js');
|
||||
|
||||
Reference in New Issue
Block a user