Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -1,11 +1,19 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
||||
import { enableIncrementalVacuum, retentionClocks } from '../../src/lib/server/db/retention-clocks.js';
|
||||
import {
|
||||
enableIncrementalVacuum,
|
||||
retentionClocks
|
||||
} from '../../src/lib/server/db/retention-clocks.js';
|
||||
|
||||
let fixture: Fixture;
|
||||
beforeEach(async () => { vi.resetModules(); fixture = await newFixture([]); });
|
||||
afterEach(async () => { await fixture.cleanup(); });
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
fixture = await newFixture([]);
|
||||
});
|
||||
afterEach(async () => {
|
||||
await fixture.cleanup();
|
||||
});
|
||||
const old = new Date(Date.now() - 90 * 86400000).toISOString();
|
||||
|
||||
it('expires completed file payloads while keeping history, replay identity, holds and active work', async () => {
|
||||
@@ -14,9 +22,16 @@ it('expires completed file payloads while keeping history, replay identity, hold
|
||||
const { pruneHistory, setRetentionControl } = await import('../../src/lib/server/retention.js');
|
||||
const { putFileBlob } = await import('../../src/lib/server/db/file-blobs.js');
|
||||
const host = hosts.createHost({ name: 'Files' });
|
||||
const payload = { files: [{ path: 'plugin.dll', contentBase64: Buffer.from('file').toString('base64') }] };
|
||||
const completed = hosts.queueHostOperation(host.id, 'files.stage', payload, { idempotencyKey: 'finished' });
|
||||
hosts.reportHostOperation(host.id, completed.id, { state: 'completed', result: { before: [{ path: 'plugin.dll', contentBase64: 'YmVmb3Jl' }] } });
|
||||
const payload = {
|
||||
files: [{ path: 'plugin.dll', contentBase64: Buffer.from('file').toString('base64') }]
|
||||
};
|
||||
const completed = hosts.queueHostOperation(host.id, 'files.stage', payload, {
|
||||
idempotencyKey: 'finished'
|
||||
});
|
||||
hosts.reportHostOperation(host.id, completed.id, {
|
||||
state: 'completed',
|
||||
result: { before: [{ path: 'plugin.dll', contentBase64: 'YmVmb3Jl' }] }
|
||||
});
|
||||
const active = hosts.queueHostOperation(host.id, 'files.stage', payload);
|
||||
const held = hosts.queueHostOperation(host.id, 'files.stage', payload);
|
||||
hosts.reportHostOperation(host.id, held.id, { state: 'completed', result: {} });
|
||||
@@ -26,22 +41,49 @@ it('expires completed file payloads while keeping history, replay identity, hold
|
||||
db().prepare('UPDATE ec_host_operations SET updated_at=?').run(old);
|
||||
db().exec("INSERT INTO servers(name) VALUES('s')");
|
||||
const hash = putFileBlob(Buffer.from('recipe-only'));
|
||||
db().prepare(`INSERT INTO server_deployments(id,server,action,state,stage,started_at,finished_at,triggered_by)
|
||||
VALUES('launch','s','create','done','done',?,?,'test')`).run(old, old);
|
||||
db().prepare('INSERT INTO server_launch_recipes(deployment_id,recipe) VALUES(?,?)')
|
||||
.run('launch', JSON.stringify({ files: [{ path: 'launch.dll', sha256: hash, contentHash: hash }] }));
|
||||
db()
|
||||
.prepare(
|
||||
`INSERT INTO server_deployments(id,server,action,state,stage,started_at,finished_at,triggered_by)
|
||||
VALUES('launch','s','create','done','done',?,?,'test')`
|
||||
)
|
||||
.run(old, old);
|
||||
db()
|
||||
.prepare('INSERT INTO server_launch_recipes(deployment_id,recipe) VALUES(?,?)')
|
||||
.run(
|
||||
'launch',
|
||||
JSON.stringify({ files: [{ path: 'launch.dll', sha256: hash, contentHash: hash }] })
|
||||
);
|
||||
expect(pruneHistory('workflow-payload')).toBe(1);
|
||||
expect(db().prepare("SELECT state FROM server_deployments WHERE id='launch'").get()).toEqual({ state: 'done' });
|
||||
expect(JSON.parse((db().prepare('SELECT recipe FROM server_launch_recipes').get() as { recipe: string }).recipe))
|
||||
.toEqual({ files: [{ path: 'launch.dll', sha256: hash }] });
|
||||
expect(db().prepare("SELECT state FROM server_deployments WHERE id='launch'").get()).toEqual({
|
||||
state: 'done'
|
||||
});
|
||||
expect(
|
||||
JSON.parse(
|
||||
(db().prepare('SELECT recipe FROM server_launch_recipes').get() as { recipe: string })
|
||||
.recipe
|
||||
)
|
||||
).toEqual({ files: [{ path: 'launch.dll', sha256: hash }] });
|
||||
expect(db().prepare('SELECT 1 FROM file_blobs WHERE sha256=?').get(hash)).toBeUndefined();
|
||||
expect(pruneHistory('operation-payload')).toBe(1);
|
||||
expect(hosts.getHostOperation(completed.id)).toMatchObject({ state: 'completed', payload: { files: [{ path: 'plugin.dll' }] }, result: { before: [{ path: 'plugin.dll' }] } });
|
||||
expect(hosts.getHostOperation(completed.id)).toMatchObject({
|
||||
state: 'completed',
|
||||
payload: { files: [{ path: 'plugin.dll' }] },
|
||||
result: { before: [{ path: 'plugin.dll' }] }
|
||||
});
|
||||
expect(hosts.getHostOperation(active.id)?.payload).toEqual(payload);
|
||||
expect(hosts.getHostOperation(held.id)?.payload).toEqual(payload);
|
||||
expect(hosts.getHostOperation(failed.id)?.payload).toEqual(payload);
|
||||
expect(hosts.queueHostOperation(host.id, 'files.stage', payload, { idempotencyKey: 'finished' }).id).toBe(completed.id);
|
||||
expect(() => hosts.queueHostOperation(host.id, 'files.stage', { files: [] }, { idempotencyKey: 'finished' })).toThrow('different request');
|
||||
expect(
|
||||
hosts.queueHostOperation(host.id, 'files.stage', payload, { idempotencyKey: 'finished' }).id
|
||||
).toBe(completed.id);
|
||||
expect(() =>
|
||||
hosts.queueHostOperation(
|
||||
host.id,
|
||||
'files.stage',
|
||||
{ files: [] },
|
||||
{ idempotencyKey: 'finished' }
|
||||
)
|
||||
).toThrow('different request');
|
||||
expect(pruneHistory('operation-payload')).toBe(0);
|
||||
});
|
||||
|
||||
@@ -52,16 +94,37 @@ it('retains rebalance recovery files until the move is reconciled, then lets his
|
||||
const { putFileBlob } = await import('../../src/lib/server/db/file-blobs.js');
|
||||
const { fleetSettings, saveFleetSettings } = await import('../../src/lib/server/settings.js');
|
||||
ensureHostSchema(db());
|
||||
saveFleetSettings({ ...fleetSettings(), historyKeepCount: 0, provisioningKeepDays: 1, payloadKeepDays: 1 });
|
||||
db().prepare(`INSERT INTO server_deployments(id,server,action,state,stage,started_at,finished_at,triggered_by)
|
||||
VALUES('move','s','rebuild','done','healthy',?,?,'rebalance:run')`).run(old, old);
|
||||
saveFleetSettings({
|
||||
...fleetSettings(),
|
||||
historyKeepCount: 0,
|
||||
provisioningKeepDays: 1,
|
||||
payloadKeepDays: 1
|
||||
});
|
||||
db()
|
||||
.prepare(
|
||||
`INSERT INTO server_deployments(id,server,action,state,stage,started_at,finished_at,triggered_by)
|
||||
VALUES('move','s','rebuild','done','healthy',?,?,'rebalance:run')`
|
||||
)
|
||||
.run(old, old);
|
||||
const hash = putFileBlob(Buffer.from('frozen deployed file'));
|
||||
db().prepare('INSERT INTO server_launch_recipes(deployment_id,recipe) VALUES(?,?)')
|
||||
.run('move', JSON.stringify({ files: [{ path: 'script.gsc', sha256: hash, contentHash: hash }] }));
|
||||
db().prepare(`INSERT INTO rebalancing_runs(id,settings,state,created_at,updated_at,message)
|
||||
VALUES('run','{}','running',?,?,'waiting')`).run(old, old);
|
||||
db().prepare(`INSERT INTO rebalancing_moves(id,run_id,ordinal,server,instance_id,generation,source_host_id,target_host_id,candidate,state,operation_id,message,updated_at)
|
||||
VALUES('m','run',0,'s','i',1,'a','b','{}','moving','move','waiting',?)`).run(old);
|
||||
db()
|
||||
.prepare('INSERT INTO server_launch_recipes(deployment_id,recipe) VALUES(?,?)')
|
||||
.run(
|
||||
'move',
|
||||
JSON.stringify({ files: [{ path: 'script.gsc', sha256: hash, contentHash: hash }] })
|
||||
);
|
||||
db()
|
||||
.prepare(
|
||||
`INSERT INTO rebalancing_runs(id,settings,state,created_at,updated_at,message)
|
||||
VALUES('run','{}','running',?,?,'waiting')`
|
||||
)
|
||||
.run(old, old);
|
||||
db()
|
||||
.prepare(
|
||||
`INSERT INTO rebalancing_moves(id,run_id,ordinal,server,instance_id,generation,source_host_id,target_host_id,candidate,state,operation_id,message,updated_at)
|
||||
VALUES('m','run',0,'s','i',1,'a','b','{}','moving','move','waiting',?)`
|
||||
)
|
||||
.run(old);
|
||||
expect(pruneHistory('workflow-payload')).toBe(0);
|
||||
expect(pruneHistory('provisioning')).toBe(0);
|
||||
db().prepare("UPDATE rebalancing_moves SET state='failed'").run();
|
||||
@@ -70,7 +133,10 @@ it('retains rebalance recovery files until the move is reconciled, then lets his
|
||||
db().prepare("UPDATE rebalancing_moves SET state='done'").run();
|
||||
expect(pruneHistory('workflow-payload')).toBe(1);
|
||||
expect(pruneHistory('provisioning')).toBe(1);
|
||||
expect(db().prepare('SELECT state,operation_id FROM rebalancing_moves').get()).toEqual({ state: 'done', operation_id: null });
|
||||
expect(db().prepare('SELECT state,operation_id FROM rebalancing_moves').get()).toEqual({
|
||||
state: 'done',
|
||||
operation_id: null
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes legacy retention clocks once and reuses clock indexes', () => {
|
||||
@@ -80,13 +146,17 @@ it('normalizes legacy retention clocks once and reuses clock indexes', () => {
|
||||
INSERT INTO history(at) VALUES('2026-01-01 12:00:00'),('2026-01-01T08:00:00-04:00'),(NULL)`);
|
||||
retentionClocks(handle, { history: ['at'] });
|
||||
expect(handle.prepare('SELECT at FROM history ORDER BY id').all()).toEqual([
|
||||
{ at: '2026-01-01T12:00:00.000Z' }, { at: '2026-01-01T12:00:00.000Z' }, { at: null }
|
||||
{ at: '2026-01-01T12:00:00.000Z' },
|
||||
{ at: '2026-01-01T12:00:00.000Z' },
|
||||
{ at: null }
|
||||
]);
|
||||
expect(handle.pragma('index_list(history)')).toHaveLength(1);
|
||||
const prepare = vi.spyOn(handle, 'prepare');
|
||||
retentionClocks(handle, { history: ['at'] });
|
||||
expect(prepare.mock.calls.some(([sql]) => sql.startsWith('UPDATE'))).toBe(false);
|
||||
} finally { handle.close(); }
|
||||
} finally {
|
||||
handle.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('enables incremental vacuum once and preserves host enable command boundaries', () => {
|
||||
@@ -98,12 +168,19 @@ it('enables incremental vacuum once and preserves host enable command boundaries
|
||||
INSERT INTO ec_host_enable_requests VALUES('h',20)`);
|
||||
enableIncrementalVacuum(handle);
|
||||
expect(handle.pragma('auto_vacuum', { simple: true })).toBe(2);
|
||||
const eligible = () => handle.prepare(`SELECT id FROM ec_host_operations o JOIN ec_host_enable_requests e ON e.host_id=o.host_id
|
||||
WHERE o.rowid>e.operation_floor`).all();
|
||||
const eligible = () =>
|
||||
handle
|
||||
.prepare(
|
||||
`SELECT id FROM ec_host_operations o JOIN ec_host_enable_requests e ON e.host_id=o.host_id
|
||||
WHERE o.rowid>e.operation_floor`
|
||||
)
|
||||
.all();
|
||||
expect(eligible()).toEqual([{ id: 'after' }]);
|
||||
const exec = vi.spyOn(handle, 'exec');
|
||||
enableIncrementalVacuum(handle);
|
||||
expect(exec.mock.calls).toHaveLength(0);
|
||||
expect(eligible()).toEqual([{ id: 'after' }]);
|
||||
} finally { handle.close(); }
|
||||
} finally {
|
||||
handle.close();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user