Add Prettier and ESLint tooling (Refs #77)

This commit is contained in:
2026-10-09 15:56:13 -04:00
parent 8ed059b8d3
commit 16469e2d43
711 changed files with 99737 additions and 28286 deletions
+203 -66
View File
@@ -29,8 +29,11 @@ beforeEach(async () => {
const host = hosts.createHost({ name: 'Transfer progress host' });
hostId = host.id;
hosts.enrollHost(hosts.createEnrollment(hostId).token, {
protocolVersion: 1, dockerRunning: true, directoriesWritable: true,
storageAvailable: 100 * 1024 ** 3, runtimeImages: [runtime]
protocolVersion: 1,
dockerRunning: true,
directoriesWritable: true,
storageAvailable: 100 * 1024 ** 3,
runtimeImages: [runtime]
});
version = await publish();
});
@@ -43,31 +46,48 @@ afterEach(async () => {
async function publish(game: 't5' | 't6' = 't5') {
const upload = libraries.createLibraryUpload({
game, files: [{ path: 'zone/game.ff', size: content.length, lastModified: 0 }]
game,
files: [{ path: 'zone/game.ff', size: content.length, lastModified: 0 }]
});
await libraries.uploadBrowserFile(upload.id, 'zone/game.ff', {
offset: 0, size: content.length, sha256: createHash('sha256').update(content).digest('hex')
}, new Response(content).body);
await libraries.uploadBrowserFile(
upload.id,
'zone/game.ff',
{
offset: 0,
size: content.length,
sha256: createHash('sha256').update(content).digest('hex')
},
new Response(content).body
);
return libraries.publishBrowserUpload(upload.id);
}
function transfer(phase: 'downloading' | 'verifying' = 'downloading') {
return {
phase, startedAt, elapsedSeconds: 10,
phase,
startedAt,
elapsedSeconds: 10,
phaseElapsedSeconds: phase === 'downloading' ? 10 : 2,
bytesPerSecond: 5, etaSeconds: 10
bytesPerSecond: 5,
etaSeconds: 10
};
}
function report(overrides: Record<string, unknown> = {}) {
return {
libraryVersionId: version.id, status: 'downloading',
bytesDownloaded: 50, bytesVerified: 0,
manifestSha256: version.manifestSha256, transfer: transfer(), ...overrides
libraryVersionId: version.id,
status: 'downloading',
bytesDownloaded: 50,
bytesVerified: 0,
manifestSha256: version.manifestSha256,
transfer: transfer(),
...overrides
};
}
function cache() { return libraries.getHostLibraryCache(hostId, version.id)!; }
function cache() {
return libraries.getHostLibraryCache(hostId, version.id)!;
}
describe('host game-file transfer progress', () => {
it('persists both phases and exposes them in the library and host preparation views', () => {
@@ -75,47 +95,91 @@ describe('host game-file transfer progress', () => {
libraries.updateHostLibraryCaches(hostId, [report()]);
const downloading = cache();
expect(downloading).toMatchObject({
status: 'downloading', bytesDownloaded: 50, bytesVerified: 0, transfer: transfer()
status: 'downloading',
bytesDownloaded: 50,
bytesVerified: 0,
transfer: transfer()
});
database.closeDb();
expect(cache()).toEqual(downloading);
const verification = transfer('verifying');
libraries.updateHostLibraryCaches(hostId, [report({
status: 'verifying', bytesDownloaded: 100, bytesVerified: 20, transfer: verification
})]);
libraries.updateHostLibraryCaches(hostId, [
report({
status: 'verifying',
bytesDownloaded: 100,
bytesVerified: 20,
transfer: verification
})
]);
const verifying = cache();
expect(verifying).toMatchObject({
status: 'verifying', bytesDownloaded: 100, bytesVerified: 20, transfer: verification
status: 'verifying',
bytesDownloaded: 100,
bytesVerified: 20,
transfer: verification
});
const overview = libraries.libraryOverview().libraries.find(library => library.game === 't5');
expect(overview!.versions.find(item => item.id === version.id)!.caches).toContainEqual(verifying);
const overview = libraries
.libraryOverview()
.libraries.find((library) => library.game === 't5');
expect(overview!.versions.find((item) => item.id === version.id)!.caches).toContainEqual(
verifying
);
expect(preparation.hostPreparationStatus(hosts.getHost(hostId)!).items).toContainEqual(
expect.objectContaining({ kind: 'library', state: 'preparing', cache: verifying })
);
expect(() => libraries.requireReadyLibrary(hostId, version.id)).toThrow(/finish downloading and verifying/);
expect(() => libraries.requireReadyLibrary(hostId, version.id)).toThrow(
/finish downloading and verifying/
);
});
it('allows an unknown transfer rate and ETA until the agent has measured progress', () => {
const initial = {
...transfer(), elapsedSeconds: 0, phaseElapsedSeconds: 0, bytesPerSecond: null, etaSeconds: null
...transfer(),
elapsedSeconds: 0,
phaseElapsedSeconds: 0,
bytesPerSecond: null,
etaSeconds: null
};
libraries.updateHostLibraryCaches(hostId, [report({ bytesDownloaded: 0, transfer: initial })]);
libraries.updateHostLibraryCaches(hostId, [
report({ bytesDownloaded: 0, transfer: initial })
]);
expect(cache()).toMatchObject({ bytesDownloaded: 0, bytesVerified: 0, transfer: initial });
});
it('retains completed timing when later ready heartbeats omit transfer metadata', () => {
const completed = { ...transfer('verifying'), elapsedSeconds: 30, phaseElapsedSeconds: 10, etaSeconds: 0 };
const completed = {
...transfer('verifying'),
elapsedSeconds: 30,
phaseElapsedSeconds: 10,
etaSeconds: 0
};
const frozen = { ...completed, bytesPerSecond: null, etaSeconds: null };
libraries.updateHostLibraryCaches(hostId, [report({
status: 'ready', bytesDownloaded: 100, bytesVerified: 100, transfer: completed
})]);
libraries.updateHostLibraryCaches(hostId, [
report({
status: 'ready',
bytesDownloaded: 100,
bytesVerified: 100,
transfer: completed
})
]);
vi.setSystemTime(Date.now() + 60_000);
libraries.updateHostLibraryCaches(hostId, [{
libraryVersionId: version.id, status: 'ready', manifestSha256: version.manifestSha256
}], { snapshot: true });
libraries.updateHostLibraryCaches(
hostId,
[
{
libraryVersionId: version.id,
status: 'ready',
manifestSha256: version.manifestSha256
}
],
{ snapshot: true }
);
expect(cache()).toMatchObject({
status: 'ready', bytesDownloaded: 100, bytesVerified: 100, transfer: frozen
status: 'ready',
bytesDownloaded: 100,
bytesVerified: 100,
transfer: frozen
});
expect(libraries.requireReadyLibrary(hostId, version.id).id).toBe(version.id);
database.closeDb();
@@ -125,49 +189,78 @@ describe('host game-file transfer progress', () => {
it('clears the previous transfer when the host no longer reports the library', () => {
libraries.updateHostLibraryCaches(hostId, [report()]);
libraries.updateHostLibraryCaches(hostId, [], { snapshot: true });
expect(cache()).toMatchObject({ status: 'missing', bytesDownloaded: 0, manifestSha256: null });
expect(cache()).toMatchObject({
status: 'missing',
bytesDownloaded: 0,
manifestSha256: null
});
expect(cache().bytesVerified ?? 0).toBe(0);
expect(cache().transfer).toBeUndefined();
});
it('clears old timing and verification when retrying while retaining resumable download bytes', () => {
const failed = libraries.queueLibraryDownload(hostId, version.id);
libraries.updateHostLibraryCaches(hostId, [report({ status: 'failed', error: 'Connection reset' })]);
hosts.reportHostOperation(hostId, failed.id, { state: 'failed', error: 'Connection reset' });
libraries.updateHostLibraryCaches(hostId, [
report({ status: 'failed', error: 'Connection reset' })
]);
hosts.reportHostOperation(hostId, failed.id, {
state: 'failed',
error: 'Connection reset'
});
const retry = libraries.queueLibraryDownload(hostId, version.id);
expect(retry.id).not.toBe(failed.id);
expect(cache()).toMatchObject({ status: 'downloading', bytesDownloaded: 50, operationId: retry.id });
expect(cache()).toMatchObject({
status: 'downloading',
bytesDownloaded: 50,
operationId: retry.id
});
expect(cache().bytesVerified ?? 0).toBe(0);
expect(cache().transfer).toBeUndefined();
});
it('continues accepting older agents without phase metadata', () => {
libraries.updateHostLibraryCaches(hostId, [{
libraryVersionId: version.id, status: 'downloading', bytesDownloaded: 25,
manifestSha256: version.manifestSha256
}]);
libraries.updateHostLibraryCaches(hostId, [
{
libraryVersionId: version.id,
status: 'downloading',
bytesDownloaded: 25,
manifestSha256: version.manifestSha256
}
]);
expect(cache()).toMatchObject({ status: 'downloading', bytesDownloaded: 25 });
expect(cache().transfer).toBeUndefined();
// Earlier agents verified files as they downloaded, so a verifying report
// could still contain a partial aggregate download count.
libraries.updateHostLibraryCaches(hostId, [{
libraryVersionId: version.id, status: 'verifying', bytesDownloaded: 25,
manifestSha256: version.manifestSha256
}]);
libraries.updateHostLibraryCaches(hostId, [
{
libraryVersionId: version.id,
status: 'verifying',
bytesDownloaded: 25,
manifestSha256: version.manifestSha256
}
]);
expect(cache().status).toBe('verifying');
libraries.updateHostLibraryCaches(hostId, [{
libraryVersionId: version.id, status: 'ready', manifestSha256: version.manifestSha256
}]);
libraries.updateHostLibraryCaches(hostId, [
{
libraryVersionId: version.id,
status: 'ready',
manifestSha256: version.manifestSha256
}
]);
expect(cache()).toMatchObject({ status: 'ready', bytesDownloaded: 100 });
expect(libraries.requireReadyLibrary(hostId, version.id).id).toBe(version.id);
});
it('does not leave an old ETA attached to a new report from an older agent', () => {
libraries.updateHostLibraryCaches(hostId, [report()]);
libraries.updateHostLibraryCaches(hostId, [{
libraryVersionId: version.id, status: 'downloading', bytesDownloaded: 60,
manifestSha256: version.manifestSha256
}]);
libraries.updateHostLibraryCaches(hostId, [
{
libraryVersionId: version.id,
status: 'downloading',
bytesDownloaded: 60,
manifestSha256: version.manifestSha256
}
]);
expect(cache()).toMatchObject({ status: 'downloading', bytesDownloaded: 60 });
expect(cache().transfer).toBeUndefined();
});
@@ -178,8 +271,19 @@ describe('host game-file transfer progress', () => {
['fractional verified bytes', { bytesVerified: 0.5 }],
['nonfinite verified bytes', { bytesVerified: Number.POSITIVE_INFINITY }],
['downloaded bytes exceed the library', { bytesDownloaded: 101 }],
['a partial download in the verification phase', { status: 'verifying', transfer: transfer('verifying') }],
['ready before verification finishes', { status: 'ready', bytesDownloaded: 100, bytesVerified: 20, transfer: transfer('verifying') }],
[
'a partial download in the verification phase',
{ status: 'verifying', transfer: transfer('verifying') }
],
[
'ready before verification finishes',
{
status: 'ready',
bytesDownloaded: 100,
bytesVerified: 20,
transfer: transfer('verifying')
}
],
['a phase inconsistent with status', { transfer: transfer('verifying') }],
['an unknown phase', { transfer: { ...transfer(), phase: 'uploading' } }],
['a malformed start time', { transfer: { ...transfer(), startedAt: 'not-a-timestamp' } }],
@@ -187,12 +291,24 @@ describe('host game-file transfer progress', () => {
['a negative elapsed time', { transfer: { ...transfer(), elapsedSeconds: -1 } }],
['a nonfinite elapsed time', { transfer: { ...transfer(), elapsedSeconds: Number.NaN } }],
['a negative phase elapsed time', { transfer: { ...transfer(), phaseElapsedSeconds: -1 } }],
['a nonfinite phase elapsed time', { transfer: { ...transfer(), phaseElapsedSeconds: Number.POSITIVE_INFINITY } }],
['a phase longer than the full transfer', { transfer: { ...transfer(), phaseElapsedSeconds: 11 } }],
[
'a nonfinite phase elapsed time',
{ transfer: { ...transfer(), phaseElapsedSeconds: Number.POSITIVE_INFINITY } }
],
[
'a phase longer than the full transfer',
{ transfer: { ...transfer(), phaseElapsedSeconds: 11 } }
],
['a negative transfer rate', { transfer: { ...transfer(), bytesPerSecond: -1 } }],
['a nonfinite transfer rate', { transfer: { ...transfer(), bytesPerSecond: Number.POSITIVE_INFINITY } }],
[
'a nonfinite transfer rate',
{ transfer: { ...transfer(), bytesPerSecond: Number.POSITIVE_INFINITY } }
],
['a negative remaining time', { transfer: { ...transfer(), etaSeconds: -1 } }],
['a nonfinite remaining time', { transfer: { ...transfer(), etaSeconds: Number.POSITIVE_INFINITY } }]
[
'a nonfinite remaining time',
{ transfer: { ...transfer(), etaSeconds: Number.POSITIVE_INFINITY } }
]
])('rejects %s without changing the existing cache', (_name, invalid) => {
libraries.updateHostLibraryCaches(hostId, [report()]);
const before = cache();
@@ -203,30 +319,51 @@ describe('host game-file transfer progress', () => {
it('rolls back the entire heartbeat when a later cache contains invalid metadata', async () => {
const second = await publish('t6');
libraries.updateHostLibraryCaches(hostId, [report(), report({ libraryVersionId: second.id })]);
libraries.updateHostLibraryCaches(hostId, [
report(),
report({ libraryVersionId: second.id })
]);
const beforeFirst = cache();
const beforeSecond = libraries.getHostLibraryCache(hostId, second.id);
expect(() => libraries.updateHostLibraryCaches(hostId, [
report({ bytesDownloaded: 70 }),
report({ libraryVersionId: second.id, transfer: { ...transfer(), elapsedSeconds: -1 } })
], { snapshot: true })).toThrow();
expect(() =>
libraries.updateHostLibraryCaches(
hostId,
[
report({ bytesDownloaded: 70 }),
report({
libraryVersionId: second.id,
transfer: { ...transfer(), elapsedSeconds: -1 }
})
],
{ snapshot: true }
)
).toThrow();
expect(cache()).toEqual(beforeFirst);
expect(libraries.getHostLibraryCache(hostId, second.id)).toEqual(beforeSecond);
});
it('adds transfer columns to an existing database without losing cached files or operation references', () => {
const operation = libraries.queueLibraryDownload(hostId, version.id);
libraries.updateHostLibraryCaches(hostId, [{
libraryVersionId: version.id, status: 'ready', manifestSha256: version.manifestSha256
}]);
libraries.updateHostLibraryCaches(hostId, [
{
libraryVersionId: version.id,
status: 'ready',
manifestSha256: version.manifestSha256
}
]);
const before = cache();
database.db().exec('ALTER TABLE host_library_caches DROP COLUMN transfer_progress');
database.db().exec('ALTER TABLE host_library_caches DROP COLUMN bytes_verified');
database.closeDb();
expect(cache()).toMatchObject({
hostId, libraryVersionId: version.id, status: 'ready', bytesDownloaded: 100,
bytesTotal: 100, manifestSha256: version.manifestSha256,
operationId: operation.id, updatedAt: before.updatedAt
hostId,
libraryVersionId: version.id,
status: 'ready',
bytesDownloaded: 100,
bytesTotal: 100,
manifestSha256: version.manifestSha256,
operationId: operation.id,
updatedAt: before.updatedAt
});
expect(cache().transfer).toBeUndefined();
expect(libraries.requireReadyLibrary(hostId, version.id).id).toBe(version.id);