Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -16,7 +16,9 @@ beforeEach(async () => {
|
||||
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
|
||||
await seedServerRegistry();
|
||||
});
|
||||
afterEach(async () => { await fixture.cleanup(); });
|
||||
afterEach(async () => {
|
||||
await fixture.cleanup();
|
||||
});
|
||||
|
||||
async function setup() {
|
||||
const library = await import('../../src/lib/server/fleet/scripts.js');
|
||||
@@ -24,41 +26,83 @@ async function setup() {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { stagedFileChanges } = await import('../../src/lib/server/fleet/staged-files.js');
|
||||
const { destOfKind, configFileOf } = await import('../../src/lib/server/config.js');
|
||||
const create = (game: string, path = 'shared.gsc') => library.createScript({ name: `${game} ${path}`, targetRelPath: path,
|
||||
scopeType: 'game', scopeValue: game, content: Buffer.from(`${game} tested`) });
|
||||
const open = (files: ReturnType<typeof create>[], servers = participants) => beta.openBeta({
|
||||
scripts: files.map(file => ({ scriptId: file.script.id, scriptVersionId: file.version.id })),
|
||||
servers, waitForHealth: false, waitForMatch: false
|
||||
});
|
||||
const create = (game: string, path = 'shared.gsc') =>
|
||||
library.createScript({
|
||||
name: `${game} ${path}`,
|
||||
targetRelPath: path,
|
||||
scopeType: 'game',
|
||||
scopeValue: game,
|
||||
content: Buffer.from(`${game} tested`)
|
||||
});
|
||||
const open = (files: ReturnType<typeof create>[], servers = participants) =>
|
||||
beta.openBeta({
|
||||
scripts: files.map((file) => ({
|
||||
scriptId: file.script.id,
|
||||
scriptVersionId: file.version.id
|
||||
})),
|
||||
servers,
|
||||
waitForHealth: false,
|
||||
waitForMatch: false
|
||||
});
|
||||
const ready = (id: number) => {
|
||||
db().prepare("UPDATE betas SET state = 'ready-promote' WHERE id = ?").run(id);
|
||||
db().prepare("UPDATE beta_servers SET stage = 'ready', restarted_at = ? WHERE beta_id = ?").run(new Date().toISOString(), id);
|
||||
db()
|
||||
.prepare("UPDATE beta_servers SET stage = 'ready', restarted_at = ? WHERE beta_id = ?")
|
||||
.run(new Date().toISOString(), id);
|
||||
};
|
||||
return {
|
||||
...library,
|
||||
...beta,
|
||||
db: db(),
|
||||
stagedFileChanges,
|
||||
destOfKind,
|
||||
configFileOf,
|
||||
create,
|
||||
open,
|
||||
ready
|
||||
};
|
||||
return { ...library, ...beta, db: db(), stagedFileChanges, destOfKind, configFileOf, create, open, ready };
|
||||
}
|
||||
|
||||
describe('multi-game beta bundles', () => {
|
||||
it('stages, revises and rolls back only each server\'s matching files', async () => {
|
||||
it("stages, revises and rolls back only each server's matching files", async () => {
|
||||
const x = await setup();
|
||||
const files = [x.create('t5', 'five.gsc'), x.create('t6', 'six.gsc')];
|
||||
// The other game's filename can already exist independently on a server.
|
||||
const unrelated = join(x.destOfKind(t5[0], 'script'), 'six.gsc');
|
||||
await writeFile(unrelated, 'unrelated original');
|
||||
const { betaId } = await x.open(files);
|
||||
expect(x.stagedFileChanges('beta', String(betaId)).map(row => [row.server, row.rel_path]))
|
||||
.toEqual([[t5[0], 'five.gsc'], [t6[0], 'six.gsc']]);
|
||||
expect(
|
||||
x.stagedFileChanges('beta', String(betaId)).map((row) => [row.server, row.rel_path])
|
||||
).toEqual([
|
||||
[t5[0], 'five.gsc'],
|
||||
[t6[0], 'six.gsc']
|
||||
]);
|
||||
const { heldByBeta, ownerOf } = await import('../../src/lib/server/fleet/servers.js');
|
||||
expect([...heldByBeta(t5[0]).keys()]).toEqual(['five.gsc']);
|
||||
expect(ownerOf(t5[0], 'six.gsc')).toBeNull();
|
||||
await activateFiles('beta', betaId);
|
||||
await x.reviseBeta(betaId, 1, [{ scriptId: files[1].script.id, content: Buffer.from('t6 revised') }]);
|
||||
expect(x.stagedFileChanges('beta', String(betaId)).map(row => [row.server, row.rel_path]))
|
||||
.toEqual([[t5[0], 'five.gsc'], [t6[0], 'six.gsc']]);
|
||||
await x.reviseBeta(betaId, 1, [
|
||||
{ scriptId: files[1].script.id, content: Buffer.from('t6 revised') }
|
||||
]);
|
||||
expect(
|
||||
x.stagedFileChanges('beta', String(betaId)).map((row) => [row.server, row.rel_path])
|
||||
).toEqual([
|
||||
[t5[0], 'five.gsc'],
|
||||
[t6[0], 'six.gsc']
|
||||
]);
|
||||
await activateFiles('beta', betaId);
|
||||
expect(await readFile(join(x.destOfKind(t6[0], 'script'), 'six.gsc'), 'utf8')).toBe('t6 revised');
|
||||
expect(await readFile(join(x.destOfKind(t6[0], 'script'), 'six.gsc'), 'utf8')).toBe(
|
||||
't6 revised'
|
||||
);
|
||||
const rollback = await x.rollbackBeta(betaId);
|
||||
expect(x.stagedFileChanges('campaign', rollback.deploymentId!).map(row => [row.server, row.rel_path]))
|
||||
.toEqual([[t5[0], 'five.gsc'], [t6[0], 'six.gsc']]);
|
||||
expect(
|
||||
x
|
||||
.stagedFileChanges('campaign', rollback.deploymentId!)
|
||||
.map((row) => [row.server, row.rel_path])
|
||||
).toEqual([
|
||||
[t5[0], 'five.gsc'],
|
||||
[t6[0], 'six.gsc']
|
||||
]);
|
||||
await activateFiles('campaign', rollback.deploymentId!);
|
||||
expect(await readFile(unrelated, 'utf8')).toBe('unrelated original');
|
||||
expect(existsSync(join(x.destOfKind(t5[0], 'script'), 'five.gsc'))).toBe(false);
|
||||
@@ -67,50 +111,87 @@ describe('multi-game beta bundles', () => {
|
||||
|
||||
it('promotes same-named files to their own game and restores the correct inventory on rollback', async () => {
|
||||
const x = await setup();
|
||||
const { recordScript, getServerScript } = await import('../../src/lib/server/fleet/inventory.js');
|
||||
const { recordScript, getServerScript } =
|
||||
await import('../../src/lib/server/fleet/inventory.js');
|
||||
const files = [x.create('t5'), x.create('t6')];
|
||||
const priors = files.map(file => x.uploadNewVersion(file.script.id, Buffer.from(`${file.script.scope_value} original`)));
|
||||
const priors = files.map((file) =>
|
||||
x.uploadNewVersion(file.script.id, Buffer.from(`${file.script.scope_value} original`))
|
||||
);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
x.db.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(priors[i].id, files[i].script.id);
|
||||
x.db
|
||||
.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?')
|
||||
.run(priors[i].id, files[i].script.id);
|
||||
for (const server of i === 0 ? t5 : t6) {
|
||||
await writeFile(join(x.destOfKind(server, 'script'), 'shared.gsc'), `${files[i].script.scope_value} original`);
|
||||
recordScript(server, 'shared.gsc', priors[i].id, priors[i].sha256, priors[i].size, 'test');
|
||||
await writeFile(
|
||||
join(x.destOfKind(server, 'script'), 'shared.gsc'),
|
||||
`${files[i].script.scope_value} original`
|
||||
);
|
||||
recordScript(
|
||||
server,
|
||||
'shared.gsc',
|
||||
priors[i].id,
|
||||
priors[i].sha256,
|
||||
priors[i].size,
|
||||
'test'
|
||||
);
|
||||
}
|
||||
}
|
||||
const { betaId } = await x.open(files);
|
||||
await activateFiles('beta', betaId); x.ready(betaId);
|
||||
await activateFiles('beta', betaId);
|
||||
x.ready(betaId);
|
||||
const preview = x.previewBetaPromotion(betaId);
|
||||
expect(preview.files.map(file => file.servers)).toEqual([t5, t6]);
|
||||
expect(preview.files.map((file) => file.servers)).toEqual([t5, t6]);
|
||||
expect(preview.blockers).toEqual([]);
|
||||
const promotion = await x.promoteBeta(betaId, preview.token);
|
||||
expect(promotion.awaitingActivation).toBe(2);
|
||||
await activateFiles('campaign', promotion.deploymentId);
|
||||
for (const server of [...t5, ...t6]) {
|
||||
expect(await readFile(join(x.destOfKind(server, 'script'), 'shared.gsc'), 'utf8')).toBe(`${server.slice(0, 2)} tested`);
|
||||
expect(await readFile(join(x.destOfKind(server, 'script'), 'shared.gsc'), 'utf8')).toBe(
|
||||
`${server.slice(0, 2)} tested`
|
||||
);
|
||||
}
|
||||
const { rollbackPromoteDeployment } = await import('../../src/lib/server/fleet/rollback.js');
|
||||
const { rollbackPromoteDeployment } =
|
||||
await import('../../src/lib/server/fleet/rollback.js');
|
||||
const rollback = await rollbackPromoteDeployment(promotion.deploymentId);
|
||||
await activateFiles('campaign', rollback.deploymentId);
|
||||
for (const server of [...t5, ...t6]) {
|
||||
expect(await readFile(join(x.destOfKind(server, 'script'), 'shared.gsc'), 'utf8')).toBe(`${server.slice(0, 2)} original`);
|
||||
expect(getServerScript(server, 'shared.gsc')?.script_version_id).toBe(priors[server.startsWith('t5') ? 0 : 1].id);
|
||||
expect(await readFile(join(x.destOfKind(server, 'script'), 'shared.gsc'), 'utf8')).toBe(
|
||||
`${server.slice(0, 2)} original`
|
||||
);
|
||||
expect(getServerScript(server, 'shared.gsc')?.script_version_id).toBe(
|
||||
priors[server.startsWith('t5') ? 0 : 1].id
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('composes and promotes separate baselines and overlays for both games', async () => {
|
||||
const x = await setup();
|
||||
const files = ['t5', 't6'].flatMap(game => [
|
||||
x.createScript({ name: `${game} baseline`, kind: 'baseline', scopeType: 'game', scopeValue: game,
|
||||
content: Buffer.from(`set baseline_game "${game}"\nset overlay_game "none"\n`) }),
|
||||
x.createScript({ name: `${game} overlay`, kind: 'config', scopeType: 'game', scopeValue: game,
|
||||
content: Buffer.from(`set overlay_game "${game}"\n`) })
|
||||
const files = ['t5', 't6'].flatMap((game) => [
|
||||
x.createScript({
|
||||
name: `${game} baseline`,
|
||||
kind: 'baseline',
|
||||
scopeType: 'game',
|
||||
scopeValue: game,
|
||||
content: Buffer.from(`set baseline_game "${game}"\nset overlay_game "none"\n`)
|
||||
}),
|
||||
x.createScript({
|
||||
name: `${game} overlay`,
|
||||
kind: 'config',
|
||||
scopeType: 'game',
|
||||
scopeValue: game,
|
||||
content: Buffer.from(`set overlay_game "${game}"\n`)
|
||||
})
|
||||
]);
|
||||
const { betaId } = await x.open(files);
|
||||
await activateFiles('beta', betaId); x.ready(betaId);
|
||||
await activateFiles('beta', betaId);
|
||||
x.ready(betaId);
|
||||
const promotion = await x.promoteBeta(betaId);
|
||||
await activateFiles('campaign', promotion.deploymentId);
|
||||
for (const server of [...t5, ...t6]) {
|
||||
const content = await readFile(join(x.destOfKind(server, 'config'), x.configFileOf(server)), 'utf8');
|
||||
const content = await readFile(
|
||||
join(x.destOfKind(server, 'config'), x.configFileOf(server)),
|
||||
'utf8'
|
||||
);
|
||||
expect(content).toContain(`set baseline_game "${server.slice(0, 2)}"`);
|
||||
expect(content).toContain(`set overlay_game "${server.slice(0, 2)}"`);
|
||||
expect(content).not.toContain(`"${server.startsWith('t5') ? 't6' : 't5'}"`);
|
||||
@@ -119,9 +200,16 @@ describe('multi-game beta bundles', () => {
|
||||
|
||||
it('leaves the configuration on script-only participants untouched during rollback', async () => {
|
||||
const x = await setup();
|
||||
const baseline = x.createScript({ name: 'T5 config', kind: 'baseline', scopeType: 'game', scopeValue: 't5', content: Buffer.from('set sv_voice "1"\n') });
|
||||
const baseline = x.createScript({
|
||||
name: 'T5 config',
|
||||
kind: 'baseline',
|
||||
scopeType: 'game',
|
||||
scopeValue: 't5',
|
||||
content: Buffer.from('set sv_voice "1"\n')
|
||||
});
|
||||
const config = join(x.destOfKind(t6[0], 'config'), x.configFileOf(t6[0]));
|
||||
await mkdir(dirname(config), { recursive: true }); await writeFile(config, 'unrelated T6 config');
|
||||
await mkdir(dirname(config), { recursive: true });
|
||||
await writeFile(config, 'unrelated T6 config');
|
||||
const { betaId } = await x.open([baseline, x.create('t6')]);
|
||||
await activateFiles('beta', betaId);
|
||||
const rollback = await x.rollbackBeta(betaId);
|
||||
@@ -132,28 +220,72 @@ describe('multi-game beta bundles', () => {
|
||||
|
||||
it('applies unscoped files to every selected server alongside game-specific files', async () => {
|
||||
const x = await setup();
|
||||
const shared = x.createScript({ name: 'Shared helper', targetRelPath: 'helper.gsc', content: Buffer.from('shared') });
|
||||
const five = x.create('t5'), six = x.create('t6');
|
||||
const shared = x.createScript({
|
||||
name: 'Shared helper',
|
||||
targetRelPath: 'helper.gsc',
|
||||
content: Buffer.from('shared')
|
||||
});
|
||||
const five = x.create('t5'),
|
||||
six = x.create('t6');
|
||||
const { betaId } = await x.open([five, six, shared]);
|
||||
expect(participants.map(server => x.listBetaScripts(betaId, server).map(file => file.script_id)))
|
||||
.toEqual([[shared.script.id, five.script.id], [shared.script.id, six.script.id]]);
|
||||
expect(x.stagedFileChanges('beta', String(betaId)).filter(row => row.rel_path === 'helper.gsc')).toHaveLength(2);
|
||||
expect(
|
||||
participants.map((server) =>
|
||||
x.listBetaScripts(betaId, server).map((file) => file.script_id)
|
||||
)
|
||||
).toEqual([
|
||||
[shared.script.id, five.script.id],
|
||||
[shared.script.id, six.script.id]
|
||||
]);
|
||||
expect(
|
||||
x
|
||||
.stagedFileChanges('beta', String(betaId))
|
||||
.filter((row) => row.rel_path === 'helper.gsc')
|
||||
).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('rejects untested files, unused servers and overlapping targets before saving a beta', async () => {
|
||||
const x = await setup();
|
||||
const five = x.create('t5'), six = x.create('t6');
|
||||
const five = x.create('t5'),
|
||||
six = x.create('t6');
|
||||
await expect(x.open([five, six], [t5[0]])).rejects.toMatchObject({ kind: 'out-of-scope' });
|
||||
await expect(x.open([five], participants)).rejects.toMatchObject({ kind: 'out-of-scope' });
|
||||
const overlap = x.createScript({ name: 'Map overlap', targetRelPath: 'shared.gsc', scopeType: 'map', scopeValue: 'zombie_theater', content: Buffer.from('overlap') });
|
||||
await expect(x.open([five, six, overlap])).rejects.toMatchObject({ kind: 'duplicate-target' });
|
||||
const global = x.createScript({ name: 'Global overlap', targetRelPath: 'shared.gsc', content: Buffer.from('overlap') });
|
||||
await expect(x.open([five, six, global])).rejects.toMatchObject({ kind: 'duplicate-target' });
|
||||
const overlap = x.createScript({
|
||||
name: 'Map overlap',
|
||||
targetRelPath: 'shared.gsc',
|
||||
scopeType: 'map',
|
||||
scopeValue: 'zombie_theater',
|
||||
content: Buffer.from('overlap')
|
||||
});
|
||||
await expect(x.open([five, six, overlap])).rejects.toMatchObject({
|
||||
kind: 'duplicate-target'
|
||||
});
|
||||
const global = x.createScript({
|
||||
name: 'Global overlap',
|
||||
targetRelPath: 'shared.gsc',
|
||||
content: Buffer.from('overlap')
|
||||
});
|
||||
await expect(x.open([five, six, global])).rejects.toMatchObject({
|
||||
kind: 'duplicate-target'
|
||||
});
|
||||
const baselines = [
|
||||
x.createScript({ name: 'Game baseline', kind: 'baseline', scopeType: 'game', scopeValue: 't5', content: Buffer.from('set sv_voice "1"\n') }),
|
||||
x.createScript({ name: 'Map baseline', kind: 'baseline', scopeType: 'map', scopeValue: 'zombie_theater', content: Buffer.from('set sv_voice "0"\n') })
|
||||
x.createScript({
|
||||
name: 'Game baseline',
|
||||
kind: 'baseline',
|
||||
scopeType: 'game',
|
||||
scopeValue: 't5',
|
||||
content: Buffer.from('set sv_voice "1"\n')
|
||||
}),
|
||||
x.createScript({
|
||||
name: 'Map baseline',
|
||||
kind: 'baseline',
|
||||
scopeType: 'map',
|
||||
scopeValue: 'zombie_theater',
|
||||
content: Buffer.from('set sv_voice "0"\n')
|
||||
})
|
||||
];
|
||||
await expect(x.open(baselines, [t5[0]])).rejects.toMatchObject({ kind: 'duplicate-target' });
|
||||
await expect(x.open(baselines, [t5[0]])).rejects.toMatchObject({
|
||||
kind: 'duplicate-target'
|
||||
});
|
||||
expect(x.db.prepare('SELECT * FROM betas').all()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user