Improve operational UX, recovery flows, and action safety
Implement the approved UX review across Fleet, Betas, Library, deployments, and Settings. Add reviewed promotion and reconciliation plans, atomic queued-removal cancellation, recoverable beta drafts and launch inputs, accessible controls, and mobile improvements. Validated with 473 tests, a clean typecheck, production build, and isolated desktop/mobile browser walkthroughs. Include the review and implementation coverage notes.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
||||
|
||||
let fixture: Fixture;
|
||||
const servers = ['t5_zombie_theater_1', 't5_zombie_cosmodrome_1'];
|
||||
beforeEach(async () => {
|
||||
vi.resetModules(); fixture = await newFixture(servers);
|
||||
vi.stubEnv('KOMODO_ADDRESS', ''); vi.stubEnv('RCON_PASSWORD', '');
|
||||
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
|
||||
await seedServerRegistry();
|
||||
});
|
||||
afterEach(async () => { await fixture.cleanup(); vi.unstubAllEnvs(); });
|
||||
|
||||
async function ready(kind: 'script' | 'baseline' = 'script', scope: 'game' | 'map' = 'game') {
|
||||
const { createScript, getLatestVersion } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const { openBeta } = await import('../../src/lib/server/fleet/beta.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { script } = createScript({ name: 'Review test', kind, targetRelPath: kind === 'script' ? 'review.gsc' : undefined,
|
||||
scopeType: scope, scopeValue: scope === 'game' ? 't5' : 'zombie_theater', content: Buffer.from(kind === 'script' ? 'main(){}' : 'set sv_voice "1"\n') });
|
||||
const version = getLatestVersion(script.id)!;
|
||||
const { betaId } = await openBeta({ scripts: [{ scriptId: script.id, scriptVersionId: version.id }], servers: [servers[0]], waitForHealth: false, waitForMatch: false });
|
||||
db().prepare("UPDATE betas SET state = 'ready-promote' WHERE id = ?").run(betaId);
|
||||
db().prepare("UPDATE beta_servers SET stage = 'ready', restarted_at = ? WHERE beta_id = ?").run(new Date().toISOString(), betaId);
|
||||
return { betaId, script, version };
|
||||
}
|
||||
const destination = () => join(fixture.t5, servers[1], 'runtime/plutonium/storage/t5/raw/scripts/sp/review.gsc');
|
||||
|
||||
describe('reviewed beta promotion', () => {
|
||||
it('previews exact versions and additional targets without writing, then links the activation rollout', async () => {
|
||||
const { betaId, script, version } = await ready();
|
||||
const { previewBetaPromotion, promoteBeta } = await import('../../src/lib/server/fleet/beta.js');
|
||||
const preview = previewBetaPromotion(betaId);
|
||||
expect(preview.files[0]).toMatchObject({ id: script.id, versionId: version.id, sha256: version.sha256, servers: [...servers].sort() });
|
||||
expect(preview.servers).toEqual(expect.arrayContaining([expect.objectContaining({ name: servers[0], tested: true }), expect.objectContaining({ name: servers[1], tested: false })]));
|
||||
expect(previewBetaPromotion(betaId).token).toBe(preview.token);
|
||||
expect(existsSync(destination())).toBe(false);
|
||||
const result = await promoteBeta(betaId, preview.token);
|
||||
expect(result).toMatchObject({ awaitingActivation: 1, failedServers: [] });
|
||||
expect(result.deploymentId).toContain('promote');
|
||||
expect(await readFile(destination(), 'utf8')).toBe('main(){}');
|
||||
});
|
||||
it('rejects changed scope membership before creating a rollout or writing extra files', async () => {
|
||||
const { betaId } = await ready('script', 'map');
|
||||
const { previewBetaPromotion, promoteBeta, getBeta } = await import('../../src/lib/server/fleet/beta.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const preview = previewBetaPromotion(betaId);
|
||||
expect(preview.servers).toHaveLength(1);
|
||||
db().prepare('UPDATE servers SET map = ? WHERE name = ?').run('zombie_theater', servers[1]);
|
||||
await expect(promoteBeta(betaId, preview.token)).rejects.toMatchObject({ kind: 'preview-changed', details: expect.objectContaining({ servers: expect.arrayContaining([expect.objectContaining({ name: servers[1] })]) }) });
|
||||
expect(existsSync(destination())).toBe(false);
|
||||
expect(getBeta(betaId)?.state).toBe('ready-promote');
|
||||
expect(db().prepare('SELECT COUNT(*) n FROM deployments').get()).toEqual({ n: 0 });
|
||||
await expect(promoteBeta(betaId, previewBetaPromotion(betaId).token)).resolves.toMatchObject({ awaitingActivation: 1 });
|
||||
});
|
||||
it('includes effective config contents in the guard, including map-specific composition', async () => {
|
||||
const { betaId } = await ready('baseline');
|
||||
const { previewBetaPromotion, promoteBeta } = await import('../../src/lib/server/fleet/beta.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const preview = previewBetaPromotion(betaId);
|
||||
expect(preview.configWrites).toHaveLength(2);
|
||||
expect(new Set(preview.configWrites.map(write => write.sha256)).size).toBe(2);
|
||||
db().prepare('UPDATE servers SET map = ?, map_slug = ? WHERE name = ?').run('zombie_moon', 'moon', servers[1]);
|
||||
await expect(promoteBeta(betaId, preview.token)).rejects.toMatchObject({ kind: 'preview-changed' });
|
||||
expect(db().prepare('SELECT COUNT(*) n FROM deployments').get()).toEqual({ n: 0 });
|
||||
});
|
||||
it('requires the reviewed token at the HTTP boundary', async () => {
|
||||
const { betaId } = await ready();
|
||||
const { POST, GET } = await import('../../src/routes/api/betas/[id]/promote/+server.js');
|
||||
const response = await POST({ params: { id: String(betaId) }, request: new Request('http://localhost/promote', { method: 'POST', body: '{}' }) });
|
||||
expect(response.status).toBe(400);
|
||||
const preview = await GET({ params: { id: String(betaId) } });
|
||||
expect(preview.headers.get('cache-control')).toBe('no-store');
|
||||
expect((await preview.json()).preview.token).toHaveLength(64);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user