Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -5,13 +5,26 @@ import { newFixture, type Fixture } from '../helpers/fixture.js';
|
||||
|
||||
let fixture: Fixture;
|
||||
const epoch = Date.parse('2026-10-05T12:00:00Z');
|
||||
const inventory = { hostname: 'request-cache-host', protocolVersion: 1, dockerRunning: true, directoriesWritable: true,
|
||||
storageAvailable: 100 * 1024 ** 3, runtimeImages: ['plutainer:test'], libraries: [{ id: 'test-library', state: 'ready' }] };
|
||||
const inventory = {
|
||||
hostname: 'request-cache-host',
|
||||
protocolVersion: 1,
|
||||
dockerRunning: true,
|
||||
directoriesWritable: true,
|
||||
storageAvailable: 100 * 1024 ** 3,
|
||||
runtimeImages: ['plutainer:test'],
|
||||
libraries: [{ id: 'test-library', state: 'ready' }]
|
||||
};
|
||||
beforeEach(async () => {
|
||||
vi.resetModules(); vi.useFakeTimers({ toFake: ['Date'] }); vi.setSystemTime(epoch);
|
||||
vi.resetModules();
|
||||
vi.useFakeTimers({ toFake: ['Date'] });
|
||||
vi.setSystemTime(epoch);
|
||||
fixture = await newFixture([]);
|
||||
});
|
||||
afterEach(async () => { vi.restoreAllMocks(); vi.useRealTimers(); await fixture.cleanup(); });
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
await fixture.cleanup();
|
||||
});
|
||||
|
||||
async function enrolled() {
|
||||
const hosts = await import('../../src/lib/server/hosts/index.js');
|
||||
@@ -27,40 +40,53 @@ describe('agent preparation admission', () => {
|
||||
it('skips fleet preparation lookups for empty, health, read and unlimited library polls', async () => {
|
||||
const api = await enrolled();
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { fleetSettings, saveFleetSettings } = await import('../../src/lib/server/settings.js');
|
||||
saveFleetSettings({...fleetSettings(),preparationConcurrency:1});
|
||||
const prepare = vi.spyOn(db(),'prepare');
|
||||
const { fleetSettings, saveFleetSettings } =
|
||||
await import('../../src/lib/server/settings.js');
|
||||
saveFleetSettings({ ...fleetSettings(), preparationConcurrency: 1 });
|
||||
const prepare = vi.spyOn(db(), 'prepare');
|
||||
expect(api.pendingHostOperations(api.hostId)).toEqual([]);
|
||||
api.queueHostOperation(api.hostId,'instance.inspect',{});
|
||||
api.pendingHostOperations(api.hostId,'health');
|
||||
api.queueHostOperation(api.hostId,'files.read',{path:'server.cfg'});
|
||||
api.pendingHostOperations(api.hostId,'read');
|
||||
saveFleetSettings({...fleetSettings(),preparationConcurrency:0});
|
||||
const download = api.queueHostOperation(api.hostId,'library.download',{});
|
||||
expect(api.pendingHostOperations(api.hostId,'write').map(op=>op.id)).toContain(download.id);
|
||||
expect(prepare.mock.calls.some(([sql])=>sql.includes('SELECT DISTINCT host_id'))).toBe(false);
|
||||
api.queueHostOperation(api.hostId, 'instance.inspect', {});
|
||||
api.pendingHostOperations(api.hostId, 'health');
|
||||
api.queueHostOperation(api.hostId, 'files.read', { path: 'server.cfg' });
|
||||
api.pendingHostOperations(api.hostId, 'read');
|
||||
saveFleetSettings({ ...fleetSettings(), preparationConcurrency: 0 });
|
||||
const download = api.queueHostOperation(api.hostId, 'library.download', {});
|
||||
expect(api.pendingHostOperations(api.hostId, 'write').map((op) => op.id)).toContain(
|
||||
download.id
|
||||
);
|
||||
expect(prepare.mock.calls.some(([sql]) => sql.includes('SELECT DISTINCT host_id'))).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('uses an indexed lookup and counts delivered preparation by host until all its jobs finish', async () => {
|
||||
const api = await enrolled();
|
||||
const other = api.createHost({name:'Other host'});
|
||||
api.enrollHost(api.createEnrollment(other.id).token,inventory);
|
||||
const other = api.createHost({ name: 'Other host' });
|
||||
api.enrollHost(api.createEnrollment(other.id).token, inventory);
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { fleetSettings, saveFleetSettings } = await import('../../src/lib/server/settings.js');
|
||||
saveFleetSettings({...fleetSettings(),preparationConcurrency:1});
|
||||
const download = api.queueHostOperation(api.hostId,'library.download',{});
|
||||
const imported = api.queueHostOperation(api.hostId,'library.import',{});
|
||||
const waiting = api.queueHostOperation(other.id,'library.download',{});
|
||||
const prepare = vi.spyOn(db(),'prepare');
|
||||
expect(api.pendingHostOperations(api.hostId,'write').map(op=>op.id)).toEqual([download.id,imported.id]);
|
||||
const sql = prepare.mock.calls.find(([sql])=>sql.includes('SELECT DISTINCT host_id'))![0];
|
||||
const plan = db().prepare(`EXPLAIN QUERY PLAN ${sql}`).all() as {detail:string}[];
|
||||
expect(plan.map(row=>row.detail).join('\n')).toContain('USING INDEX ec_host_operations_kind_state (kind=? AND state=?)');
|
||||
expect(api.pendingHostOperations(other.id,'write')).toEqual([]);
|
||||
api.reportHostOperation(api.hostId,download.id,{state:'completed'});
|
||||
expect(api.pendingHostOperations(other.id,'write')).toEqual([]);
|
||||
api.reportHostOperation(api.hostId,imported.id,{state:'failed'});
|
||||
expect(api.pendingHostOperations(other.id,'write').map(op=>op.id)).toEqual([waiting.id]);
|
||||
const { fleetSettings, saveFleetSettings } =
|
||||
await import('../../src/lib/server/settings.js');
|
||||
saveFleetSettings({ ...fleetSettings(), preparationConcurrency: 1 });
|
||||
const download = api.queueHostOperation(api.hostId, 'library.download', {});
|
||||
const imported = api.queueHostOperation(api.hostId, 'library.import', {});
|
||||
const waiting = api.queueHostOperation(other.id, 'library.download', {});
|
||||
const prepare = vi.spyOn(db(), 'prepare');
|
||||
expect(api.pendingHostOperations(api.hostId, 'write').map((op) => op.id)).toEqual([
|
||||
download.id,
|
||||
imported.id
|
||||
]);
|
||||
const sql = prepare.mock.calls.find(([sql]) => sql.includes('SELECT DISTINCT host_id'))![0];
|
||||
const plan = db().prepare(`EXPLAIN QUERY PLAN ${sql}`).all() as { detail: string }[];
|
||||
expect(plan.map((row) => row.detail).join('\n')).toContain(
|
||||
'USING INDEX ec_host_operations_kind_state (kind=? AND state=?)'
|
||||
);
|
||||
expect(api.pendingHostOperations(other.id, 'write')).toEqual([]);
|
||||
api.reportHostOperation(api.hostId, download.id, { state: 'completed' });
|
||||
expect(api.pendingHostOperations(other.id, 'write')).toEqual([]);
|
||||
api.reportHostOperation(api.hostId, imported.id, { state: 'failed' });
|
||||
expect(api.pendingHostOperations(other.id, 'write').map((op) => op.id)).toEqual([
|
||||
waiting.id
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,16 +98,24 @@ describe('agent credential usage writes', () => {
|
||||
CREATE TRIGGER count_credential_usage AFTER UPDATE OF last_used_at ON ec_host_credentials
|
||||
BEGIN INSERT INTO credential_usage_writes VALUES (NEW.id, NEW.last_used_at); END;`);
|
||||
const writes = () => db().prepare('SELECT at FROM credential_usage_writes').all();
|
||||
const authenticate = () => expect(api.authenticateCredential(api.credential)?.id).toBe(api.hostId);
|
||||
authenticate(); authenticate(); authenticate();
|
||||
const authenticate = () =>
|
||||
expect(api.authenticateCredential(api.credential)?.id).toBe(api.hostId);
|
||||
authenticate();
|
||||
authenticate();
|
||||
authenticate();
|
||||
expect(writes()).toEqual([{ at: new Date(epoch).toISOString() }]);
|
||||
vi.setSystemTime(epoch + 59_999);
|
||||
authenticate();
|
||||
closeDb(); authenticate();
|
||||
closeDb();
|
||||
authenticate();
|
||||
expect(writes()).toHaveLength(1);
|
||||
vi.setSystemTime(epoch + 60_000);
|
||||
authenticate(); authenticate();
|
||||
expect(writes()).toEqual([{ at: new Date(epoch).toISOString() }, { at: new Date(epoch + 60_000).toISOString() }]);
|
||||
authenticate();
|
||||
authenticate();
|
||||
expect(writes()).toEqual([
|
||||
{ at: new Date(epoch).toISOString() },
|
||||
{ at: new Date(epoch + 60_000).toISOString() }
|
||||
]);
|
||||
expect(api.authenticateCredential('invalid')).toBeNull();
|
||||
expect(writes()).toHaveLength(2);
|
||||
});
|
||||
@@ -95,8 +129,13 @@ describe('agent credential usage writes', () => {
|
||||
const replacement = api.rotateCredential(api.hostId);
|
||||
expect(api.authenticateCredential(api.credential)).toBeNull();
|
||||
expect(api.authenticateCredential(replacement)?.id).toBe(api.hostId);
|
||||
expect(db().prepare('SELECT last_used_at FROM ec_host_credentials WHERE revoked_at IS NULL').get())
|
||||
.toEqual({ last_used_at: new Date(epoch + 1000).toISOString() });
|
||||
expect(
|
||||
db()
|
||||
.prepare(
|
||||
'SELECT last_used_at FROM ec_host_credentials WHERE revoked_at IS NULL'
|
||||
)
|
||||
.get()
|
||||
).toEqual({ last_used_at: new Date(epoch + 1000).toISOString() });
|
||||
api.revokeCredentials(api.hostId);
|
||||
expect(api.authenticateCredential(replacement)).toBeNull();
|
||||
expect(api.getHost(api.hostId)?.credentialActive).toBe(false);
|
||||
@@ -111,11 +150,14 @@ describe('request-scoped host inventory parsing', () => {
|
||||
const parse = vi.spyOn(JSON, 'parse');
|
||||
await api.withHostRequestContext(async () => {
|
||||
const authenticated = api.authenticateCredential(api.credential)!;
|
||||
const operation = api.queueHostOperation(api.hostId, 'instance.inspect', { name: 'server' });
|
||||
const operation = api.queueHostOperation(api.hostId, 'instance.inspect', {
|
||||
name: 'server'
|
||||
});
|
||||
api.pendingHostOperationRows(api.hostId, 'read', false);
|
||||
api.reportHostOperationStatus(api.hostId, operation.id, { state: 'accepted' });
|
||||
await Promise.resolve();
|
||||
for (let i = 0; i < 10; i++) expect(api.getHost(api.hostId)?.inventory).toBe(authenticated.inventory);
|
||||
for (let i = 0; i < 10; i++)
|
||||
expect(api.getHost(api.hostId)?.inventory).toBe(authenticated.inventory);
|
||||
expect(api.requireReadyHost(api.hostId).inventory).toBe(authenticated.inventory);
|
||||
expect(api.listHosts()[0].inventory).toBe(authenticated.inventory);
|
||||
});
|
||||
@@ -130,17 +172,25 @@ describe('request-scoped host inventory parsing', () => {
|
||||
await api.withHostRequestContext(() => {
|
||||
const original = api.getHost(api.hostId)!;
|
||||
const updated = api.updateHost(api.hostId, { name: 'Renamed host', mode: 'draining' });
|
||||
expect(updated).toMatchObject({ name: 'Renamed host', mode: 'draining', readiness: 'draining' });
|
||||
expect(updated).toMatchObject({
|
||||
name: 'Renamed host',
|
||||
mode: 'draining',
|
||||
readiness: 'draining'
|
||||
});
|
||||
expect(updated.inventory).toBe(original.inventory);
|
||||
const heartbeat = api.heartbeat(api.hostId, { ...inventory, storageAvailable: 123 });
|
||||
expect(heartbeat.inventory).not.toBe(original.inventory);
|
||||
expect(api.getHost(api.hostId)?.inventory).toBe(heartbeat.inventory);
|
||||
expect(api.getHost(api.hostId)?.inventory.storageAvailable).toBe(123);
|
||||
expect(() => db().transaction(() => {
|
||||
db().prepare('UPDATE ec_hosts SET inventory=? WHERE id=?').run(JSON.stringify({ ...inventory, storageAvailable: 456 }), api.hostId);
|
||||
expect(api.getHost(api.hostId)?.inventory.storageAvailable).toBe(456);
|
||||
throw new Error('rollback');
|
||||
})()).toThrow('rollback');
|
||||
expect(() =>
|
||||
db().transaction(() => {
|
||||
db()
|
||||
.prepare('UPDATE ec_hosts SET inventory=? WHERE id=?')
|
||||
.run(JSON.stringify({ ...inventory, storageAvailable: 456 }), api.hostId);
|
||||
expect(api.getHost(api.hostId)?.inventory.storageAvailable).toBe(456);
|
||||
throw new Error('rollback');
|
||||
})()
|
||||
).toThrow('rollback');
|
||||
expect(api.getHost(api.hostId)?.inventory.storageAvailable).toBe(123);
|
||||
});
|
||||
});
|
||||
@@ -148,7 +198,9 @@ describe('request-scoped host inventory parsing', () => {
|
||||
it('isolates overlapping requests and does not cache reads outside a request', async () => {
|
||||
const api = await enrolled();
|
||||
let release!: () => void;
|
||||
const gate = new Promise<void>(resolve => { release = resolve; });
|
||||
const gate = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
let first!: HostInventory;
|
||||
const request = api.withHostRequestContext(async () => {
|
||||
first = api.getHost(api.hostId)!.inventory;
|
||||
@@ -163,25 +215,35 @@ describe('request-scoped host inventory parsing', () => {
|
||||
expect(api.getHost(api.hostId)!.inventory).toBe(second);
|
||||
});
|
||||
expect(api.getHost(api.hostId)!.inventory).not.toBe(api.getHost(api.hostId)!.inventory);
|
||||
} finally { release(); await request; }
|
||||
} finally {
|
||||
release();
|
||||
await request;
|
||||
}
|
||||
});
|
||||
|
||||
it.each([false, true])('releases cached inventory from inherited work after request completion (failed: %s)', async failed => {
|
||||
const api = await enrolled();
|
||||
let release!: () => void;
|
||||
const gate = new Promise<void>(resolve => { release = resolve; });
|
||||
let detached!: Promise<void>;
|
||||
let original!: HostInventory;
|
||||
const request = api.withHostRequestContext(() => {
|
||||
original = api.getHost(api.hostId)!.inventory;
|
||||
detached = gate.then(() => {
|
||||
const later = api.getHost(api.hostId)!.inventory;
|
||||
expect(later).not.toBe(original);
|
||||
expect(api.getHost(api.hostId)!.inventory).not.toBe(later);
|
||||
it.each([false, true])(
|
||||
'releases cached inventory from inherited work after request completion (failed: %s)',
|
||||
async (failed) => {
|
||||
const api = await enrolled();
|
||||
let release!: () => void;
|
||||
const gate = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
if (failed) throw new Error('request failed');
|
||||
});
|
||||
if (failed) await expect(request).rejects.toThrow('request failed'); else await request;
|
||||
release(); await detached;
|
||||
});
|
||||
let detached!: Promise<void>;
|
||||
let original!: HostInventory;
|
||||
const request = api.withHostRequestContext(() => {
|
||||
original = api.getHost(api.hostId)!.inventory;
|
||||
detached = gate.then(() => {
|
||||
const later = api.getHost(api.hostId)!.inventory;
|
||||
expect(later).not.toBe(original);
|
||||
expect(api.getHost(api.hostId)!.inventory).not.toBe(later);
|
||||
});
|
||||
if (failed) throw new Error('request failed');
|
||||
});
|
||||
if (failed) await expect(request).rejects.toThrow('request failed');
|
||||
else await request;
|
||||
release();
|
||||
await detached;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user