Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -3,8 +3,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { newFixture, type Fixture } from '../helpers/fixture.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();
|
||||
});
|
||||
|
||||
async function setup(capabilities: string[] = []) {
|
||||
const hosts = await import('../../src/lib/server/hosts/index.js');
|
||||
@@ -12,52 +17,94 @@ async function setup(capabilities: string[] = []) {
|
||||
hosts.enrollHost(hosts.createEnrollment(host.id).token, { protocolVersion: 1, capabilities });
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
db().prepare("INSERT INTO servers(name) VALUES('test')").run();
|
||||
db().prepare(`INSERT INTO native_instances(id,server,host_id,library_id,image,port,created_at)
|
||||
VALUES('instance','test',?,'library','image',28960,?)`).run(host.id, new Date().toISOString());
|
||||
db()
|
||||
.prepare(
|
||||
`INSERT INTO native_instances(id,server,host_id,library_id,image,port,created_at)
|
||||
VALUES('instance','test',?,'library','image',28960,?)`
|
||||
)
|
||||
.run(host.id, new Date().toISOString());
|
||||
const native = await import('../../src/lib/server/backends/native.js');
|
||||
const instance = native.nativeInstance('test')!;
|
||||
return { ...hosts, ...native, db: db(), host, instance };
|
||||
}
|
||||
|
||||
describe('overlapping native observations', () => {
|
||||
it.each([false, true])('negotiates incremental log reads with agent support: %s', async incremental => {
|
||||
const api = await setup(incremental ? ['logs.read-offset'] : []);
|
||||
const { nativeLogChunk } = await import('../../src/lib/server/backends/files.js');
|
||||
const cursor = { fileId: '1:2', offset: 3, size: 3, modified: '4', anchorLength: 3, anchorHash: 'hash' };
|
||||
const request = nativeLogChunk(join(fixture.t5, 'test', 'logs/games_sp.log'), cursor);
|
||||
const operation = api.listHostOperations(api.host.id)[0];
|
||||
expect(operation.payload).toEqual({ path: 'logs/games_sp.log', ...(incremental ? { mode: 'incremental', cursor } : { lines: 2000 }) });
|
||||
const result = incremental ? { contentBase64: '', cursor, reset: false } : { lines: ['legacy tail'] };
|
||||
api.reportHostOperationStatus(api.host.id, operation.id, { state: 'completed', result });
|
||||
expect(await request).toEqual(result);
|
||||
expect(api.getHostOperation(operation.id)?.result).toMatchObject({ retained: false });
|
||||
});
|
||||
it.each([false, true])(
|
||||
'negotiates incremental log reads with agent support: %s',
|
||||
async (incremental) => {
|
||||
const api = await setup(incremental ? ['logs.read-offset'] : []);
|
||||
const { nativeLogChunk } = await import('../../src/lib/server/backends/files.js');
|
||||
const cursor = {
|
||||
fileId: '1:2',
|
||||
offset: 3,
|
||||
size: 3,
|
||||
modified: '4',
|
||||
anchorLength: 3,
|
||||
anchorHash: 'hash'
|
||||
};
|
||||
const request = nativeLogChunk(join(fixture.t5, 'test', 'logs/games_sp.log'), cursor);
|
||||
const operation = api.listHostOperations(api.host.id)[0];
|
||||
expect(operation.payload).toEqual({
|
||||
path: 'logs/games_sp.log',
|
||||
...(incremental ? { mode: 'incremental', cursor } : { lines: 2000 })
|
||||
});
|
||||
const result = incremental
|
||||
? { contentBase64: '', cursor, reset: false }
|
||||
: { lines: ['legacy tail'] };
|
||||
api.reportHostOperationStatus(api.host.id, operation.id, {
|
||||
state: 'completed',
|
||||
result
|
||||
});
|
||||
expect(await request).toEqual(result);
|
||||
expect(api.getHostOperation(operation.id)?.result).toMatchObject({ retained: false });
|
||||
}
|
||||
);
|
||||
|
||||
it.each(['files.read', 'files.list', 'files.stat', 'files.manifest', 'logs.read', 'instance.inspect'])('shares %s only until completion', async kind => {
|
||||
it.each([
|
||||
'files.read',
|
||||
'files.list',
|
||||
'files.stat',
|
||||
'files.manifest',
|
||||
'logs.read',
|
||||
'instance.inspect'
|
||||
])('shares %s only until completion', async (kind) => {
|
||||
const api = await setup();
|
||||
const requests = Array.from({ length: 4 }, () => api.nativeRequest(api.instance, kind, { path: 'same' }));
|
||||
const requests = Array.from({ length: 4 }, () =>
|
||||
api.nativeRequest(api.instance, kind, { path: 'same' })
|
||||
);
|
||||
const operations = api.listHostOperations(api.host.id);
|
||||
expect(operations).toHaveLength(1);
|
||||
api.reportHostOperation(api.host.id, operations[0].id, { state: 'completed', result: { value: 'first' } });
|
||||
api.reportHostOperation(api.host.id, operations[0].id, {
|
||||
state: 'completed',
|
||||
result: { value: 'first' }
|
||||
});
|
||||
expect(await Promise.all(requests)).toEqual(Array(4).fill({ value: 'first' }));
|
||||
const fresh = api.nativeRequest(api.instance, kind, { path: 'same' });
|
||||
const next = api.listHostOperations(api.host.id)[0];
|
||||
expect(next.id).not.toBe(operations[0].id);
|
||||
api.reportHostOperation(api.host.id, next.id, { state: 'completed', result: { value: 'changed' } });
|
||||
api.reportHostOperation(api.host.id, next.id, {
|
||||
state: 'completed',
|
||||
result: { value: 'changed' }
|
||||
});
|
||||
expect(await fresh).toEqual({ value: 'changed' });
|
||||
});
|
||||
|
||||
it('does not merge different paths, mutations, or reads across lifecycle submissions', async () => {
|
||||
const api = await setup();
|
||||
const requests = [api.nativeRequest(api.instance, 'files.read', { path: 'one' }),
|
||||
const requests = [
|
||||
api.nativeRequest(api.instance, 'files.read', { path: 'one' }),
|
||||
api.nativeRequest(api.instance, 'files.read', { path: 'two' }),
|
||||
api.nativeRequest(api.instance, 'rcon.execute', { command: 'map_restart' }),
|
||||
api.nativeRequest(api.instance, 'rcon.execute', { command: 'map_restart' })];
|
||||
api.db.prepare("INSERT INTO restart_rollout(server,requested_at) VALUES('test',?)").run(new Date().toISOString());
|
||||
api.nativeRequest(api.instance, 'rcon.execute', { command: 'map_restart' })
|
||||
];
|
||||
api.db
|
||||
.prepare("INSERT INTO restart_rollout(server,requested_at) VALUES('test',?)")
|
||||
.run(new Date().toISOString());
|
||||
requests.push(api.nativeRequest(api.instance, 'files.read', { path: 'one' }));
|
||||
const operations = api.listHostOperations(api.host.id);
|
||||
expect(operations).toHaveLength(5);
|
||||
for (const operation of operations) api.reportHostOperation(api.host.id, operation.id, { state: 'completed', result: {} });
|
||||
for (const operation of operations)
|
||||
api.reportHostOperation(api.host.id, operation.id, { state: 'completed', result: {} });
|
||||
await Promise.all(requests);
|
||||
});
|
||||
|
||||
@@ -65,11 +112,17 @@ describe('overlapping native observations', () => {
|
||||
const api = await setup();
|
||||
const first = api.nativeRequest(api.instance, 'files.read', { path: 'one' });
|
||||
const rejected = expect(first).rejects.toThrow('unavailable');
|
||||
api.reportHostOperation(api.host.id, api.listHostOperations(api.host.id)[0].id, { state: 'failed', error: 'unavailable' });
|
||||
api.reportHostOperation(api.host.id, api.listHostOperations(api.host.id)[0].id, {
|
||||
state: 'failed',
|
||||
error: 'unavailable'
|
||||
});
|
||||
await rejected;
|
||||
const next = api.nativeRequest(api.instance, 'files.read', { path: 'one' });
|
||||
expect(api.listHostOperations(api.host.id)).toHaveLength(2);
|
||||
api.reportHostOperation(api.host.id, api.listHostOperations(api.host.id)[0].id, { state: 'completed', result: {} });
|
||||
api.reportHostOperation(api.host.id, api.listHostOperations(api.host.id)[0].id, {
|
||||
state: 'completed',
|
||||
result: {}
|
||||
});
|
||||
await next;
|
||||
});
|
||||
|
||||
@@ -78,12 +131,16 @@ describe('overlapping native observations', () => {
|
||||
const old = api.nativeRequest(api.instance, 'files.read', { path: 'one' });
|
||||
const rejected = expect(old).rejects.toThrow('Instance was replaced');
|
||||
api.db.prepare("UPDATE native_instances SET generation=2 WHERE id='instance'").run();
|
||||
const current = api.nativeRequest(api.nativeInstance('test')!, 'files.read', { path: 'one' });
|
||||
const current = api.nativeRequest(api.nativeInstance('test')!, 'files.read', {
|
||||
path: 'one'
|
||||
});
|
||||
const operations = api.listHostOperations(api.host.id);
|
||||
expect(operations).toHaveLength(2);
|
||||
for (const operation of operations) api.reportHostOperation(api.host.id, operation.id, {
|
||||
state: 'completed', result: { generation: operation.generation }
|
||||
});
|
||||
for (const operation of operations)
|
||||
api.reportHostOperation(api.host.id, operation.id, {
|
||||
state: 'completed',
|
||||
result: { generation: operation.generation }
|
||||
});
|
||||
await rejected;
|
||||
expect(await current).toEqual({ generation: 2 });
|
||||
});
|
||||
@@ -98,30 +155,48 @@ describe('native file reads through stored agent receipts', () => {
|
||||
const { readFile } = await import('../../src/lib/server/backends/files.js');
|
||||
const reading = readFile(join(fixture.t5, 'test', 'configs/server.cfg'));
|
||||
const operation = api.listHostOperations(api.host.id)[0];
|
||||
expect(operation).toMatchObject({ kind: 'files.read', payload: { path: 'configs/server.cfg' } });
|
||||
api.reportHostOperationStatus(api.host.id, operation.id, {
|
||||
state: 'completed', result: { contentBase64: content.toString('base64') }
|
||||
expect(operation).toMatchObject({
|
||||
kind: 'files.read',
|
||||
payload: { path: 'configs/server.cfg' }
|
||||
});
|
||||
api.reportHostOperationStatus(api.host.id, operation.id, {
|
||||
state: 'completed',
|
||||
result: { contentBase64: content.toString('base64') }
|
||||
});
|
||||
expect(api.getHostOperationStatus(operation.id)?.result).toEqual({
|
||||
contentSha256: expect.any(String)
|
||||
});
|
||||
expect(api.getHostOperationStatus(operation.id)?.result).toEqual({ contentSha256: expect.any(String) });
|
||||
expect(await reading).toEqual(content);
|
||||
});
|
||||
|
||||
it('preserves missing-file errors from the agent', async () => {
|
||||
const api = await setup();
|
||||
const { readFile } = await import('../../src/lib/server/backends/files.js');
|
||||
const reading = expect(readFile(join(fixture.t5, 'test', 'missing'))).rejects.toMatchObject({ code: 'ENOENT' });
|
||||
const reading = expect(readFile(join(fixture.t5, 'test', 'missing'))).rejects.toMatchObject(
|
||||
{ code: 'ENOENT' }
|
||||
);
|
||||
api.reportHostOperationStatus(api.host.id, api.listHostOperations(api.host.id)[0].id, {
|
||||
state: 'completed', result: { contentBase64: null }
|
||||
state: 'completed',
|
||||
result: { contentBase64: null }
|
||||
});
|
||||
await reading;
|
||||
});
|
||||
|
||||
it.each([null, {}, { contentBase64: 123 }, { contentSha256: 123 }])('reports an invalid response for malformed file content: %j', async result => {
|
||||
const api = await setup();
|
||||
const { readFile } = await import('../../src/lib/server/backends/files.js');
|
||||
const reading = expect(readFile(join(fixture.t5, 'test', 'configs/server.cfg')))
|
||||
.rejects.toThrow('Agent returned an invalid response for files.read: configs/server.cfg');
|
||||
api.reportHostOperationStatus(api.host.id, api.listHostOperations(api.host.id)[0].id, { state: 'completed', result });
|
||||
await reading;
|
||||
});
|
||||
it.each([null, {}, { contentBase64: 123 }, { contentSha256: 123 }])(
|
||||
'reports an invalid response for malformed file content: %j',
|
||||
async (result) => {
|
||||
const api = await setup();
|
||||
const { readFile } = await import('../../src/lib/server/backends/files.js');
|
||||
const reading = expect(
|
||||
readFile(join(fixture.t5, 'test', 'configs/server.cfg'))
|
||||
).rejects.toThrow(
|
||||
'Agent returned an invalid response for files.read: configs/server.cfg'
|
||||
);
|
||||
api.reportHostOperationStatus(api.host.id, api.listHostOperations(api.host.id)[0].id, {
|
||||
state: 'completed',
|
||||
result
|
||||
});
|
||||
await reading;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user