Let agents idle for updates required by creation cancellation

This commit is contained in:
2026-10-04 18:14:11 -04:00
parent 176b7dd86b
commit fae7775c72
5 changed files with 65 additions and 2 deletions
+37
View File
@@ -136,6 +136,43 @@ describe('native host readiness and reservations', () => {
});
describe('durable host operations', () => {
it('drains an old agent for a cancellation update while allowing receipts, heartbeats, and other hosts', async () => {
const api = await enrolled();
const original = api.queueHostOperation(api.host.id, 'instance.create', {}, { instanceId: 'cancelled-instance', generation: 1 });
const inspect = api.queueHostOperation(api.host.id, 'instance.inspect', {});
expect(api.pendingHostOperations(api.host.id).map(op => op.id)).toContain(original.id);
api.reportHostOperation(api.host.id, original.id, { state: 'running' });
api.reportHostOperation(api.host.id, inspect.id, { state: 'unknown' });
const cleanup = api.queueHostOperation(api.host.id, 'instance.cancel-create', { createOperationId: original.id }, { instanceId: 'cancelled-instance', generation: 1 });
const logs = api.queueHostOperation(api.host.id, 'logs.read', {});
const preparation = api.queueHostOperation(api.host.id, 'library.download', {});
const other = api.createHost({ name: 'Unaffected host' });
api.enrollHost(api.createEnrollment(other.id).token, inventory);
const otherWork = api.queueHostOperation(other.id, 'host.inspect', {});
const { db } = await import('../../src/lib/server/db/index.js');
const before = db().prepare('SELECT id,state,last_delivered_at FROM ec_host_operations WHERE host_id=? ORDER BY id').all(api.host.id);
for (let poll = 0; poll < 3; poll++) expect(api.pendingHostOperations(api.host.id)).toEqual([]);
expect(db().prepare('SELECT id,state,last_delivered_at FROM ec_host_operations WHERE host_id=? ORDER BY id').all(api.host.id)).toEqual(before);
expect(api.pendingHostOperations(other.id).map(op => op.id)).toEqual([otherWork.id]);
api.reportHostOperation(api.host.id, inspect.id, { state: 'completed', result: { healthy: true } });
api.heartbeat(api.host.id, inventory);
expect(api.pendingHostOperations(api.host.id)).toEqual([]);
// The controller hold must clear on capability, without waiting for cleanup
// or the original creation to finish (which would recreate the deadlock).
api.heartbeat(api.host.id, { ...inventory, capabilities: ['instance.cancel-create'] });
expect(api.pendingHostOperations(api.host.id).map(op => op.id)).toEqual([cleanup.id, logs.id, preparation.id]);
expect(api.getHostOperation(original.id)?.state).toBe('running');
expect(api.getHostOperation(cleanup.id)?.state).toBe('pending');
});
it.each(['completed', 'failed'] as const)('does not pause an old host for a terminal %s cancellation', async state => {
const api = await enrolled();
const cleanup = api.queueHostOperation(api.host.id, 'instance.cancel-create', { createOperationId: 'creation' });
api.reportHostOperation(api.host.id, cleanup.id, { state });
const work = api.queueHostOperation(api.host.id, 'host.inspect', {});
expect(api.pendingHostOperations(api.host.id).map(op => op.id)).toEqual([work.id]);
});
it('rotates unresolved operation delivery so an old batch cannot starve new work', async () => {
const api = await enrolled();
const operations = Array.from({ length: 30 }, () => api.queueHostOperation(api.host.id, 'instance.inspect', {}));