Clarify host readiness when game files or runtime are missing

This commit is contained in:
2026-10-04 10:03:43 -04:00
parent 3d62dfff27
commit 62d7817ea3
8 changed files with 58 additions and 10 deletions
+1 -1
View File
@@ -156,7 +156,7 @@ describe('host deletion', () => {
expect(roleAllows('admin', permission)).toBe(true);
expect(roleAllows('operator', permission)).toBe(false); expect(roleAllows('viewer', permission)).toBe(false);
const { DELETE } = await import('../../src/routes/api/hosts/[id]/+server.js');
for (const headers of [{ origin: 'https://attacker.test' }, { authorization: `Bearer ${hosts.credentials.credential}` }]) {
for (const headers of [new Headers({ origin: 'https://attacker.test' }), new Headers({ authorization: `Bearer ${hosts.credentials.credential}` })]) {
expect(() => DELETE({ request: new Request('https://ec.test/api/hosts/id', { method: 'DELETE', headers }), params: { id: hosts.host.id } })).toThrow(expect.objectContaining({ status: 403 }));
}
expect(hosts.getHost(hosts.host.id)).not.toBeNull();
+35 -1
View File
@@ -71,7 +71,7 @@ describe('native host readiness and reservations', () => {
expect(api.requireReadyHost(api.host.id).readiness).toBe('ready');
expect(() => api.requireReadyHost(api.host.id, { image: 'missing:image' })).toThrow('runtime');
expect(() => api.requireReadyHost(api.host.id, { libraryId: 'missing' })).toThrow('library');
expect(api.heartbeat(api.host.id, { ...inventory, libraries: [] }).readiness).toBe('preparing');
expect(api.heartbeat(api.host.id, { ...inventory, libraries: [] }).readiness).toBe('needs-game-files');
const host = api.heartbeat(api.host.id, { ...inventory, dockerRunning: false });
expect(host.connected).toBe(true); expect(host.readiness).toBe('degraded');
expect(() => api.requireReadyHost(host.id)).toThrow('Docker');
@@ -79,6 +79,40 @@ describe('native host readiness and reservations', () => {
vi.useFakeTimers(); vi.setSystemTime(Date.now() + 91_000);
expect(api.getHost(host.id)?.connectionState).toBe('offline'); expect(() => api.requireReadyHost(host.id)).toThrow('stale');
});
it.each([
{ missing: { libraries: [] }, expected: 'needs-game-files' },
{ missing: { runtimeImages: [] }, expected: 'needs-runtime' },
{ missing: { runtimeImages: [], libraries: [] }, expected: 'needs-runtime' }
])('names missing content as $expected until a prepared heartbeat arrives', async ({ missing, expected }) => {
const api = await enrolled();
const host = api.heartbeat(api.host.id, { ...inventory, ...missing });
expect(host.connected).toBe(true);
expect(host.readiness).toBe(expected);
expect(() => api.requireReadyHost(host.id)).toThrow();
expect(api.heartbeat(host.id, inventory).readiness).toBe('ready');
expect(api.requireReadyHost(host.id).id).toBe(host.id);
});
it('keeps setup work deliverable while new instance creation waits for missing game files', async () => {
const api = await enrolled();
const create = api.queueHostOperation(api.host.id, 'instance.create', {});
api.heartbeat(api.host.id, { ...inventory, libraries: [] });
expect(() => api.queueHostOperation(api.host.id, 'instance.create', {})).toThrow('game-file library');
const download = api.queueHostOperation(api.host.id, 'library.download', {});
const runtime = api.queueHostOperation(api.host.id, 'runtime.pull', {});
expect(api.pendingHostOperations(api.host.id).map(operation => operation.id)).toEqual([download.id, runtime.id]);
api.heartbeat(api.host.id, { ...inventory, runtimeImages: [], libraries: [] });
expect(api.pendingHostOperations(api.host.id).map(operation => operation.id)).toEqual([download.id, runtime.id]);
api.heartbeat(api.host.id, inventory);
expect(api.pendingHostOperations(api.host.id).map(operation => operation.id)).toContain(create.id);
});
it('gives enrollment, infrastructure failure and host modes priority over missing content', async () => {
const api = await enrolled();
expect(api.createHost({ name: 'New host' }).readiness).toBe('preparing');
expect(api.heartbeat(api.host.id, { ...inventory, libraries: [], dockerRunning: false }).readiness).toBe('degraded');
api.heartbeat(api.host.id, { ...inventory, libraries: [] });
expect(api.updateHost(api.host.id, { mode: 'draining' }).readiness).toBe('draining');
expect(api.updateHost(api.host.id, { mode: 'disabled' }).readiness).toBe('disabled');
});
it('blocks new deployments on a draining or disabled host without altering existing operations', async () => {
const api = await enrolled(); const existing = api.queueHostOperation(api.host.id, 'instance.inspect', {}, { instanceId: 'immutable-id', generation: 1 });
api.updateHost(api.host.id, { mode: 'draining' });