This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
import { afterEach, expect, it, vi } from 'vitest';
|
||||
import { flushSync, mount, unmount } from 'svelte';
|
||||
import BetaRevisionDialog from '../../src/lib/ui/components/BetaRevisionDialog.svelte';
|
||||
import ScriptUploadDrawer from '../../src/lib/ui/components/ScriptUploadDrawer.svelte';
|
||||
|
||||
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
|
||||
let component: ReturnType<typeof mount> | undefined;
|
||||
afterEach(async () => {
|
||||
if (component) await unmount(component);
|
||||
component = undefined;
|
||||
document.body.replaceChildren();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('fills exact filenames from a manifest, preserves manual values, and sends each release', async () => {
|
||||
const uploaded: FormData[] = [];
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(async (url: string, options?: RequestInit) => {
|
||||
if (url === '/api/scope-options') return Response.json({ games: [], mapsByGame: {} });
|
||||
if (url === '/api/servers') return Response.json({ servers: [] });
|
||||
if (url === '/api/scripts/inspect')
|
||||
return Response.json({ sha256: 'test', duplicates: [] });
|
||||
uploaded.push(options!.body as FormData);
|
||||
return Response.json({
|
||||
ok: true,
|
||||
script: { id: uploaded.length },
|
||||
versionId: uploaded.length
|
||||
});
|
||||
})
|
||||
);
|
||||
const drawer = mount(ScriptUploadDrawer, {
|
||||
target: document.body,
|
||||
props: { scripts: [], onuploaded: async () => {} }
|
||||
});
|
||||
component = drawer;
|
||||
flushSync();
|
||||
const dialog = document.querySelector('dialog')!;
|
||||
dialog.showModal = () => {
|
||||
dialog.open = true;
|
||||
};
|
||||
await drawer.addFiles([
|
||||
new File(['hud'], 'hud.gsc'),
|
||||
new File(['join'], 'join.gsc'),
|
||||
new File(['lock'], 'lock.gsc')
|
||||
]);
|
||||
flushSync();
|
||||
const versions = [...document.querySelectorAll<HTMLInputElement>('input[placeholder="1.2.0"]')];
|
||||
expect(versions).toHaveLength(3);
|
||||
const uploadButton = [...document.querySelectorAll<HTMLButtonElement>('button')].find(
|
||||
(button) => button.textContent?.trim() === 'Upload'
|
||||
)!;
|
||||
expect(uploadButton.disabled).toBe(true);
|
||||
versions[0].value = '3.0.0';
|
||||
versions[0].dispatchEvent(new Event('input', { bubbles: true }));
|
||||
flushSync();
|
||||
const manifest = document.querySelector<HTMLInputElement>('input[accept=".json"]')!;
|
||||
Object.defineProperty(manifest, 'files', {
|
||||
configurable: true,
|
||||
value: [
|
||||
new File(['{"hud.gsc":"1.0.0","join.gsc":"2.0.0","LOCK.gsc":"9.0.0"}'], 'versions.json')
|
||||
]
|
||||
});
|
||||
manifest.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
await vi.waitFor(() => {
|
||||
flushSync();
|
||||
expect(versions[1].value).toBe('2.0.0');
|
||||
});
|
||||
expect(versions[0].value).toBe('3.0.0');
|
||||
expect(versions[2].value).toBe('');
|
||||
expect(uploadButton.disabled).toBe(true);
|
||||
versions[2].value = '1.0';
|
||||
versions[2].dispatchEvent(new Event('input', { bubbles: true }));
|
||||
flushSync();
|
||||
expect(uploadButton.disabled).toBe(true);
|
||||
versions[2].value = '1.0.0';
|
||||
versions[2].dispatchEvent(new Event('input', { bubbles: true }));
|
||||
flushSync();
|
||||
expect(uploadButton.disabled).toBe(false);
|
||||
uploadButton.click();
|
||||
await vi.waitFor(() => expect(uploaded).toHaveLength(3));
|
||||
expect(uploaded.map((form) => form.get('releaseVersion'))).toEqual(['3.0.0', '2.0.0', '1.0.0']);
|
||||
});
|
||||
|
||||
it('requires a replacement release before beta review and includes it in the revision request', async () => {
|
||||
let submitted: FormData | null = null;
|
||||
const onUpdate = vi.fn();
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(async (url: string, options?: RequestInit) => {
|
||||
if (url === '/api/scripts/inspect')
|
||||
return Response.json({ sha256: 'new-hash', duplicates: [] });
|
||||
if (url.endsWith('/files/4')) return new Response('original script');
|
||||
submitted = options!.body as FormData;
|
||||
return Response.json({ ok: true });
|
||||
})
|
||||
);
|
||||
component = mount(BetaRevisionDialog, {
|
||||
target: document.body,
|
||||
props: {
|
||||
open: true,
|
||||
betaId: 7,
|
||||
revision: 1,
|
||||
servers: [],
|
||||
onUpdate,
|
||||
files: [
|
||||
{
|
||||
id: 4,
|
||||
name: 'HUD',
|
||||
kind: 'script',
|
||||
target_rel_path: 'hud.gsc',
|
||||
version: { version: 1, release_version: '1.0.0', sha256: 'old-hash' }
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
flushSync();
|
||||
const input = document.querySelector<HTMLInputElement>('input[type="file"]:not([accept])')!;
|
||||
Object.defineProperty(input, 'files', {
|
||||
configurable: true,
|
||||
value: [new File(['updated script'], 'hud.gsc')]
|
||||
});
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
await vi.waitFor(() => {
|
||||
flushSync();
|
||||
expect(document.querySelector('input[required]')).not.toBeNull();
|
||||
});
|
||||
const review = [...document.querySelectorAll<HTMLButtonElement>('button')].find(
|
||||
(button) => button.textContent?.trim() === 'Review update'
|
||||
)!;
|
||||
expect(review.disabled).toBe(true);
|
||||
const version = document.querySelector<HTMLInputElement>('input[required]')!;
|
||||
version.value = '1.0.1';
|
||||
version.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
flushSync();
|
||||
expect(review.disabled).toBe(false);
|
||||
review.click();
|
||||
flushSync();
|
||||
expect(document.body.textContent).toContain('Release 1.0.0 → 1.0.1');
|
||||
const deploy = [...document.querySelectorAll<HTMLButtonElement>('button')].find(
|
||||
(button) => button.textContent?.trim() === 'Deploy revision 2'
|
||||
)!;
|
||||
deploy.click();
|
||||
await vi.waitFor(() => expect(onUpdate).toHaveBeenCalledOnce());
|
||||
expect(submitted!.get('release:4')).toBe('1.0.1');
|
||||
expect(submitted!.get('file:4')).toBeInstanceOf(File);
|
||||
});
|
||||
@@ -44,6 +44,7 @@ async function attachBeta(state = 'running', outcome?: string, deleted = false)
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { script, version } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Protected beta',
|
||||
targetRelPath: 'protected.gsc',
|
||||
scopeType: 'game',
|
||||
@@ -171,6 +172,7 @@ describe('active beta destruction guard', () => {
|
||||
expect(queued.ok).toBe(true);
|
||||
if (!queued.ok) throw new Error('Expected queued removal');
|
||||
const { script, version } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Beta admission',
|
||||
targetRelPath: 'admission.gsc',
|
||||
scopeType: 'game',
|
||||
@@ -221,6 +223,7 @@ describe('active beta destruction guard', () => {
|
||||
.prepare('UPDATE server_deployments SET state = ?, stage = ? WHERE id = ?')
|
||||
.run(state, stage, operation);
|
||||
const { script, version } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Beta admission',
|
||||
targetRelPath: 'admission.gsc',
|
||||
scopeType: 'game',
|
||||
|
||||
@@ -18,6 +18,7 @@ async function seedBeta(multiple: boolean) {
|
||||
const games = multiple ? ['t5', 't6'] : ['t5'];
|
||||
const scripts = games.map((game) => {
|
||||
const { script } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: `${game} test`,
|
||||
targetRelPath: `${game}.gsc`,
|
||||
scopeType: 'game',
|
||||
|
||||
@@ -28,6 +28,7 @@ async function setup() {
|
||||
const { destOfKind, configFileOf } = await import('../../src/lib/server/config.js');
|
||||
const create = (game: string, path = 'shared.gsc') =>
|
||||
library.createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: `${game} ${path}`,
|
||||
targetRelPath: path,
|
||||
scopeType: 'game',
|
||||
@@ -82,7 +83,11 @@ describe('multi-game beta bundles', () => {
|
||||
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') }
|
||||
{
|
||||
scriptId: files[1].script.id,
|
||||
releaseVersion: '1.0.1',
|
||||
content: Buffer.from('t6 revised')
|
||||
}
|
||||
]);
|
||||
expect(
|
||||
x.stagedFileChanges('beta', String(betaId)).map((row) => [row.server, row.rel_path])
|
||||
@@ -115,7 +120,12 @@ describe('multi-game beta bundles', () => {
|
||||
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`))
|
||||
x.uploadNewVersion(
|
||||
file.script.id,
|
||||
Buffer.from(`${file.script.scope_value} original`),
|
||||
undefined,
|
||||
'1.0.1'
|
||||
)
|
||||
);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
x.db
|
||||
@@ -221,6 +231,7 @@ 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({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Shared helper',
|
||||
targetRelPath: 'helper.gsc',
|
||||
content: Buffer.from('shared')
|
||||
@@ -250,6 +261,7 @@ describe('multi-game beta bundles', () => {
|
||||
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({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Map overlap',
|
||||
targetRelPath: 'shared.gsc',
|
||||
scopeType: 'map',
|
||||
@@ -260,6 +272,7 @@ describe('multi-game beta bundles', () => {
|
||||
kind: 'duplicate-target'
|
||||
});
|
||||
const global = x.createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Global overlap',
|
||||
targetRelPath: 'shared.gsc',
|
||||
content: Buffer.from('overlap')
|
||||
|
||||
@@ -84,6 +84,7 @@ async function openPlan(plan: BetaPlan, servers = [names[0]]) {
|
||||
const { openBeta } = await import('../../src/lib/server/fleet/beta.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { script } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Beta test',
|
||||
targetRelPath: 'test.gsc',
|
||||
content: Buffer.from('main() {}')
|
||||
@@ -148,6 +149,7 @@ describe('durable beta test plans', () => {
|
||||
fileScope === 'game' ? otherGame : otherMap
|
||||
].map((scopeValue) =>
|
||||
createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: `Test ${scopeValue}`,
|
||||
targetRelPath: `${scopeValue}.gsc`,
|
||||
scopeType: fileScope,
|
||||
@@ -218,7 +220,11 @@ describe('durable beta test plans', () => {
|
||||
const file = listBetaScripts(id)[0];
|
||||
expect(getBeta(id)?.state).toBe('ready-promote');
|
||||
await reviseBeta(id, 1, [
|
||||
{ scriptId: file.script_id, content: Buffer.from('main() { /* fixed */ }') }
|
||||
{
|
||||
scriptId: file.script_id,
|
||||
releaseVersion: '1.0.1',
|
||||
content: Buffer.from('main() { /* fixed */ }')
|
||||
}
|
||||
]);
|
||||
const replacement = listBetaScripts(id)[0].script_version_id;
|
||||
expect(replacement).not.toBe(file.script_version_id);
|
||||
@@ -613,6 +619,7 @@ describe('durable beta test plans', () => {
|
||||
const { openBeta, getBeta, listBetaServers } =
|
||||
await import('../../src/lib/server/fleet/beta.js');
|
||||
const { script, version } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Legacy beta',
|
||||
targetRelPath: 'legacy.gsc',
|
||||
content: Buffer.from('main() {}')
|
||||
|
||||
@@ -28,6 +28,7 @@ async function ready(kind: 'script' | 'baseline' = 'script', scope: 'game' | 'ma
|
||||
const { openBeta } = await import('../../src/lib/server/fleet/beta.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { script } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Review test',
|
||||
kind,
|
||||
targetRelPath: kind === 'script' ? 'review.gsc' : undefined,
|
||||
|
||||
@@ -42,6 +42,7 @@ async function setup(
|
||||
await writeFile(path, options.original);
|
||||
}
|
||||
const { script, version } = scripts.createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Revision test',
|
||||
targetRelPath: kind === 'script' ? 'revision.gsc' : undefined,
|
||||
kind,
|
||||
@@ -89,7 +90,13 @@ describe('beta revisions', () => {
|
||||
x.reviseBeta(
|
||||
x.betaId,
|
||||
1,
|
||||
[{ scriptId: x.script.id, content: Buffer.from('revision two') }],
|
||||
[
|
||||
{
|
||||
scriptId: x.script.id,
|
||||
releaseVersion: '2.0.0',
|
||||
content: Buffer.from('revision two')
|
||||
}
|
||||
],
|
||||
'Small fix'
|
||||
)
|
||||
).resolves.toEqual({ revision: 2 });
|
||||
@@ -132,7 +139,11 @@ describe('beta revisions', () => {
|
||||
await activateFiles('beta', x.betaId);
|
||||
for (let revision = 2; revision <= 3; revision++) {
|
||||
await x.reviseBeta(x.betaId, revision - 1, [
|
||||
{ scriptId: x.script.id, content: Buffer.from(`revision ${revision}`) }
|
||||
{
|
||||
scriptId: x.script.id,
|
||||
releaseVersion: `${revision}.0.0`,
|
||||
content: Buffer.from(`revision ${revision}`)
|
||||
}
|
||||
]);
|
||||
await activateFiles('beta', x.betaId);
|
||||
}
|
||||
@@ -157,7 +168,11 @@ describe('beta revisions', () => {
|
||||
await activateFiles('beta', x.betaId);
|
||||
}
|
||||
await x.reviseBeta(x.betaId, 1, [
|
||||
{ scriptId: x.script.id, content: Buffer.from('pending replacement') }
|
||||
{
|
||||
scriptId: x.script.id,
|
||||
releaseVersion: '2.0.0',
|
||||
content: Buffer.from('pending replacement')
|
||||
}
|
||||
]);
|
||||
if (waves)
|
||||
expect(x.listBetaServers(x.betaId).map((row) => row.stage)).toEqual([
|
||||
@@ -174,7 +189,9 @@ describe('beta revisions', () => {
|
||||
|
||||
it('does not restore or delete files when no revision was ever activated', async () => {
|
||||
const x = await setup({ original: 'untouched' });
|
||||
await x.reviseBeta(x.betaId, 1, [{ scriptId: x.script.id, content: Buffer.from('new') }]);
|
||||
await x.reviseBeta(x.betaId, 1, [
|
||||
{ scriptId: x.script.id, releaseVersion: '2.0.0', content: Buffer.from('new') }
|
||||
]);
|
||||
expect(await x.rollbackBeta(x.betaId)).toMatchObject({ deploymentId: null });
|
||||
expect(await readFile(x.path, 'utf8')).toBe('untouched');
|
||||
});
|
||||
@@ -183,11 +200,16 @@ describe('beta revisions', () => {
|
||||
const x = await setup({ original: 'tracked original' });
|
||||
const { recordScript, getServerScript } =
|
||||
await import('../../src/lib/server/fleet/inventory.js');
|
||||
const prior = x.uploadNewVersion(x.script.id, Buffer.from('tracked original'));
|
||||
const prior = x.uploadNewVersion(
|
||||
x.script.id,
|
||||
Buffer.from('tracked original'),
|
||||
undefined,
|
||||
'1.0.1'
|
||||
);
|
||||
recordScript(names[0], 'revision.gsc', prior.id, prior.sha256, prior.size, 'test');
|
||||
await activateFiles('beta', x.betaId);
|
||||
await x.reviseBeta(x.betaId, 1, [
|
||||
{ scriptId: x.script.id, content: Buffer.from('replacement') }
|
||||
{ scriptId: x.script.id, releaseVersion: '2.0.0', content: Buffer.from('replacement') }
|
||||
]);
|
||||
await activateFiles('beta', x.betaId);
|
||||
expect(x.db.prepare('SELECT prior_version_id FROM beta_server_scripts').get()).toEqual({
|
||||
@@ -210,7 +232,7 @@ describe('beta revisions', () => {
|
||||
});
|
||||
x.db.prepare("UPDATE beta_test_plans SET outcome = 'paused' WHERE beta_id = ?").run(betaId);
|
||||
await x.reviseBeta(betaId, 1, [
|
||||
{ scriptId: x.script.id, content: Buffer.from('replacement') }
|
||||
{ scriptId: x.script.id, releaseVersion: '2.0.0', content: Buffer.from('replacement') }
|
||||
]);
|
||||
expect(x.listBetaServers(betaId).map((row) => [row.server, row.stage])).toEqual([
|
||||
[names[0], 'queued'],
|
||||
@@ -229,7 +251,11 @@ describe('beta revisions', () => {
|
||||
releaseRestartRequest(names[0], { kind: 'beta', id: String(x.betaId) });
|
||||
await expect(
|
||||
x.reviseBeta(x.betaId, 1, [
|
||||
{ scriptId: x.script.id, content: Buffer.from('replacement') }
|
||||
{
|
||||
scriptId: x.script.id,
|
||||
releaseVersion: '2.0.0',
|
||||
content: Buffer.from('replacement')
|
||||
}
|
||||
])
|
||||
).resolves.toEqual({ revision: 2 });
|
||||
});
|
||||
@@ -238,7 +264,11 @@ describe('beta revisions', () => {
|
||||
const x = await setup({ original: 'original config', kind: 'baseline' });
|
||||
await activateFiles('beta', x.betaId);
|
||||
await x.reviseBeta(x.betaId, 1, [
|
||||
{ scriptId: x.script.id, content: Buffer.from('set sv_voice "0"\n') }
|
||||
{
|
||||
scriptId: x.script.id,
|
||||
releaseVersion: '2.0.0',
|
||||
content: Buffer.from('set sv_voice "0"\n')
|
||||
}
|
||||
]);
|
||||
await activateFiles('beta', x.betaId);
|
||||
expect(await readFile(x.path, 'utf8')).toContain('set sv_voice "0"');
|
||||
@@ -259,7 +289,11 @@ describe('beta revisions', () => {
|
||||
'activating'
|
||||
])('rejects %s updates without saving a revision', async (reason) => {
|
||||
const x = await setup();
|
||||
const upload = { scriptId: x.script.id, content: Buffer.from('changed') };
|
||||
const upload = {
|
||||
scriptId: x.script.id,
|
||||
releaseVersion: '2.0.0',
|
||||
content: Buffer.from('changed')
|
||||
};
|
||||
if (reason === 'identical')
|
||||
upload.content = Buffer.from(
|
||||
(await import('../../src/lib/server/fleet/scripts.js')).getVersionContent(
|
||||
@@ -301,7 +335,7 @@ describe('beta revisions', () => {
|
||||
x.db.prepare('UPDATE betas SET opened_at = ?').run('2000-01-01T00:00:00Z');
|
||||
x.db.prepare("UPDATE beta_test_plans SET outcome = 'inconclusive'").run();
|
||||
await x.reviseBeta(x.betaId, 1, [
|
||||
{ scriptId: x.script.id, content: Buffer.from('changed') }
|
||||
{ scriptId: x.script.id, releaseVersion: '2.0.0', content: Buffer.from('changed') }
|
||||
]);
|
||||
const { closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
closeDb();
|
||||
@@ -320,7 +354,7 @@ describe('beta revisions', () => {
|
||||
const x = await setup({ legacy: true });
|
||||
await activateFiles('beta', x.betaId);
|
||||
await x.reviseBeta(x.betaId, 1, [
|
||||
{ scriptId: x.script.id, content: Buffer.from('changed') }
|
||||
{ scriptId: x.script.id, releaseVersion: '2.0.0', content: Buffer.from('changed') }
|
||||
]);
|
||||
expect(x.listBetaServers(x.betaId)[0]).toMatchObject({
|
||||
stage: 'awaiting-empty',
|
||||
@@ -346,8 +380,16 @@ describe('beta revisions', () => {
|
||||
.run(x.betaId, overlay.script.id, overlay.version.id);
|
||||
await expect(
|
||||
x.reviseBeta(x.betaId, 1, [
|
||||
{ scriptId: x.script.id, content: Buffer.from('valid new version') },
|
||||
{ scriptId: overlay.script.id, content: Buffer.from('invalid config') }
|
||||
{
|
||||
scriptId: x.script.id,
|
||||
releaseVersion: '2.0.0',
|
||||
content: Buffer.from('valid new version')
|
||||
},
|
||||
{
|
||||
scriptId: overlay.script.id,
|
||||
releaseVersion: '2.0.0',
|
||||
content: Buffer.from('invalid config')
|
||||
}
|
||||
])
|
||||
).rejects.toThrow('at least one');
|
||||
expect(x.listVersions(x.script.id)).toHaveLength(1);
|
||||
@@ -365,6 +407,7 @@ describe('beta revisions', () => {
|
||||
const form = new FormData();
|
||||
form.set('expectedRevision', '1');
|
||||
form.set('notes', 'Fix scoring');
|
||||
form.set(`release:${x.script.id}`, '2.0.0');
|
||||
form.set(`file:${x.script.id}`, new File(['changed'], 'revision.gsc'));
|
||||
const post = () =>
|
||||
POST({
|
||||
@@ -380,4 +423,25 @@ describe('beta revisions', () => {
|
||||
});
|
||||
expect(await download.text()).toBe('revision one');
|
||||
});
|
||||
it.each([undefined, 'bad', '1.0.0'])(
|
||||
'rejects invalid replacement release %s without modifying the beta',
|
||||
async (release) => {
|
||||
const x = await setup();
|
||||
const { POST } = await import('../../src/routes/api/betas/[id]/revisions/+server.js');
|
||||
const before = x.getBeta(x.betaId);
|
||||
const form = new FormData();
|
||||
form.set('expectedRevision', '1');
|
||||
form.set(`file:${x.script.id}`, new File(['changed'], 'revision.gsc'));
|
||||
if (release !== undefined) form.set(`release:${x.script.id}`, release);
|
||||
const response = await POST({
|
||||
params: { id: String(x.betaId) },
|
||||
request: new Request('http://test/revisions', { method: 'POST', body: form })
|
||||
});
|
||||
expect(response.status).toBe(release === '1.0.0' ? 409 : 400);
|
||||
expect(x.getBeta(x.betaId)).toEqual(before);
|
||||
expect(x.listVersions(x.script.id)).toHaveLength(1);
|
||||
expect(x.db.prepare('SELECT * FROM beta_revisions').all()).toEqual([]);
|
||||
expect(x.listBetaScripts(x.betaId)[0].script_version_id).toBe(x.version.id);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ afterEach(async () => {
|
||||
async function file(scopeType: 'game' | 'map' = 'game', scopeValue = 't5') {
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
return createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: `Test ${scopeValue}`,
|
||||
targetRelPath: `${scopeValue}.gsc`,
|
||||
scopeType,
|
||||
|
||||
@@ -22,6 +22,7 @@ async function open(kind: 'script' | 'plugin' | 'baseline' = 'script') {
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const { openBeta } = await import('../../src/lib/server/fleet/beta.js');
|
||||
const { script, version } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: `Staged ${kind}`,
|
||||
kind,
|
||||
targetRelPath:
|
||||
@@ -274,7 +275,12 @@ describe('staged beta files', () => {
|
||||
)
|
||||
.run(promotion.deploymentId);
|
||||
const { uploadNewVersion } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const newer = uploadNewVersion(script.id, Buffer.from('newer beta bytes'));
|
||||
const newer = uploadNewVersion(
|
||||
script.id,
|
||||
Buffer.from('newer beta bytes'),
|
||||
undefined,
|
||||
'1.0.1'
|
||||
);
|
||||
const request = openBeta({
|
||||
scripts: [{ scriptId: script.id, scriptVersionId: newer.id }],
|
||||
servers: [servers[1]],
|
||||
@@ -296,7 +302,7 @@ describe('staged beta files', () => {
|
||||
const { betaId: olderBeta, script } = await open(kind);
|
||||
const { uploadNewVersion } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const newestBytes = kind === 'script' ? 'newest tested bytes' : 'set sv_voice "0"\n';
|
||||
const newer = uploadNewVersion(script.id, Buffer.from(newestBytes));
|
||||
const newer = uploadNewVersion(script.id, Buffer.from(newestBytes), undefined, '1.0.2');
|
||||
const { openBeta, promoteBeta } = await import('../../src/lib/server/fleet/beta.js');
|
||||
const { betaId: newerBeta } = await openBeta({
|
||||
scripts: [{ scriptId: script.id, scriptVersionId: newer.id }],
|
||||
|
||||
@@ -149,6 +149,7 @@ describe('config workbench API', () => {
|
||||
content: Buffer.from('set b 1')
|
||||
});
|
||||
const script = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Script',
|
||||
targetRelPath: 'test.gsc',
|
||||
content: Buffer.from('code')
|
||||
|
||||
@@ -28,6 +28,7 @@ async function open(planned = false, servers = [names[0]]) {
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const { openBeta } = await import('../../src/lib/server/fleet/beta.js');
|
||||
const { script, version } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Retained history',
|
||||
targetRelPath: 'history.gsc',
|
||||
scopeType: 'game',
|
||||
|
||||
@@ -92,6 +92,7 @@ describe('multi-server blueprints', () => {
|
||||
const { previewCreation } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
const artifact = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Telemetry',
|
||||
targetRelPath: 'telemetry.gsc',
|
||||
scopeType: 'game',
|
||||
@@ -107,7 +108,12 @@ describe('multi-server blueprints', () => {
|
||||
'INSERT INTO server_blueprints (id,name,created_at,recipe) VALUES (?, ?, ?, ?)'
|
||||
)
|
||||
.run('legacy', 'Old favorite', new Date().toISOString(), JSON.stringify(recipe));
|
||||
const current = uploadNewVersion(artifact.script.id, Buffer.from('current telemetry'));
|
||||
const current = uploadNewVersion(
|
||||
artifact.script.id,
|
||||
Buffer.from('current telemetry'),
|
||||
undefined,
|
||||
'1.0.1'
|
||||
);
|
||||
db()
|
||||
.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?')
|
||||
.run(current.id, artifact.script.id);
|
||||
|
||||
@@ -255,6 +255,7 @@ describe('drift scanning', () => {
|
||||
const server = 't5_zombie_theater_1';
|
||||
const relPath = kind === 'script' ? 'outdated.gsc' : 'outdated.dll';
|
||||
const { script, version } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: `outdated ${kind}`,
|
||||
kind,
|
||||
scopeType: 'game',
|
||||
@@ -295,6 +296,7 @@ describe('drift scanning', () => {
|
||||
const relPath = kind === 'script' ? 'managed.gsc' : 'managed.dll';
|
||||
const content = Buffer.from('promoted content');
|
||||
const { script, version } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: `managed ${kind}`,
|
||||
kind,
|
||||
scopeType: 'game',
|
||||
@@ -465,6 +467,7 @@ describe('drift scanning', () => {
|
||||
const content = Buffer.from('main(){}\n');
|
||||
const before = mode === 'identical' ? content : Buffer.from('old bytes');
|
||||
const { script, version } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'spawn fix',
|
||||
kind: 'script',
|
||||
scopeType: 'game',
|
||||
@@ -543,6 +546,7 @@ describe('drift rebuild campaigns', () => {
|
||||
await seedServerRegistry();
|
||||
const server = 't5_zombie_theater_1';
|
||||
const { script, version } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'managed',
|
||||
kind: 'script',
|
||||
scopeType: 'game',
|
||||
@@ -620,6 +624,7 @@ describe('drift rebuild campaigns', () => {
|
||||
// off disk so drift's "missing" pass has something to find.
|
||||
async function promoteAndOrphan(
|
||||
createScript: (i: {
|
||||
releaseVersion?: string;
|
||||
name: string;
|
||||
kind: 'script' | 'plugin' | 'baseline' | 'config';
|
||||
scopeType?: 'game' | 'map';
|
||||
@@ -638,6 +643,7 @@ async function promoteAndOrphan(
|
||||
db: () => { prepare: (q: string) => { run: (...args: unknown[]) => unknown } }
|
||||
): Promise<{ scriptId: number }> {
|
||||
const created = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 't5 hud',
|
||||
kind: 'script',
|
||||
scopeType: 'game',
|
||||
|
||||
@@ -244,7 +244,12 @@ describe('GitHub DLL imports', () => {
|
||||
await expect(importPluginRelease({ url, assetId: 999, name: 'Utilities' })).rejects.toThrow(
|
||||
/no longer available/
|
||||
);
|
||||
const script = createScript({ name: 'Script', targetRelPath: 's.gsc', content });
|
||||
const script = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Script',
|
||||
targetRelPath: 's.gsc',
|
||||
content
|
||||
});
|
||||
await expect(
|
||||
importPluginRelease({ url, assetId: 200, pluginId: script.script.id })
|
||||
).rejects.toMatchObject({ status: 404 });
|
||||
|
||||
@@ -51,6 +51,7 @@ async function seedArtifacts() {
|
||||
content: Buffer.from('set sv_voice "1"\n')
|
||||
});
|
||||
const script = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Classic perks',
|
||||
kind: 'script',
|
||||
scopeType: 'game',
|
||||
|
||||
@@ -20,6 +20,7 @@ describe('scope impact preview', () => {
|
||||
await import('../../src/lib/server/fleet/inventory.js');
|
||||
const { GET } = await import('../../src/routes/api/scripts/[id]/scope/+server.js');
|
||||
const script = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Example',
|
||||
scopeType: 'game',
|
||||
scopeValue: 't5',
|
||||
|
||||
@@ -55,6 +55,7 @@ async function setup(capable = true) {
|
||||
targetRelPath?: string
|
||||
) {
|
||||
const artifact = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name,
|
||||
kind,
|
||||
targetRelPath,
|
||||
|
||||
@@ -275,6 +275,7 @@ describe('asynchronous native launch preparation', () => {
|
||||
kind === 'baseline'
|
||||
? baseline
|
||||
: createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: `Launch ${kind}`,
|
||||
kind,
|
||||
targetRelPath:
|
||||
@@ -299,7 +300,7 @@ describe('asynchronous native launch preparation', () => {
|
||||
? 'set rcon_password "password"\nset sv_voice "0"\n'
|
||||
: 'newly promoted file'
|
||||
);
|
||||
const latest = uploadNewVersion(script.id, content);
|
||||
const latest = uploadNewVersion(script.id, content, undefined, '1.0.1');
|
||||
db()
|
||||
.prepare('UPDATE scripts SET current_version_id=? WHERE id=?')
|
||||
.run(latest.id, script.id);
|
||||
|
||||
@@ -188,6 +188,7 @@ describe('rebalance instance migrations', () => {
|
||||
content: Buffer.from('set sv_voice "1"\n')
|
||||
});
|
||||
const script = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Deployed script',
|
||||
kind: 'script',
|
||||
targetRelPath: 'preserved.gsc',
|
||||
@@ -208,7 +209,9 @@ describe('rebalance instance migrations', () => {
|
||||
await acknowledge(job.deploymentId);
|
||||
const patched = uploadNewVersion(
|
||||
script.script.id,
|
||||
Buffer.from('confirmed patch after initial launch')
|
||||
Buffer.from('confirmed patch after initial launch'),
|
||||
undefined,
|
||||
'1.0.1'
|
||||
);
|
||||
const { recordScript } = await import('../../src/lib/server/fleet/inventory.js');
|
||||
recordScript(
|
||||
@@ -221,7 +224,9 @@ describe('rebalance instance migrations', () => {
|
||||
);
|
||||
const newest = uploadNewVersion(
|
||||
script.script.id,
|
||||
Buffer.from('new promotion must not be installed')
|
||||
Buffer.from('new promotion must not be installed'),
|
||||
undefined,
|
||||
'1.0.2'
|
||||
);
|
||||
db()
|
||||
.prepare('UPDATE scripts SET current_version_id=? WHERE id=?')
|
||||
@@ -3418,6 +3423,7 @@ describe('native provisioning cancellation and rollback', () => {
|
||||
const { reportHostOperation } = await import('../../src/lib/server/hosts/index.js');
|
||||
const { rollbackBeta } = await import('../../src/lib/server/fleet/beta.js');
|
||||
const { script, version } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Broken beta',
|
||||
kind: 'script',
|
||||
targetRelPath: 'beta.gsc',
|
||||
@@ -3774,6 +3780,7 @@ describe('cancelled native mutation bookkeeping', () => {
|
||||
const { reportHostOperation, getHostOperation } =
|
||||
await import('../../src/lib/server/hosts/index.js');
|
||||
const { script, version } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Revised beta',
|
||||
kind: 'script',
|
||||
targetRelPath: 'revision.gsc',
|
||||
@@ -3804,7 +3811,11 @@ describe('cancelled native mutation bookkeeping', () => {
|
||||
// A dispatched activation must settle before revision submission.
|
||||
await expect(
|
||||
reviseBeta(betaId, 1, [
|
||||
{ scriptId: script.id, content: Buffer.from('premature update') }
|
||||
{
|
||||
scriptId: script.id,
|
||||
releaseVersion: '1.0.1',
|
||||
content: Buffer.from('premature update')
|
||||
}
|
||||
])
|
||||
).rejects.toThrow();
|
||||
const activation = (await operationFor(restart.id))!;
|
||||
@@ -3823,7 +3834,11 @@ describe('cancelled native mutation bookkeeping', () => {
|
||||
}
|
||||
await activate(original);
|
||||
await reviseBeta(betaId, 1, [
|
||||
{ scriptId: script.id, content: Buffer.from('revision two') }
|
||||
{
|
||||
scriptId: script.id,
|
||||
releaseVersion: '1.0.1',
|
||||
content: Buffer.from('revision two')
|
||||
}
|
||||
]);
|
||||
await activate('revision one');
|
||||
const rollback = await rollbackBeta(betaId);
|
||||
|
||||
@@ -165,6 +165,7 @@ describe('native disruption safety at dispatch', () => {
|
||||
const pending = await suspendInspection();
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const { script, version } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Race beta',
|
||||
kind: 'script',
|
||||
targetRelPath: 'beta.gsc',
|
||||
|
||||
@@ -29,6 +29,7 @@ beforeEach(async () => {
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { script, version } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'HUD',
|
||||
targetRelPath: 'hud.gsc',
|
||||
content: Buffer.from('main(){}')
|
||||
|
||||
@@ -34,6 +34,7 @@ async function promotion(kind: 'script' | 'plugin' | 'baseline', staged: boolean
|
||||
? 'managed.dll'
|
||||
: 'managed.gsc';
|
||||
const { script, version: prior } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'managed',
|
||||
kind,
|
||||
targetRelPath: relPath,
|
||||
@@ -41,7 +42,7 @@ async function promotion(kind: 'script' | 'plugin' | 'baseline', staged: boolean
|
||||
scopeValue: 't5',
|
||||
content: before
|
||||
});
|
||||
const version = uploadNewVersion(script.id, after);
|
||||
const version = uploadNewVersion(script.id, after, undefined, '1.0.1');
|
||||
db()
|
||||
.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?')
|
||||
.run(version.id, script.id);
|
||||
|
||||
@@ -63,6 +63,7 @@ describe('editing promoted script scopes', () => {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const scopeValue = scopeType === 'game' ? 't5' : 'zombie_theater';
|
||||
const { script, version } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'restore scope',
|
||||
targetRelPath: 'restore.gsc',
|
||||
scopeType,
|
||||
@@ -135,6 +136,7 @@ describe('editing promoted script scopes', () => {
|
||||
const { openBeta } = await import('../../src/lib/server/fleet/beta.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { script, version } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'active scope',
|
||||
targetRelPath: 'active.gsc',
|
||||
scopeType: 'game',
|
||||
@@ -179,6 +181,7 @@ describe('promote creates a deployment', () => {
|
||||
|
||||
// A game-scoped script matches both fixture servers.
|
||||
const { script } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'hud tweaks',
|
||||
targetRelPath: 'hud.gsc',
|
||||
scopeType: 'game',
|
||||
@@ -268,6 +271,7 @@ describe('rollback beta on ready-promote marks version test-failed', () => {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
|
||||
const { script } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'flag experiment',
|
||||
targetRelPath: 'flag.gsc',
|
||||
scopeType: 'map',
|
||||
@@ -310,6 +314,7 @@ describe('rollback promote deployment', () => {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
|
||||
const { script } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'roll me back',
|
||||
targetRelPath: 'rb.gsc',
|
||||
scopeType: 'game',
|
||||
@@ -379,6 +384,7 @@ describe('multi-script beta', () => {
|
||||
|
||||
// Two scripts, both scope=game=t5 so they both land on both fixture servers.
|
||||
const a = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'hud',
|
||||
targetRelPath: 'hud.gsc',
|
||||
scopeType: 'game',
|
||||
@@ -386,6 +392,7 @@ describe('multi-script beta', () => {
|
||||
content: Buffer.from('hud(){}\n')
|
||||
});
|
||||
const b = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'flag',
|
||||
targetRelPath: 'flag.gsc',
|
||||
scopeType: 'game',
|
||||
|
||||
@@ -10,8 +10,9 @@ beforeEach(async () => {
|
||||
afterEach(async () => {
|
||||
await fixture.cleanup();
|
||||
});
|
||||
function request(content: string, reuse = false) {
|
||||
function request(content: string, reuse = false, releaseVersion = '1.0.0') {
|
||||
const form = new FormData();
|
||||
form.append('releaseVersion', releaseVersion);
|
||||
form.append('content', new File([content], 'hud.gsc'));
|
||||
if (reuse) form.append('reuseExisting', 'true');
|
||||
return new Request('http://localhost/api/scripts/inspect', { method: 'POST', body: form });
|
||||
@@ -21,6 +22,7 @@ async function seed() {
|
||||
return {
|
||||
...mod,
|
||||
...mod.createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'HUD',
|
||||
targetRelPath: 'hud.gsc',
|
||||
scopeType: 'game',
|
||||
@@ -33,7 +35,7 @@ async function seed() {
|
||||
describe('script upload review', () => {
|
||||
it('inspects content against all versions without creating records or including plugins', async () => {
|
||||
const { script, version, uploadNewVersion, createScript, listVersions } = await seed();
|
||||
uploadNewVersion(script.id, Buffer.from('changed(){}'));
|
||||
uploadNewVersion(script.id, Buffer.from('changed(){}'), undefined, '1.0.1');
|
||||
createScript({
|
||||
name: 'Plugin',
|
||||
targetRelPath: 'plugin.dll',
|
||||
@@ -44,17 +46,19 @@ describe('script upload review', () => {
|
||||
const res = await POST({ request: request('main(){}') });
|
||||
expect(await res.json()).toEqual({
|
||||
sha256: version.sha256,
|
||||
duplicates: [{ scriptId: script.id, versionId: version.id, version: 1 }]
|
||||
duplicates: [
|
||||
{ scriptId: script.id, versionId: version.id, version: 1, release_version: '1.0.0' }
|
||||
]
|
||||
});
|
||||
expect(listVersions(script.id)).toHaveLength(2);
|
||||
expect((await POST({ request: request('') })).status).toBe(400);
|
||||
});
|
||||
it('reuses identical historical content and creates changed versions while preserving scope', async () => {
|
||||
const { script, version, uploadNewVersion, listVersions, getScript } = await seed();
|
||||
uploadNewVersion(script.id, Buffer.from('changed(){}'));
|
||||
uploadNewVersion(script.id, Buffer.from('changed(){}'), undefined, '1.0.2');
|
||||
const { POST } = await import('../../src/routes/api/scripts/[id]/versions/+server.js');
|
||||
const invoke = (content: string) =>
|
||||
POST({ params: { id: String(script.id) }, request: request(content, true) });
|
||||
const invoke = (content: string, release = '1.0.0') =>
|
||||
POST({ params: { id: String(script.id) }, request: request(content, true, release) });
|
||||
const res = await invoke('main(){}');
|
||||
expect(await res.json()).toMatchObject({
|
||||
ok: true,
|
||||
@@ -62,7 +66,7 @@ describe('script upload review', () => {
|
||||
version: { id: version.id, version: 1 }
|
||||
});
|
||||
expect(listVersions(script.id)).toHaveLength(2);
|
||||
const added = await invoke('third(){}');
|
||||
const added = await invoke('third(){}', '1.1.0');
|
||||
expect(await added.json()).toMatchObject({ ok: true, version: { version: 3 } });
|
||||
expect(listVersions(script.id)).toHaveLength(3);
|
||||
expect(getScript(script.id)).toMatchObject({ scope_type: 'game', scope_value: 't5' });
|
||||
@@ -90,7 +94,7 @@ describe('script upload review', () => {
|
||||
});
|
||||
it('lists promoted and latest version metadata separately without file contents', async () => {
|
||||
const { script, version, uploadNewVersion } = await seed();
|
||||
const latest = uploadNewVersion(script.id, Buffer.from('changed(){}'));
|
||||
const latest = uploadNewVersion(script.id, Buffer.from('changed(){}'), undefined, '1.0.3');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
db()
|
||||
.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?')
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
||||
import {
|
||||
manifestVersion,
|
||||
parseScriptVersions,
|
||||
scriptVersionError
|
||||
} from '../../src/lib/script-version.js';
|
||||
import { artifactVersionLabel } from '../../src/lib/artifact-version.js';
|
||||
|
||||
it('validates numeric release versions and exact manifest filenames', () => {
|
||||
for (const value of [
|
||||
undefined,
|
||||
null,
|
||||
'',
|
||||
' ',
|
||||
'v1.0.0',
|
||||
'01.0.0',
|
||||
'1.0',
|
||||
'1.0.0-beta',
|
||||
'1.0.0\nextra',
|
||||
'1.'.repeat(50)
|
||||
])
|
||||
expect(scriptVersionError(value)).not.toBe('');
|
||||
for (const value of ['0.0.0', '1.20.3', ' 1.2.0 ']) expect(scriptVersionError(value)).toBe('');
|
||||
expect(manifestVersion({}, 'constructor')).toBe('');
|
||||
expect(parseScriptVersions('{"hud.gsc":"1.2.0","HUD.gsc":"2.0.0","t5-plugin":"tag"}')).toEqual({
|
||||
'hud.gsc': '1.2.0',
|
||||
'HUD.gsc': '2.0.0'
|
||||
});
|
||||
for (const text of [
|
||||
'oops',
|
||||
'[]',
|
||||
'null',
|
||||
'{}',
|
||||
'{"hud.gsc":1}',
|
||||
'{"hud.gsc":"1"}',
|
||||
'{"path/hud.gsc":"1.0.0"}'
|
||||
])
|
||||
expect(() => parseScriptVersions(text)).toThrow();
|
||||
});
|
||||
|
||||
it('distinguishes historical script uploads from publisher versions', () => {
|
||||
expect(artifactVersionLabel({ version: 3 }, 'script')).toBe('Unversioned (upload 3)');
|
||||
expect(artifactVersionLabel({ version: 3, release_version: '1.2.0' }, 'script')).toBe(
|
||||
'1.2.0 (upload 3)'
|
||||
);
|
||||
expect(artifactVersionLabel({ version: 3 }, 'config')).toBe('v3');
|
||||
});
|
||||
|
||||
let fixture: Fixture;
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
fixture = await newFixture();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await fixture.cleanup();
|
||||
});
|
||||
const script = { name: 'HUD', targetRelPath: 'hud.gsc', content: Buffer.from('main(){}') };
|
||||
|
||||
describe('mandatory script releases', () => {
|
||||
it('rejects missing and invalid versions without creating records', async () => {
|
||||
const { createScript, uploadNewVersion, listScripts, listVersions } =
|
||||
await import('../../src/lib/server/fleet/scripts.js');
|
||||
for (const releaseVersion of [undefined, '', 'v1.0.0', '1.0', '1.0.0-rc1']) {
|
||||
expect(() => createScript({ ...script, releaseVersion })).toThrow(/release version/);
|
||||
}
|
||||
expect(listScripts()).toHaveLength(0);
|
||||
const saved = createScript({ ...script, releaseVersion: ' 1.0.0 ' });
|
||||
expect(saved.version.release_version).toBe('1.0.0');
|
||||
expect(() => uploadNewVersion(saved.script.id, script.content)).toThrow(/required/);
|
||||
expect(listVersions(saved.script.id)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('reuses identical releases, rejects changed contents and accepts older versions', async () => {
|
||||
const { createScript, uploadNewVersion, listVersions } =
|
||||
await import('../../src/lib/server/fleet/scripts.js');
|
||||
const saved = createScript({ ...script, releaseVersion: '2.0.0' });
|
||||
expect(uploadNewVersion(saved.script.id, script.content, 'retry', '2.0.0')).toEqual(
|
||||
saved.version
|
||||
);
|
||||
expect(() =>
|
||||
uploadNewVersion(saved.script.id, Buffer.from('changed'), undefined, '2.0.0')
|
||||
).toThrow(/Bump/);
|
||||
const older = uploadNewVersion(
|
||||
saved.script.id,
|
||||
Buffer.from('old release'),
|
||||
undefined,
|
||||
'1.0.0'
|
||||
);
|
||||
expect(older).toMatchObject({ version: 2, release_version: '1.0.0' });
|
||||
expect(listVersions(saved.script.id)).toHaveLength(2);
|
||||
expect(
|
||||
createScript({ ...script, name: 'Other', releaseVersion: '2.0.0' }).version
|
||||
.release_version
|
||||
).toBe('2.0.0');
|
||||
});
|
||||
|
||||
it('preserves legacy content and does not infer a release from matching bytes', async () => {
|
||||
const { createScript, uploadNewVersion, getVersion, getVersionContent } =
|
||||
await import('../../src/lib/server/fleet/scripts.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const saved = createScript({ ...script, releaseVersion: '1.0.0' });
|
||||
db()
|
||||
.prepare('UPDATE script_versions SET release_version = NULL WHERE id = ?')
|
||||
.run(saved.version.id);
|
||||
const next = uploadNewVersion(saved.script.id, script.content, undefined, '1.0.0');
|
||||
expect(next).toMatchObject({ version: 2, release_version: '1.0.0' });
|
||||
expect(getVersion(saved.version.id)!.release_version).toBeNull();
|
||||
expect(getVersionContent(saved.version.id)).toEqual(script.content);
|
||||
});
|
||||
|
||||
it('enforces the API contract even when reuseExisting is requested', async () => {
|
||||
const create = await import('../../src/routes/api/scripts/+server.js');
|
||||
const upload = await import('../../src/routes/api/scripts/[id]/versions/+server.js');
|
||||
const { listScripts, listVersions } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const url = new URL('http://test/api/scripts');
|
||||
const form = new FormData();
|
||||
form.set('name', 'HUD');
|
||||
form.set('content', new File(['main(){}'], 'hud.gsc'));
|
||||
const request = () => new Request(url, { method: 'POST', body: form });
|
||||
expect((await create.POST({ url, request: request() })).status).toBe(400);
|
||||
expect(listScripts()).toHaveLength(0);
|
||||
form.set('releaseVersion', '1.0.0');
|
||||
const saved = await (await create.POST({ url, request: request() })).json();
|
||||
const params = { id: String(saved.script.id) };
|
||||
form.set('reuseExisting', 'true');
|
||||
form.delete('releaseVersion');
|
||||
expect((await upload.POST({ params, request: request() })).status).toBe(400);
|
||||
form.set('releaseVersion', '1.0.0');
|
||||
expect(await (await upload.POST({ params, request: request() })).json()).toMatchObject({
|
||||
reused: true,
|
||||
version: { id: saved.versionId, release_version: '1.0.0' }
|
||||
});
|
||||
form.set('content', new File(['different'], 'hud.gsc'));
|
||||
expect((await upload.POST({ params, request: request() })).status).toBe(409);
|
||||
expect(listVersions(saved.script.id)).toHaveLength(1);
|
||||
form.set('releaseVersion', '0.9.0');
|
||||
expect((await upload.POST({ params, request: request() })).status).toBe(200);
|
||||
});
|
||||
});
|
||||
@@ -68,7 +68,7 @@ describe('server detail data', () => {
|
||||
const { script, version } = createScript({
|
||||
name: 'Plugin',
|
||||
kind: 'plugin',
|
||||
releaseVersion: 'v1.0.0',
|
||||
releaseVersion: '1.0.0',
|
||||
targetRelPath: 'native.bin',
|
||||
content: Buffer.from('plugin')
|
||||
});
|
||||
@@ -96,7 +96,7 @@ describe('server detail data', () => {
|
||||
).toMatchObject({
|
||||
artifactId: script.id,
|
||||
version: version.version,
|
||||
releaseVersion: 'v1.0.0',
|
||||
releaseVersion: '1.0.0',
|
||||
sha256: version.sha256
|
||||
});
|
||||
});
|
||||
@@ -113,7 +113,7 @@ describe('server detail data', () => {
|
||||
const { version } = createScript({
|
||||
name: 'Plugin',
|
||||
kind: 'plugin',
|
||||
releaseVersion: 'v1.0.0',
|
||||
releaseVersion: '1.0.0',
|
||||
targetRelPath: path,
|
||||
content: Buffer.from('plugin')
|
||||
});
|
||||
|
||||
@@ -60,6 +60,7 @@ async function queueChange(source: Source, targets = [servers[0]]) {
|
||||
const { recordScript, getServerScript } =
|
||||
await import('../../src/lib/server/fleet/inventory.js');
|
||||
const original = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'Staged script',
|
||||
kind: 'script',
|
||||
targetRelPath: relPath,
|
||||
@@ -67,7 +68,7 @@ async function queueChange(source: Source, targets = [servers[0]]) {
|
||||
scopeValue: 't5',
|
||||
content: before
|
||||
});
|
||||
const replacement = uploadNewVersion(original.script.id, after);
|
||||
const replacement = uploadNewVersion(original.script.id, after, undefined, '1.0.1');
|
||||
for (const server of targets) {
|
||||
await writeFile(destination(server), before);
|
||||
recordScript(
|
||||
|
||||
@@ -108,3 +108,12 @@ it('explains pending automatic downloads and keeps copy errors visible even for
|
||||
expect(body).toContain('Host report needs review');
|
||||
expect(body).not.toContain('Ready hosts');
|
||||
});
|
||||
|
||||
it('shows script release versions and explicitly labels historical uploads', () => {
|
||||
const current = { id: 1, version: 1, release_version: null };
|
||||
const body = render(LibraryVersions, {
|
||||
props: { kind: 'script', current, latest: { id: 2, version: 2, release_version: '1.2.0' } }
|
||||
}).body;
|
||||
expect(body).toContain('Unversioned (upload 1)');
|
||||
expect(body).toContain('1.2.0 (upload 2)');
|
||||
});
|
||||
|
||||
@@ -621,6 +621,7 @@ describe('Watchdog detection and ownership', () => {
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const { openBeta } = await import('../../src/lib/server/fleet/beta.js');
|
||||
const { script, version } = createScript({
|
||||
releaseVersion: '1.0.0',
|
||||
name: 'beta',
|
||||
content: Buffer.from('experimental'),
|
||||
targetRelPath: 'beta.gsc'
|
||||
|
||||
Reference in New Issue
Block a user