Add all eligible files to beta setup
CI / checks (push) Successful in 5m59s

This commit is contained in:
2026-10-14 19:47:31 -04:00
parent 1fc329b123
commit 68616327ec
3 changed files with 202 additions and 5 deletions
+4
View File
@@ -12,6 +12,10 @@ file applies to all participants. Every file needs at least one matching test
participant. For example, a T5 script and a T6 script can share one beta without participant. For example, a T5 script and a T6 script can share one beta without
either script being installed on the other game's servers. either script being installed on the other game's servers.
Choose **Add all eligible files** to add the latest unpromoted version of every
remaining eligible file at once. Files and exact versions you already selected
stay unchanged.
Setup defaults to automatic selection. Enter one **Instances per game** number, Setup defaults to automatic selection. Enter one **Instances per game** number,
which defaults to 1 and applies to every game represented by the selected files which defaults to 1 and applies to every game represented by the selected files
and their matching servers. Every automatically selected instance must pass and their matching servers. Every automatically selected instance must pass
+35 -5
View File
@@ -84,6 +84,7 @@
allowLeave = false; allowLeave = false;
let hasLinkedSetup = $state(false); let hasLinkedSetup = $state(false);
let loadingVersions = $state<Set<number>>(new Set()); let loadingVersions = $state<Set<number>>(new Set());
let addingAllFiles = $state(false);
const rowRequests = new Map<number, number>(); const rowRequests = new Map<number, number>();
let versionRequest = 0; let versionRequest = 0;
let betaLabel = $state(''); let betaLabel = $state('');
@@ -103,6 +104,9 @@
picks.some((pick) => pick.scriptId === file.id && versionsFor(pick).length > 0)) picks.some((pick) => pick.scriptId === file.id && versionsFor(pick).length > 0))
) )
); );
const remainingFiles = $derived(
eligibleFiles.filter((file) => !picks.some((pick) => pick.scriptId === file.id))
);
const filesReady = $derived( const filesReady = $derived(
picks.length > 0 && picks.length > 0 &&
picks.every( picks.every(
@@ -526,6 +530,22 @@
loadingVersions = new Set([...loadingVersions].filter((key) => key !== pick.key)); loadingVersions = new Set([...loadingVersions].filter((key) => key !== pick.key));
} }
} }
async function addAllFiles() {
if (addingAllFiles || loadingVersions.size || !remainingFiles.length) return;
const selected = picks.filter((pick) => pick.scriptId !== null);
const additions = remainingFiles.map((file) => ({
key: nextKey++,
scriptId: file.id,
versionId: null
}));
picks = [...selected, ...additions];
addingAllFiles = true;
try {
await Promise.all(additions.map((_, index) => onPickFile(selected.length + index)));
} finally {
addingAllFiles = false;
}
}
function toggleServer(name: string) { function toggleServer(name: string) {
const next = new Set(pickedServers); const next = new Set(pickedServers);
if (next.has(name)) next.delete(name); if (next.has(name)) next.delete(name);
@@ -749,11 +769,21 @@
</p>{/if}{/if} </p>{/if}{/if}
</section>{/each} </section>{/each}
</div> </div>
<Button <div class="actions">
onclick={() => <Button
(picks = [...picks, { key: nextKey++, scriptId: null, versionId: null }])} onclick={() =>
disabled={picks.length >= eligibleFiles.length}>Add file</Button (picks = [
> ...picks,
{ key: nextKey++, scriptId: null, versionId: null }
])}
disabled={picks.length >= eligibleFiles.length}>Add file</Button
>
<Button
onclick={addAllFiles}
disabled={!remainingFiles.length || loadingVersions.size > 0}
loading={addingAllFiles}>Add all eligible files</Button
>
</div>
<p class="muted"> <p class="muted">
Combine files for different games in one beta. Each file installs only on Combine files for different games in one beta. Each file installs only on
servers matching its scope, and every file needs at least one test participant. servers matching its scope, and every file needs at least one test participant.
+163
View File
@@ -0,0 +1,163 @@
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { flushSync, mount, unmount } from 'svelte';
import { writable } from 'svelte/store';
import BetaSetupPage from '../../src/routes/betas/new/+page.svelte';
const currentPage = writable({
url: new URL('http://localhost/betas/new'),
data: { user: { role: 'operator' } }
});
vi.mock('$app/stores', () => ({
page: {
subscribe: (run: Parameters<typeof currentPage.subscribe>[0]) => currentPage.subscribe(run)
}
}));
vi.mock('$app/navigation', () => ({ goto: vi.fn(), beforeNavigate: vi.fn() }));
vi.mock('../../src/lib/ui/stores/fleet.js', () => ({
fleetServers: writable([]),
fleetError: writable(''),
refreshFleet: vi.fn().mockResolvedValue(undefined)
}));
let component: ReturnType<typeof mount> | undefined;
beforeEach(() => {
const storage = new Map<string, string>();
vi.stubGlobal('localStorage', {
getItem: (key: string) => storage.get(key) ?? null,
setItem: (key: string, value: string) => storage.set(key, value),
removeItem: (key: string) => storage.delete(key)
});
window.history.replaceState({}, '', '/betas/new');
});
afterEach(async () => {
if (component) await unmount(component);
component = undefined;
document.body.replaceChildren();
vi.unstubAllGlobals();
});
function file(
id: number,
options: {
currentVersionId?: number;
latestVersionId?: number | null;
runningBetaId?: number;
} = {}
) {
const latestVersionId =
options.latestVersionId === undefined ? id * 10 + 2 : options.latestVersionId;
return {
id,
name: `File ${id}`,
kind: 'script',
target_rel_path: `file-${id}.gsc`,
scope_type: null,
scope_value: null,
current_version_id: options.currentVersionId ?? id * 10,
latestVersion: latestVersionId === null ? null : { id: latestVersionId },
runningBetaId: options.runningBetaId ?? null
};
}
const files = [
file(1),
file(2),
file(3, { currentVersionId: 32 }),
file(4, { runningBetaId: 7 }),
file(5, { latestVersionId: null })
];
const button = (label: string) =>
[...document.querySelectorAll('button')].find((node) => node.textContent?.trim() === label)!;
const selections = () =>
[...document.querySelectorAll('.pick-row')].map((row) =>
[...row.querySelectorAll('select')].map((select) => select.value)
);
async function show({ query = '', failingFile = 0 } = {}) {
window.history.replaceState({}, '', `/betas/new${query}`);
currentPage.set({
url: new URL(window.location.href),
data: { user: { role: 'operator' } }
});
const fetch = vi.fn(async (url: string) => {
if (url === '/api/scripts') return Response.json({ scripts: files });
const script = files.find((entry) => url === `/api/scripts/${entry.id}`);
if (!script) throw new Error(`Unexpected request: ${url}`);
if (script.id === failingFile) return new Response('', { status: 503 });
return Response.json({
script,
versions: [2, 1, 0].map((version) => ({
id: script.id * 10 + version,
version: version + 1,
release_version: `1.0.${version}`,
sha256: `hash-${script.id}-${version}`,
state: 'untested'
})),
matchedServers: []
});
});
vi.stubGlobal('fetch', fetch);
component = mount(BetaSetupPage, { target: document.body });
flushSync();
await vi.waitFor(() => {
flushSync();
expect(button('Add all eligible files')).toBeDefined();
});
return fetch;
}
it('adds every eligible file with its latest unpromoted version and replaces empty rows', async () => {
const fetch = await show();
button('Add file').click();
flushSync();
expect(document.querySelectorAll('.pick-row')).toHaveLength(2);
button('Add all eligible files').click();
await vi.waitFor(() => {
flushSync();
expect(selections()).toEqual([
['1', '12'],
['2', '22']
]);
});
expect(button('Continue').disabled).toBe(false);
expect(button('Add all eligible files').disabled).toBe(true);
button('Add all eligible files').click();
flushSync();
expect(document.querySelectorAll('.pick-row')).toHaveLength(2);
expect(fetch.mock.calls.map(([url]) => url)).toEqual([
'/api/scripts',
'/api/scripts/1',
'/api/scripts/2'
]);
});
it('preserves exact linked versions, including an older upload of a promoted file', async () => {
await show({ query: '?artifact=1:11&artifact=3:31' });
button('Add file').click();
flushSync();
button('Add all eligible files').click();
await vi.waitFor(() => {
flushSync();
expect(selections()).toEqual([
['1', '11'],
['3', '31'],
['2', '22']
]);
});
expect(button('Continue').disabled).toBe(false);
});
it('retains successful selections and blocks continuing when one file cannot load versions', async () => {
await show({ failingFile: 2 });
button('Add all eligible files').click();
await vi.waitFor(() => {
flushSync();
expect(document.querySelector('[role="alert"]')?.textContent).toContain(
'Could not load versions'
);
});
expect(selections()[0]).toEqual(['1', '12']);
expect(selections()[1][0]).toBe('2');
const failedVersion = document.querySelectorAll('.pick-row')[1].querySelectorAll('select')[1];
expect(failedVersion.selectedOptions[0].textContent).toBe('Choose a version');
expect(button('Continue').disabled).toBe(true);
});