Simplify game files to automatic T5 and T6 uploads

This commit is contained in:
2026-10-04 12:23:36 -04:00
parent 482e27e735
commit 68323c0baa
19 changed files with 588 additions and 232 deletions
+46 -9
View File
@@ -9,7 +9,7 @@ const state = vi.hoisted(() => ({ handle: null as Database.Database | null }));
vi.mock('../../src/lib/server/db/index.js', () => ({ db: () => state.handle }));
import {
browserUploadStatus, createLibraryImport, createLibraryUpload, getHostLibraryCache,
getLibraryVersion, listLibraries, publishBrowserUpload, queueLibraryDownload,
getLibraryVersion, latestPublishedLibraries, listLibraries, newestReadyLibrary, publishBrowserUpload, queueLibraryDownload,
requestBrowserPublication, requireReadyLibrary, updateHostLibraryCaches, uploadBrowserFile
} from '../../src/lib/server/game-files/index.js';
import { createEnrollment, createHost, enrollHost, listHosts } from '../../src/lib/server/hosts/index.js';
@@ -24,8 +24,7 @@ const sha = (value: Uint8Array) => createHash('sha256').update(value).digest('he
const file = (path = 'zone/base.ff', content = bytes) => ({ path, size: content.length, lastModified: modified });
const upload = (id: string, path: string, content: Buffer, offset = 0, checksum = sha(content)) =>
uploadBrowserFile(id, path, { offset, size: content.length, sha256: checksum }, new Response(new Uint8Array(content)).body);
const begin = (game: 't5' | 't6' = 't6', files = [file()], version = '1') =>
createLibraryUpload({ game, name: `Plutonium ${game.toUpperCase()}`, version, description: 'Uploaded game folder', files });
const begin = (game: 't5' | 't6' = 't6', files = [file()]) => createLibraryUpload({ game, files });
beforeEach(async () => {
directory = await mkdtemp(join(tmpdir(), 'ec-browser-library-'));
@@ -37,6 +36,7 @@ afterEach(async () => {
state.handle?.close(); state.handle = null;
await rm(directory, { recursive: true, force: true });
vi.unstubAllEnvs();
vi.useRealTimers();
});
function host() {
@@ -65,7 +65,7 @@ describe('browser folder uploads', () => {
expect(ready.manifestSha256).toBe(manifestChecksum(ready.files));
expect(await readFile(join(ready.storageLocation, file().path))).toEqual(bytes);
expect(await browserUploadStatus(initial.id)).toMatchObject({ status: 'published' });
expect(listLibraries()[0].versions[0]).not.toHaveProperty('dlc');
expect(listLibraries().find(library => library.game === game)?.versions[0]).not.toHaveProperty('dlc');
expect(listHosts()).toEqual([]);
});
@@ -122,7 +122,7 @@ describe('browser folder uploads', () => {
const chunkSize = (await browserUploadStatus(sizeProbe.id)).chunkSize;
expect(chunkSize).toBeGreaterThan(0);
const large = Buffer.alloc(chunkSize + 17, 0x41);
const initial = begin('t6', [file('main/archive.iwd', large)], 'large');
const initial = begin('t6', [file('main/archive.iwd', large)]);
const first = large.subarray(0, chunkSize);
await upload(initial.id, 'main/archive.iwd', first);
expect((await browserUploadStatus(initial.id)).files[0]).toMatchObject({ receivedBytes: chunkSize, complete: false });
@@ -146,17 +146,54 @@ describe('browser folder uploads', () => {
expect((await browserUploadStatus(initial.id)).files[0].receivedBytes).toBe(0);
});
it('keeps ready bytes and version metadata immutable and requires a new version label', async () => {
it('keeps published files immutable while accepting repeated uploads without labels', async () => {
const initial = begin(); await upload(initial.id, file().path, bytes);
const ready = await publishBrowserUpload(initial.id);
await expect(upload(initial.id, file().path, bytes)).rejects.toThrow();
expect(() => state.handle!.prepare('UPDATE game_file_versions SET description=? WHERE id=?').run('changed', ready.id)).toThrow(/immutable/);
expect(() => begin()).toThrow(/already exists/);
expect((await publishBrowserUpload(initial.id)).manifestSha256).toBe(ready.manifestSha256);
expect(begin('t6', [file()], '2').id).not.toBe(ready.id);
const next = begin();
expect(next.id).not.toBe(ready.id);
expect(next.version).not.toBe(ready.version);
expect(next.libraryId).toBe(ready.libraryId);
expect(await readFile(join(ready.storageLocation, file().path))).toEqual(bytes);
});
it('ignores legacy names, descriptions, and labels when accepting new game files', () => {
const legacyFields = { name: 'same name', version: 'same version', description: 'a library description' };
const first = createLibraryUpload({ game: 't5', files: [file()], ...legacyFields });
const next = createLibraryUpload({ game: 't5', files: [file()], ...legacyFields });
const otherGame = createLibraryUpload({ game: 't6', files: [file()], ...legacyFields });
expect(first.description).toBe('');
expect(first.version).not.toBe(legacyFields.version);
expect(next.version).not.toBe(first.version);
expect(next.libraryId).toBe(first.libraryId);
expect(otherGame.libraryId).not.toBe(first.libraryId);
expect(listLibraries().map(library => library.name)).toEqual(['Black Ops I (T5)', 'Black Ops II (T6)']);
});
it('follows completion order and preserves the latest successful files during failed or unfinished uploads', async () => {
vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime(new Date('2026-01-01T00:00:00Z'));
const slow = begin();
vi.setSystemTime(new Date('2026-01-02T00:00:00Z'));
const fast = begin();
await upload(fast.id, file().path, bytes);
vi.setSystemTime(new Date('2026-01-03T00:00:00Z'));
await publishBrowserUpload(fast.id);
expect(newestReadyLibrary('t6')?.id).toBe(fast.id);
await upload(slow.id, file().path, bytes);
vi.setSystemTime(new Date('2026-01-04T00:00:00Z'));
await publishBrowserUpload(slow.id);
const failed = begin();
await requestBrowserPublication(failed.id);
await vi.waitFor(() => expect(getLibraryVersion(failed.id)?.state).toBe('failed'));
begin();
expect(newestReadyLibrary('t6')?.id).toBe(slow.id);
expect(latestPublishedLibraries().map(version => version.id)).toEqual([slow.id]);
expect(listLibraries().find(library => library.game === 't6')?.latestVersionId).toBe(slow.id);
});
it('recovers publication after the verified directory was renamed but the ready commit was interrupted', async () => {
const initial = begin(); await upload(initial.id, file().path, bytes);
const files = [{ path: file().path, size: bytes.length, sha256: sha(bytes) }];
@@ -220,7 +257,7 @@ describe('browser folder uploads', () => {
expect(getHostLibraryCache(target.id, ready.id)?.status).toBe('failed');
updateHostLibraryCaches(target.id, [{ libraryVersionId: ready.id, status: 'ready', manifestSha256: ready.manifestSha256 }]);
expect(requireReadyLibrary(target.id, ready.id).id).toBe(ready.id);
const replacement = begin('t5', [file()], '2'); await upload(replacement.id, file().path, bytes);
const replacement = begin('t5'); await upload(replacement.id, file().path, bytes);
await publishBrowserUpload(replacement.id);
expect(requireReadyLibrary(target.id, ready.id).manifestSha256).toBe(ready.manifestSha256);
});