Initial commit — SvelteKit fleet manager for T5 Plutonium servers

Ports push_scripts.sh (1497 lines of bash) to a SvelteKit + SQLite web app
that manages a fleet of T5 Plutonium servers running on Komodo. Same push
and beta workflow as the shell script, with a live SSE pipeline view, a
!rt-token-gated auth modal that hard-pauses deployments on stale tokens,
per-file audit diffs, and a Komodo-container deployment target.

- Push/beta engines port the exact filter, held-by-beta, conflict, and
  stale-backup rules from the shell script
- SQLite (better-sqlite3) replaces script_metadata.json; a one-shot
  importer moves legacy state over on first boot
- 16 vitest tests cover push add/update/skip, beta open→rollback,
  open→promote, conflict guards, sweep frozen/manual branches, and IW4M
  login/status parsing
- docker/ + komodo/stack.toml deploy as a managed container on the same
  host as the game servers, /root/zombies bind-mounted at the same path
  the shell script used
This commit is contained in:
2026-09-12 00:23:51 -04:00
commit d99711563e
67 changed files with 9415 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { newFixture, type Fixture } from '../helpers/fixture.js';
let fixture: Fixture;
beforeEach(async () => {
vi.resetModules();
fixture = await newFixture();
process.env.IW4M_ADDRESS = 'http://iw4m.test';
process.env.IW4M_CLIENT_ID = '1';
vi.stubGlobal('fetch', vi.fn());
});
afterEach(async () => {
await fixture.cleanup();
vi.unstubAllGlobals();
});
describe('loginWithToken', () => {
it('returns cookie + expiry on 2xx', async () => {
(globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce(
new Response('', {
status: 200,
headers: { 'Set-Cookie': '.AspNetCore.Cookies=abc; path=/; httponly' }
})
);
const { loginWithToken } = await import('../../src/lib/server/integrations/iw4m.js');
const res = await loginWithToken('abcd');
expect(res.ok).toBe(true);
if (res.ok) {
expect(res.cookie).toContain('.AspNetCore.Cookies=abc');
expect(res.expiresAt).toBeGreaterThan(Date.now());
}
});
it('returns rejected on 401', async () => {
(globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce(new Response('', { status: 401 }));
const { loginWithToken } = await import('../../src/lib/server/integrations/iw4m.js');
const res = await loginWithToken('abcd');
expect(res.ok).toBe(false);
if (!res.ok) expect(res.reason).toBe('rejected');
});
it('returns unreachable on network error', async () => {
(globalThis.fetch as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('ECONNREFUSED'));
const { loginWithToken } = await import('../../src/lib/server/integrations/iw4m.js');
const res = await loginWithToken('abcd');
expect(res.ok).toBe(false);
if (!res.ok) expect(res.reason).toBe('unreachable');
});
});
describe('fetchStatus', () => {
it('normalizes camelCase / PascalCase entries', async () => {
(globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce(
new Response(
JSON.stringify([
{ id: 1, port: 3072, name: 'kino der toten', currentPlayers: 3 },
{ Id: 2, Port: 3073, Name: 'ascension', CurrentPlayers: 0 }
]),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
)
);
const { fetchStatus } = await import('../../src/lib/server/integrations/iw4m.js');
const res = await fetchStatus();
expect(res.ok).toBe(true);
if (res.ok) {
expect(res.entries[0].currentPlayers).toBe(3);
expect(res.entries[1].name).toBe('ascension');
}
});
});