This commit is contained in:
@@ -18,7 +18,7 @@ const inventory: HostInventory = {
|
||||
dockerRunning: true,
|
||||
directoriesWritable: true,
|
||||
storageAvailable: 100 * 1024 ** 3,
|
||||
runtimeImages: [],
|
||||
preparedRuntimeImages: [],
|
||||
libraries: [],
|
||||
capabilities: ['host.decommission']
|
||||
};
|
||||
@@ -123,7 +123,7 @@ describe('automatic host preparation', () => {
|
||||
const t5 = await publish('t5');
|
||||
const t6 = await publish('t6');
|
||||
const readyInventory = {
|
||||
runtimeImages: [runtime],
|
||||
preparedRuntimeImages: [runtime],
|
||||
libraries: [t5, t6].map((version) => ({ id: version.id, state: 'ready' as const })),
|
||||
capabilities: [
|
||||
...inventory.capabilities!,
|
||||
@@ -195,7 +195,7 @@ describe('automatic host preparation', () => {
|
||||
it('requires both games when enabling even if one library is already cached', async () => {
|
||||
const version = await publish('t5');
|
||||
const id = enroll({
|
||||
runtimeImages: [runtime],
|
||||
preparedRuntimeImages: [runtime],
|
||||
libraries: [{ id: version.id, state: 'ready' }]
|
||||
});
|
||||
verified(id, version);
|
||||
@@ -212,7 +212,7 @@ describe('automatic host preparation', () => {
|
||||
const t5 = await publish('t5');
|
||||
const t6 = await publish('t6');
|
||||
const readyInventory = {
|
||||
runtimeImages: [runtime],
|
||||
preparedRuntimeImages: [runtime],
|
||||
libraries: [t5, t6].map((version) => ({ id: version.id, state: 'ready' as const }))
|
||||
};
|
||||
const id = enroll(readyInventory);
|
||||
@@ -310,7 +310,11 @@ describe('automatic host preparation', () => {
|
||||
expect(operations(host.id)).toHaveLength(1);
|
||||
|
||||
vi.setSystemTime(Date.now() + 1);
|
||||
const preparedInventory = { ...inventory, runtimeImages: [runtime], libraryCaches: [] };
|
||||
const preparedInventory = {
|
||||
...inventory,
|
||||
preparedRuntimeImages: [runtime],
|
||||
libraryCaches: []
|
||||
};
|
||||
const runtimeHeartbeat = await reportHeartbeat({
|
||||
request: request('heartbeat', { inventory: preparedInventory })
|
||||
});
|
||||
@@ -369,7 +373,7 @@ describe('automatic host preparation', () => {
|
||||
|
||||
it('presents preparation without queuing work and prepares the configured runtime even when another image exists', async () => {
|
||||
const version = await publish();
|
||||
const id = enroll({ runtimeImages: ['another:runtime'] });
|
||||
const id = enroll({ preparedRuntimeImages: ['another:runtime'] });
|
||||
const host = hosts.getHost(id)!;
|
||||
const status = preparation.hostPreparationStatus(host);
|
||||
expect(status.items.map((item) => item.kind)).toEqual(['runtime', 'library']);
|
||||
@@ -406,7 +410,7 @@ describe('automatic host preparation', () => {
|
||||
preparation.reconcileHostPreparation(id);
|
||||
expect(operations(id)).toHaveLength(1);
|
||||
|
||||
heartbeat(id, { runtimeImages: [runtime] });
|
||||
heartbeat(id, { preparedRuntimeImages: [runtime] });
|
||||
preparation.reconcileHostPreparation(id);
|
||||
expect(operations(id)).toHaveLength(2);
|
||||
const download = operations(id)[0];
|
||||
@@ -417,7 +421,7 @@ describe('automatic host preparation', () => {
|
||||
verified(id, version);
|
||||
hosts.reportHostOperation(id, download.id, { state: 'completed' });
|
||||
heartbeat(id, {
|
||||
runtimeImages: [runtime],
|
||||
preparedRuntimeImages: [runtime],
|
||||
libraries: [{ id: version.id, state: 'ready' }]
|
||||
});
|
||||
expect(preparation.reconcileHostPreparation(id).state).toBe('ready');
|
||||
@@ -432,7 +436,7 @@ describe('automatic host preparation', () => {
|
||||
game: 't5',
|
||||
files: [{ path: 'zone/game.ff', size: content.length, lastModified: 0 }]
|
||||
});
|
||||
const id = enroll({ runtimeImages: [runtime] });
|
||||
const id = enroll({ preparedRuntimeImages: [runtime] });
|
||||
const expected = new Map([latest, t6].map((version) => [version.id, version]));
|
||||
const downloaded: string[] = [];
|
||||
for (let index = 0; index < expected.size; index++) {
|
||||
@@ -452,7 +456,7 @@ describe('automatic host preparation', () => {
|
||||
preparation.reconcileHostPreparation(id);
|
||||
expect(operations(id)).toHaveLength(index + 1);
|
||||
heartbeat(id, {
|
||||
runtimeImages: [runtime],
|
||||
preparedRuntimeImages: [runtime],
|
||||
libraries: downloaded.map((id) => ({ id, state: 'ready' }))
|
||||
});
|
||||
}
|
||||
@@ -462,12 +466,15 @@ describe('automatic host preparation', () => {
|
||||
});
|
||||
|
||||
it('picks up publication after enrollment and keeps verified historical versions intact', async () => {
|
||||
const id = enroll({ runtimeImages: [runtime] });
|
||||
const id = enroll({ preparedRuntimeImages: [runtime] });
|
||||
preparation.reconcileHostPreparation(id);
|
||||
expect(operations(id)).toEqual([]);
|
||||
const old = await publish();
|
||||
verified(id, old);
|
||||
heartbeat(id, { runtimeImages: [runtime], libraries: [{ id: old.id, state: 'ready' }] });
|
||||
heartbeat(id, {
|
||||
preparedRuntimeImages: [runtime],
|
||||
libraries: [{ id: old.id, state: 'ready' }]
|
||||
});
|
||||
expect(preparation.reconcileHostPreparation(id).state).toBe('ready');
|
||||
const latest = await publish();
|
||||
preparation.reconcileHostPreparation(id);
|
||||
@@ -479,7 +486,7 @@ describe('automatic host preparation', () => {
|
||||
|
||||
it('replaces an undelivered automatic download when newer files finish uploading', async () => {
|
||||
const old = await publish();
|
||||
const id = enroll({ runtimeImages: [runtime] });
|
||||
const id = enroll({ preparedRuntimeImages: [runtime] });
|
||||
preparation.reconcileHostPreparation(id);
|
||||
const first = operations(id)[0];
|
||||
expect(first.payload).toMatchObject({ libraryVersionId: old.id, automatic: true });
|
||||
@@ -503,8 +510,8 @@ describe('automatic host preparation', () => {
|
||||
async (automatic) => {
|
||||
const old = await publish();
|
||||
const image = 'ghcr.io/example/plutainer:queued-version';
|
||||
const id = enroll({ runtimeImages: [runtime, image] });
|
||||
const other = enroll({ runtimeImages: [runtime] });
|
||||
const id = enroll({ preparedRuntimeImages: [runtime, image] });
|
||||
const other = enroll({ preparedRuntimeImages: [runtime] });
|
||||
const { queueNativeCreation } =
|
||||
await import('../../src/lib/server/provisioning/native.js');
|
||||
for (let index = 0; index < 2; index++) {
|
||||
@@ -521,7 +528,7 @@ describe('automatic host preparation', () => {
|
||||
).toBe(true);
|
||||
}
|
||||
// Also cover pre-existing intents whose image is no longer cached.
|
||||
heartbeat(id, { runtimeImages: [runtime] });
|
||||
heartbeat(id, { preparedRuntimeImages: [runtime] });
|
||||
const latest = await publish();
|
||||
const status = preparation.hostPreparationStatus(hosts.getHost(id)!);
|
||||
expect(status.items.filter((item) => item.label === image)).toHaveLength(0);
|
||||
@@ -555,7 +562,7 @@ describe('automatic host preparation', () => {
|
||||
vi.setSystemTime(Date.now() + 1);
|
||||
hosts.reportHostOperation(id, download.id, { state: 'completed' });
|
||||
heartbeat(id, {
|
||||
runtimeImages: [runtime, image],
|
||||
preparedRuntimeImages: [runtime, image],
|
||||
libraries: [{ id: old.id, state: 'ready' }]
|
||||
});
|
||||
preparation.reconcileHostPreparation(id);
|
||||
@@ -568,7 +575,7 @@ describe('automatic host preparation', () => {
|
||||
|
||||
it('retains an undelivered library download for an accepted intent until that intent is cancelled', async () => {
|
||||
const old = await publish();
|
||||
const id = enroll({ runtimeImages: [runtime] });
|
||||
const id = enroll({ preparedRuntimeImages: [runtime] });
|
||||
preparation.reconcileHostPreparation(id);
|
||||
const download = operations(id)[0];
|
||||
const { queueNativeCreation } = await import('../../src/lib/server/provisioning/native.js');
|
||||
@@ -604,7 +611,7 @@ describe('automatic host preparation', () => {
|
||||
|
||||
it('finishes a delivered download before following a newer upload under a new operation ID', async () => {
|
||||
const old = await publish();
|
||||
const id = enroll({ runtimeImages: [runtime] });
|
||||
const id = enroll({ preparedRuntimeImages: [runtime] });
|
||||
preparation.reconcileHostPreparation(id);
|
||||
const [first] = hosts.pendingHostOperations(id);
|
||||
const latest = await publish();
|
||||
@@ -613,7 +620,10 @@ describe('automatic host preparation', () => {
|
||||
expect(hosts.getHostOperation(first.id)?.state).toBe('pending');
|
||||
verified(id, old);
|
||||
hosts.reportHostOperation(id, first.id, { state: 'completed' });
|
||||
heartbeat(id, { runtimeImages: [runtime], libraries: [{ id: old.id, state: 'ready' }] });
|
||||
heartbeat(id, {
|
||||
preparedRuntimeImages: [runtime],
|
||||
libraries: [{ id: old.id, state: 'ready' }]
|
||||
});
|
||||
preparation.reconcileHostPreparation(id);
|
||||
expect(operations(id)[0]).toMatchObject({
|
||||
state: 'pending',
|
||||
@@ -688,23 +698,26 @@ describe('automatic host preparation', () => {
|
||||
it('blocks a library that cannot fit while preserving the host free-space reserve', async () => {
|
||||
const version = await publish();
|
||||
const id = enroll({
|
||||
runtimeImages: [runtime],
|
||||
preparedRuntimeImages: [runtime],
|
||||
storageAvailable: 1_000 + version.sizeBytes - 1
|
||||
});
|
||||
hosts.updateHost(id, { minimumFreeBytes: 1_000 });
|
||||
expect(preparation.reconcileHostPreparation(id).state).toBe('blocked');
|
||||
expect(operations(id)).toEqual([]);
|
||||
heartbeat(id, { runtimeImages: [runtime], storageAvailable: 1_000 + version.sizeBytes });
|
||||
heartbeat(id, {
|
||||
preparedRuntimeImages: [runtime],
|
||||
storageAvailable: 1_000 + version.sizeBytes
|
||||
});
|
||||
expect(preparation.reconcileHostPreparation(id).state).toBe('preparing');
|
||||
expect(operations(id)[0].payload.libraryVersionId).toBe(version.id);
|
||||
});
|
||||
|
||||
it('returns an active download even when its in-progress bytes have consumed free space', async () => {
|
||||
const version = await publish();
|
||||
const id = enroll({ runtimeImages: [runtime] });
|
||||
const id = enroll({ preparedRuntimeImages: [runtime] });
|
||||
const operation = libraries.queueLibraryDownload(id, version.id);
|
||||
heartbeat(id, {
|
||||
runtimeImages: [runtime],
|
||||
preparedRuntimeImages: [runtime],
|
||||
storageAvailable: hosts.getHost(id)!.minimumFreeBytes
|
||||
});
|
||||
expect(libraries.queueLibraryDownload(id, version.id).id).toBe(operation.id);
|
||||
@@ -715,7 +728,7 @@ describe('automatic host preparation', () => {
|
||||
it('prepares T6 when the latest T5 upload cannot fit', async () => {
|
||||
const oversized = await publish();
|
||||
const small = await publish('t6', 'small');
|
||||
const id = enroll({ runtimeImages: [runtime], storageAvailable: 1_010 });
|
||||
const id = enroll({ preparedRuntimeImages: [runtime], storageAvailable: 1_010 });
|
||||
hosts.updateHost(id, { minimumFreeBytes: 1_000 });
|
||||
const status = preparation.reconcileHostPreparation(id);
|
||||
expect(status.items).toEqual(
|
||||
@@ -731,7 +744,7 @@ describe('automatic host preparation', () => {
|
||||
|
||||
it('does not count partial bytes from a different manifest toward download storage', async () => {
|
||||
const version = await publish();
|
||||
const id = enroll({ runtimeImages: [runtime] });
|
||||
const id = enroll({ preparedRuntimeImages: [runtime] });
|
||||
libraries.updateHostLibraryCaches(id, [
|
||||
{
|
||||
libraryVersionId: version.id,
|
||||
@@ -741,7 +754,7 @@ describe('automatic host preparation', () => {
|
||||
}
|
||||
]);
|
||||
heartbeat(id, {
|
||||
runtimeImages: [runtime],
|
||||
preparedRuntimeImages: [runtime],
|
||||
storageAvailable: hosts.getHost(id)!.minimumFreeBytes + 20
|
||||
});
|
||||
expect(preparation.reconcileHostPreparation(id).state).toBe('blocked');
|
||||
@@ -753,7 +766,7 @@ describe('automatic host preparation', () => {
|
||||
|
||||
it('resumes a failed verified-manifest download using only its remaining storage', async () => {
|
||||
const version = await publish();
|
||||
const id = enroll({ runtimeImages: [runtime] });
|
||||
const id = enroll({ preparedRuntimeImages: [runtime] });
|
||||
preparation.reconcileHostPreparation(id);
|
||||
const operation = operations(id)[0];
|
||||
libraries.updateHostLibraryCaches(id, [
|
||||
@@ -768,11 +781,11 @@ describe('automatic host preparation', () => {
|
||||
hosts.reportHostOperation(id, operation.id, { state: 'failed', error: 'Connection reset' });
|
||||
const failedAt = Date.now();
|
||||
const free = hosts.getHost(id)!.minimumFreeBytes + version.sizeBytes - 80;
|
||||
heartbeat(id, { runtimeImages: [runtime], storageAvailable: free });
|
||||
heartbeat(id, { preparedRuntimeImages: [runtime], storageAvailable: free });
|
||||
preparation.reconcileHostPreparation(id);
|
||||
expect(operations(id)).toHaveLength(1);
|
||||
vi.setSystemTime(failedAt + 60_000);
|
||||
heartbeat(id, { runtimeImages: [runtime], storageAvailable: free });
|
||||
heartbeat(id, { preparedRuntimeImages: [runtime], storageAvailable: free });
|
||||
expect(preparation.reconcileHostPreparation(id).state).toBe('preparing');
|
||||
expect(operations(id)).toHaveLength(2);
|
||||
expect(operations(id)[0].payload.libraryVersionId).toBe(version.id);
|
||||
@@ -800,7 +813,7 @@ describe('optional host Plutonium cache', () => {
|
||||
(state) => {
|
||||
const ready = {
|
||||
...capable,
|
||||
runtimeImages: [runtime],
|
||||
preparedRuntimeImages: [runtime],
|
||||
libraries: [{ id: 'game-files', state: 'ready' as const }]
|
||||
};
|
||||
const id = enroll(ready);
|
||||
@@ -819,7 +832,7 @@ describe('optional host Plutonium cache', () => {
|
||||
it('delivers newer deployments before queued cache work and before a cache replay', () => {
|
||||
const id = enroll({
|
||||
...capable,
|
||||
runtimeImages: [runtime],
|
||||
preparedRuntimeImages: [runtime],
|
||||
libraries: [{ id: 'game-files', state: 'ready' }]
|
||||
});
|
||||
preparation.reconcileHostPreparation(id);
|
||||
@@ -842,7 +855,7 @@ describe('optional host Plutonium cache', () => {
|
||||
const ready = {
|
||||
...inventory,
|
||||
...capable,
|
||||
runtimeImages: [runtime],
|
||||
preparedRuntimeImages: [runtime],
|
||||
libraries: [{ id: 'game-files', state: 'ready' as const }]
|
||||
};
|
||||
const { credential } = hosts.enrollHost(hosts.createEnrollment(host.id).token, ready);
|
||||
@@ -903,7 +916,7 @@ describe('optional host Plutonium cache', () => {
|
||||
const version = await publish();
|
||||
const ready = {
|
||||
...capable,
|
||||
runtimeImages: [runtime],
|
||||
preparedRuntimeImages: [runtime],
|
||||
libraries: [{ id: version.id, state: 'ready' as const }],
|
||||
plutoniumCache: { revision: 5354, bytes: 200 }
|
||||
};
|
||||
@@ -948,7 +961,7 @@ describe('optional host Plutonium cache', () => {
|
||||
const pull = operations(id)[0];
|
||||
expect(pull.kind).toBe('runtime.pull');
|
||||
hosts.reportHostOperation(id, pull.id, { state: 'completed' });
|
||||
heartbeat(id, { ...capable, runtimeImages: [runtime] });
|
||||
heartbeat(id, { ...capable, preparedRuntimeImages: [runtime] });
|
||||
preparation.reconcileHostPreparation(id);
|
||||
const download = operations(id)[0];
|
||||
expect(download.kind).toBe('library.download');
|
||||
@@ -956,7 +969,7 @@ describe('optional host Plutonium cache', () => {
|
||||
hosts.reportHostOperation(id, download.id, { state: 'completed' });
|
||||
const ready = {
|
||||
...capable,
|
||||
runtimeImages: [runtime],
|
||||
preparedRuntimeImages: [runtime],
|
||||
libraries: [{ id: version.id, state: 'ready' as const }]
|
||||
};
|
||||
heartbeat(id, ready);
|
||||
@@ -1005,7 +1018,7 @@ describe('optional host Plutonium cache', () => {
|
||||
it('refreshes for newer production releases and reconciles an interrupted refresh without duplicates', async () => {
|
||||
const ready = {
|
||||
...capable,
|
||||
runtimeImages: [runtime],
|
||||
preparedRuntimeImages: [runtime],
|
||||
plutoniumCache: { revision: 5354, bytes: 25_000 }
|
||||
};
|
||||
const id = enroll(ready);
|
||||
@@ -1048,7 +1061,7 @@ describe('optional host Plutonium cache', () => {
|
||||
checkedAt: new Date(Date.now() - 60 * 60_000).toISOString()
|
||||
});
|
||||
if (evidence === 'failed') await release(5354, { error: 'Manifest unavailable' });
|
||||
const id = enroll({ ...capable, runtimeImages: [runtime] });
|
||||
const id = enroll({ ...capable, preparedRuntimeImages: [runtime] });
|
||||
preparation.reconcileHostPreparation(id);
|
||||
expect(operations(id)[0]).toMatchObject({
|
||||
kind: 'plutonium.prepare',
|
||||
@@ -1060,7 +1073,7 @@ describe('optional host Plutonium cache', () => {
|
||||
).toBe('Latest production revision');
|
||||
const cached = enroll({
|
||||
...capable,
|
||||
runtimeImages: [runtime],
|
||||
preparedRuntimeImages: [runtime],
|
||||
plutoniumCache: { revision: 5353, bytes: 200 }
|
||||
});
|
||||
preparation.reconcileHostPreparation(cached);
|
||||
@@ -1071,7 +1084,7 @@ describe('optional host Plutonium cache', () => {
|
||||
it('leaves agents without the cache capability unchanged', async () => {
|
||||
await release(5355);
|
||||
const id = enroll({
|
||||
runtimeImages: [runtime],
|
||||
preparedRuntimeImages: [runtime],
|
||||
plutoniumCache: { revision: 5354, bytes: 200 }
|
||||
});
|
||||
expect(
|
||||
@@ -1084,7 +1097,7 @@ describe('optional host Plutonium cache', () => {
|
||||
const version = await publish();
|
||||
const ready = {
|
||||
...capable,
|
||||
runtimeImages: [runtime],
|
||||
preparedRuntimeImages: [runtime],
|
||||
libraries: [{ id: version.id, state: 'ready' as const }]
|
||||
};
|
||||
const id = enroll(ready);
|
||||
@@ -1135,7 +1148,7 @@ describe('optional host Plutonium cache', () => {
|
||||
const version = await publish();
|
||||
const id = enroll({
|
||||
...capable,
|
||||
runtimeImages: [runtime],
|
||||
preparedRuntimeImages: [runtime],
|
||||
storageAvailable: 1_000 + version.sizeBytes - 1
|
||||
});
|
||||
hosts.updateHost(id, { minimumFreeBytes: 1_000 });
|
||||
|
||||
Reference in New Issue
Block a user