173 lines
7.7 KiB
TypeScript
173 lines
7.7 KiB
TypeScript
import Database from 'better-sqlite3';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import schema from '../../src/lib/server/db/schema.sql?raw';
|
|
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
|
import type { LaunchRecipe } from '../../src/lib/server/fleet/launch-recipes.js';
|
|
|
|
let fixture: Fixture;
|
|
beforeEach(async () => {
|
|
vi.resetModules();
|
|
fixture = await newFixture([]);
|
|
});
|
|
afterEach(async () => {
|
|
vi.restoreAllMocks();
|
|
await fixture.cleanup();
|
|
});
|
|
|
|
describe('persisted launch recipe summaries', () => {
|
|
it.each([false, true])(
|
|
'restores exact historical release labels with cached summary=%s without rewriting the recipe',
|
|
async (cached) => {
|
|
const { db } = await import('../../src/lib/server/db/index.js');
|
|
const { createScript, uploadNewVersion } =
|
|
await import('../../src/lib/server/fleet/scripts.js');
|
|
const {
|
|
captureRecipe,
|
|
summarizeRecipe,
|
|
recipeSummaryForDeployment,
|
|
recipeForDeployment
|
|
} = await import('../../src/lib/server/fleet/launch-recipes.js');
|
|
const saved = createScript({
|
|
name: 'Historical script',
|
|
scopeType: 'game',
|
|
scopeValue: 't5',
|
|
targetRelPath: 'history.gsc',
|
|
content: Buffer.from('historical bytes'),
|
|
releaseVersion: '2.3.4'
|
|
});
|
|
db()
|
|
.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?')
|
|
.run(saved.version.id, saved.script.id);
|
|
const recipe = captureRecipe('t5', 'theater');
|
|
const original = recipe.files[0];
|
|
delete original.releaseVersion;
|
|
const unversioned = createScript({
|
|
name: 'Legacy script',
|
|
targetRelPath: 'legacy.gsc',
|
|
content: Buffer.from('historical bytes'),
|
|
releaseVersion: '1.0.0'
|
|
});
|
|
db()
|
|
.prepare('UPDATE script_versions SET release_version = NULL WHERE id = ?')
|
|
.run(unversioned.version.id);
|
|
recipe.files.push(
|
|
{ ...original, path: 'recorded.gsc', releaseVersion: '1.8.0' },
|
|
{ ...original, path: 'legacy.gsc', versionId: unversioned.version.id },
|
|
{ ...original, path: 'missing.gsc', versionId: 9999 },
|
|
{ ...original, path: 'mismatched.gsc', sha256: 'different' }
|
|
);
|
|
const latest = uploadNewVersion(
|
|
saved.script.id,
|
|
Buffer.from('newer bytes'),
|
|
undefined,
|
|
'9.0.0'
|
|
);
|
|
db()
|
|
.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?')
|
|
.run(latest.id, saved.script.id);
|
|
const encoded = JSON.stringify(recipe);
|
|
const summary = summarizeRecipe(recipe);
|
|
db()
|
|
.exec(`INSERT INTO server_deployments(id,server,action,state,stage,started_at,triggered_by)
|
|
VALUES('historical','retired-server','create','done','Complete','2025-01-01','fixture')`);
|
|
db()
|
|
.prepare(
|
|
'INSERT INTO server_launch_recipes(deployment_id,recipe,recipe_summary) VALUES(?,?,?)'
|
|
)
|
|
.run('historical', encoded, cached ? JSON.stringify(summary) : null);
|
|
|
|
const restored = recipeSummaryForDeployment('historical')!.recipe;
|
|
expect(restored.fingerprint).toBe(summary.fingerprint);
|
|
expect(restored.files.find((file) => file.path === 'history.gsc')).toMatchObject({
|
|
version: 1,
|
|
releaseVersion: '2.3.4'
|
|
});
|
|
expect(
|
|
restored.files.find((file) => file.path === 'recorded.gsc')!.releaseVersion
|
|
).toBe('1.8.0');
|
|
expect(
|
|
restored.files.find((file) => file.path === 'legacy.gsc')!.releaseVersion
|
|
).toBeNull();
|
|
for (const path of ['missing.gsc', 'mismatched.gsc'])
|
|
expect(restored.files.find((file) => file.path === path)).not.toHaveProperty(
|
|
'releaseVersion'
|
|
);
|
|
expect(recipeSummaryForDeployment('historical')!.recipe).toEqual(restored);
|
|
expect(recipeForDeployment('historical')!.recipe).toEqual(recipe);
|
|
const stored = db()
|
|
.prepare(
|
|
'SELECT recipe,recipe_summary FROM server_launch_recipes WHERE deployment_id = ?'
|
|
)
|
|
.get('historical') as { recipe: string; recipe_summary: string };
|
|
expect(stored.recipe).toBe(encoded);
|
|
expect(JSON.parse(stored.recipe_summary)).toEqual(restored);
|
|
}
|
|
);
|
|
|
|
it('lazily upgrades legacy snapshots once without changing fingerprints or retained bytes', async () => {
|
|
const recipe: LaunchRecipe = {
|
|
format: 1,
|
|
game: 't5',
|
|
mapSlug: 'theater',
|
|
mapName: 'Kino der Toten',
|
|
expectedMap: 'zombie_theater',
|
|
capturedAt: '2025-01-01T00:00:00.000Z',
|
|
image: 'plutainer:test',
|
|
configFile: 'dedicated_sp.cfg',
|
|
configSource: 'Defaults',
|
|
files: [
|
|
{
|
|
name: 'Old plugin',
|
|
kind: 'plugin',
|
|
path: 'legacy.dll',
|
|
versionId: 1,
|
|
version: 1,
|
|
sha256: 'a'.repeat(64),
|
|
size: 2 * 1024 ** 2,
|
|
content: Buffer.alloc(2 * 1024 ** 2, 42).toString('base64')
|
|
}
|
|
]
|
|
};
|
|
const legacy = new Database(fixture.dbPath);
|
|
const encoded = JSON.stringify(recipe, null, 2);
|
|
try {
|
|
legacy.exec(schema.replace(' recipe_summary TEXT,\n', ''));
|
|
legacy.exec(`INSERT INTO server_deployments(id,server,action,state,stage,started_at,triggered_by)
|
|
VALUES('legacy','retired-server','create','done','Complete','2025-01-01','fixture');`);
|
|
legacy
|
|
.prepare(
|
|
'INSERT INTO server_launch_recipes(deployment_id,recipe,blueprint_name) VALUES(?,?,?)'
|
|
)
|
|
.run('legacy', encoded, 'Old collection');
|
|
} finally {
|
|
legacy.close();
|
|
}
|
|
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
|
|
const { recipeSummaryForDeployment, recipeForDeployment, summarizeRecipe } =
|
|
await import('../../src/lib/server/fleet/launch-recipes.js');
|
|
const expected = { recipe: summarizeRecipe(recipe), blueprintName: 'Old collection' };
|
|
expect(db().prepare('SELECT recipe_summary FROM server_launch_recipes').get()).toEqual({
|
|
recipe_summary: null
|
|
});
|
|
expect(recipeSummaryForDeployment('legacy')).toEqual(expected);
|
|
closeDb();
|
|
const parse = JSON.parse;
|
|
const parsing = vi.spyOn(JSON, 'parse').mockImplementation((value, reviver) => {
|
|
if (value.length > 1024 * 1024) throw new Error('Legacy recipe was decoded again.');
|
|
return parse(value, reviver);
|
|
});
|
|
try {
|
|
expect(recipeSummaryForDeployment('legacy')).toEqual(expected);
|
|
expect(recipeSummaryForDeployment('missing')).toBeNull();
|
|
} finally {
|
|
parsing.mockRestore();
|
|
}
|
|
expect(recipeForDeployment('legacy')).toEqual({ recipe, blueprintName: 'Old collection' });
|
|
const stored = db().prepare('SELECT recipe FROM server_launch_recipes').get() as {
|
|
recipe: string;
|
|
};
|
|
expect(stored.recipe.length).toBeLessThan(1024);
|
|
expect(JSON.parse(stored.recipe).files[0]).not.toHaveProperty('content');
|
|
});
|
|
});
|