Fix release version labels across deployment views

This commit is contained in:
2026-10-17 09:45:53 -04:00
parent b5037a70fe
commit ba9bed6a0d
20 changed files with 485 additions and 39 deletions
+92 -6
View File
@@ -174,7 +174,7 @@ async function moveTarget() {
}
describe('rebalance instance migrations', () => {
it('preserves deployed configuration, patched file versions, identity and credentials on an active source', async () => {
it.each(['current', 'legacy'])('preserves deployed files (%s)', async (recipeFormat) => {
const { config } = await import('../../src/lib/server/config.js');
Object.assign(config.rcon, { password: '' });
const { createScript, uploadNewVersion } =
@@ -196,7 +196,16 @@ describe('rebalance instance migrations', () => {
scopeValue: 't5',
content: Buffer.from('original deployed script')
});
for (const artifact of [baseline, script])
const plugin = createScript({
releaseVersion: 'v2.4.0',
name: 'Deployed plugin',
kind: 'plugin',
targetRelPath: 'preserved.dll',
scopeType: 'game',
scopeValue: 't5',
content: Buffer.from('deployed plugin')
});
for (const artifact of [baseline, script, plugin])
db()
.prepare('UPDATE scripts SET current_version_id=? WHERE id=?')
.run(artifact.version.id, artifact.script.id);
@@ -243,10 +252,36 @@ describe('rebalance instance migrations', () => {
const original = nativeInstance(job.name)!;
const recipe = captureMoveRecipe(job.name);
expect(recipe.files.every((file) => file.content === undefined)).toBe(true);
expect(recipe.files.find((file) => file.path === 'preserved.gsc')).toMatchObject({
versionId: patched.id,
version: patched.version,
releaseVersion: '1.0.1'
});
expect(recipe.files.find((file) => file.path === 'preserved.dll')).toMatchObject({
versionId: plugin.version.id,
releaseVersion: 'v2.4.0'
});
const id = requestInstanceMove(job.name, target.id, {
triggeredBy: 'rebalance:test',
recipe
});
if (recipeFormat === 'legacy') {
const stored = db()
.prepare(
'SELECT recipe, recipe_summary FROM server_launch_recipes WHERE deployment_id=?'
)
.get(id) as { recipe: string; recipe_summary: string };
const legacy = (encoded: string) => {
const value = JSON.parse(encoded);
for (const file of value.files) delete file.releaseVersion;
return JSON.stringify(value);
};
db()
.prepare(
'UPDATE server_launch_recipes SET recipe=?,recipe_summary=? WHERE deployment_id=?'
)
.run(legacy(stored.recipe), legacy(stored.recipe_summary), id);
}
await advance();
const removal = hostMigration(id)!.remove_operation_id!;
expect(removal).toBeTruthy();
@@ -304,7 +339,14 @@ describe('rebalance instance migrations', () => {
generation: 2
});
expect(getHost(hostId)?.mode).toBe('active');
expect(JSON.stringify(getDeployment(id)?.recipe)).not.toContain(launch.rconPassword);
const displayedRecipe = getDeployment(id)?.recipe;
expect(displayedRecipe?.files.find((file) => file.path === 'preserved.gsc')).toMatchObject({
releaseVersion: '1.0.1'
});
expect(displayedRecipe?.files.find((file) => file.path === 'preserved.dll')).toMatchObject({
releaseVersion: 'v2.4.0'
});
expect(JSON.stringify(displayedRecipe)).not.toContain(launch.rconPassword);
});
it('rechecks occupancy, target capacity and run admission before removing the source', async () => {
@@ -1773,17 +1815,21 @@ describe('native instance provisioning', () => {
expect(getHost(hostId)?.mode).toBe('active');
});
it('builds campaign replacements with requested plugins, configuration, additions and removals', async () => {
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
const { createScript, uploadNewVersion } =
await import('../../src/lib/server/fleet/scripts.js');
const { db } = await import('../../src/lib/server/db/index.js');
let replacementPluginId = 0;
for (const path of ['replace.dll', 'remove.dll']) {
const file = createScript({
name: path,
kind: 'plugin',
releaseVersion: 'v2.0.0',
targetRelPath: path,
scopeType: 'game',
scopeValue: 't5',
content: Buffer.from('promoted plugin')
});
if (path === 'replace.dll') replacementPluginId = file.script.id;
db()
.prepare('UPDATE scripts SET current_version_id=? WHERE id=?')
.run(file.version.id, file.script.id);
@@ -1791,6 +1837,21 @@ describe('native instance provisioning', () => {
const job = await queue();
await advance();
await acknowledge(job.deploymentId);
const replacement = uploadNewVersion(
replacementPluginId,
Buffer.from('requested plugin'),
undefined,
'v2.1.0'
);
const addition = createScript({
name: 'New script',
kind: 'script',
releaseVersion: '3.2.1',
targetRelPath: 'new.gsc',
scopeType: 'game',
scopeValue: 't5',
content: Buffer.from('new script')
});
db()
.prepare(
"INSERT INTO deployments(id,action,started_at) VALUES('files-rebuild','promote',?)"
@@ -1803,9 +1864,19 @@ describe('native instance provisioning', () => {
.run(job.name);
const { queueFileChange } = await import('../../src/lib/server/fleet/staged-files.js');
const changes = [
{ kind: 'plugin', relPath: 'replace.dll', content: Buffer.from('requested plugin') },
{
kind: 'plugin',
relPath: 'replace.dll',
content: Buffer.from('requested plugin'),
scriptVersionId: replacement.id
},
{ kind: 'plugin', relPath: 'remove.dll', content: null },
{ kind: 'script', relPath: 'new.gsc', content: Buffer.from('new script') },
{
kind: 'script',
relPath: 'new.gsc',
content: Buffer.from('new script'),
scriptVersionId: addition.version.id
},
{ kind: 'config', relPath: 'custom.cfg', content: Buffer.from('set custom 1') }
] as const;
for (const change of changes)
@@ -1839,6 +1910,21 @@ describe('native instance provisioning', () => {
if (change.content === null) expect(actual).toBeUndefined();
else expect(Buffer.from(actual!.content!, 'base64')).toEqual(change.content);
}
expect(recipe.files.find((file) => file.path === 'replace.dll')).toMatchObject({
versionId: replacement.id,
version: replacement.version,
releaseVersion: 'v2.1.0'
});
expect(recipe.files.find((file) => file.path === 'new.gsc')).toMatchObject({
versionId: addition.version.id,
version: addition.version.version,
releaseVersion: '3.2.1'
});
expect(recipe.files.find((file) => file.path === 'custom.cfg')).toMatchObject({
versionId: null,
version: null,
releaseVersion: null
});
const files = getHostOperation(staged.stage_operation_id)!.payload.files as Array<{
id: number;
path: string;