Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -12,127 +12,263 @@ beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
fixture = await newFixture([]);
|
||||
});
|
||||
afterEach(async () => { await fixture.cleanup(); vi.restoreAllMocks(); });
|
||||
afterEach(async () => {
|
||||
await fixture.cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
async function setup(capable = true) {
|
||||
const hosts = await import('../../src/lib/server/hosts/index.js');
|
||||
const host = hosts.createHost({ name: 'Drift manifest host' });
|
||||
hosts.enrollHost(hosts.createEnrollment(host.id).token, {
|
||||
protocolVersion: 1, capabilities: capable ? ['files.manifest'] : []
|
||||
protocolVersion: 1,
|
||||
capabilities: capable ? ['files.manifest'] : []
|
||||
});
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const database = db();
|
||||
database.prepare(`INSERT INTO servers(name,game,map,game_mode,map_slug,created_by)
|
||||
VALUES(?,'t5','zombie_theater','zombie','theater','ui')`).run(server);
|
||||
database.prepare(`INSERT INTO native_instances(id,server,host_id,library_id,image,port,created_at)
|
||||
VALUES(?,?,?,'library','image',28960,?)`).run(randomUUID(), server, host.id, new Date().toISOString());
|
||||
database.prepare(`INSERT INTO ec_host_operations(id,host_id,instance_id,generation,kind,payload,state,created_at,updated_at)
|
||||
SELECT 'launch',host_id,id,generation,'instance.create','{}','completed',created_at,created_at FROM native_instances WHERE server=?`).run(server);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO servers(name,game,map,game_mode,map_slug,created_by)
|
||||
VALUES(?,'t5','zombie_theater','zombie','theater','ui')`
|
||||
)
|
||||
.run(server);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO native_instances(id,server,host_id,library_id,image,port,created_at)
|
||||
VALUES(?,?,?,'library','image',28960,?)`
|
||||
)
|
||||
.run(randomUUID(), server, host.id, new Date().toISOString());
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO ec_host_operations(id,host_id,instance_id,generation,kind,payload,state,created_at,updated_at)
|
||||
SELECT 'launch',host_id,id,generation,'instance.create','{}','completed',created_at,created_at FROM native_instances WHERE server=?`
|
||||
)
|
||||
.run(server);
|
||||
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const { recordScript } = await import('../../src/lib/server/fleet/inventory.js');
|
||||
const contents = new Map<string, Buffer>();
|
||||
function promoted(kind: 'script' | 'plugin' | 'baseline', name: string, content: Buffer, targetRelPath?: string) {
|
||||
const artifact = createScript({ name, kind, targetRelPath, scopeType: 'game', scopeValue: 't5', content });
|
||||
database.prepare('UPDATE scripts SET current_version_id=? WHERE id=?').run(artifact.version.id, artifact.script.id);
|
||||
function promoted(
|
||||
kind: 'script' | 'plugin' | 'baseline',
|
||||
name: string,
|
||||
content: Buffer,
|
||||
targetRelPath?: string
|
||||
) {
|
||||
const artifact = createScript({
|
||||
name,
|
||||
kind,
|
||||
targetRelPath,
|
||||
scopeType: 'game',
|
||||
scopeValue: 't5',
|
||||
content
|
||||
});
|
||||
database
|
||||
.prepare('UPDATE scripts SET current_version_id=? WHERE id=?')
|
||||
.run(artifact.version.id, artifact.script.id);
|
||||
return artifact;
|
||||
}
|
||||
const script = promoted('script', 'Managed script', Buffer.from('main() { wait 1; }'), 'managed.gsc');
|
||||
const plugin = promoted('plugin', 'Managed plugin', Buffer.from('promoted plugin'), 'managed.dll');
|
||||
const script = promoted(
|
||||
'script',
|
||||
'Managed script',
|
||||
Buffer.from('main() { wait 1; }'),
|
||||
'managed.gsc'
|
||||
);
|
||||
const plugin = promoted(
|
||||
'plugin',
|
||||
'Managed plugin',
|
||||
Buffer.from('promoted plugin'),
|
||||
'managed.dll'
|
||||
);
|
||||
const baseline = promoted('baseline', 'Baseline', Buffer.from('set sv_voice "1"\n'));
|
||||
for (const [root, artifact] of [[scriptRoot, script], [pluginRoot, plugin]] as const) {
|
||||
for (const [root, artifact] of [
|
||||
[scriptRoot, script],
|
||||
[pluginRoot, plugin]
|
||||
] as const) {
|
||||
const { version } = artifact;
|
||||
contents.set(`${root}/${artifact.script.target_rel_path}`, (await import('../../src/lib/server/fleet/scripts.js')).getVersionContent(version.id));
|
||||
recordScript(server, artifact.script.target_rel_path, version.id, version.sha256, version.size, 'test');
|
||||
contents.set(
|
||||
`${root}/${artifact.script.target_rel_path}`,
|
||||
(await import('../../src/lib/server/fleet/scripts.js')).getVersionContent(version.id)
|
||||
);
|
||||
recordScript(
|
||||
server,
|
||||
artifact.script.target_rel_path,
|
||||
version.id,
|
||||
version.sha256,
|
||||
version.size,
|
||||
'test'
|
||||
);
|
||||
}
|
||||
const { getPromotedServerConfig, getServerMapConfig, MAP_CONFIG_FILE } = await import('../../src/lib/server/fleet/server-config.js');
|
||||
const { getPromotedServerConfig, getServerMapConfig, MAP_CONFIG_FILE } =
|
||||
await import('../../src/lib/server/fleet/server-config.js');
|
||||
const config = getPromotedServerConfig('t5', 'zombie_theater', 'theater')!;
|
||||
contents.set('configs/dedicated_sp.cfg', config);
|
||||
recordScript(server, 'dedicated_sp.cfg', null, hash(config), config.length, 'test');
|
||||
contents.set(`configs/${MAP_CONFIG_FILE}`, getServerMapConfig('t5', 'zombie_theater', 'theater')!);
|
||||
contents.set(
|
||||
`configs/${MAP_CONFIG_FILE}`,
|
||||
getServerMapConfig('t5', 'zombie_theater', 'theater')!
|
||||
);
|
||||
// The runtime owns this generated file; neither scanner should classify it as drift.
|
||||
contents.set('configs/dedicated.cfg', Buffer.from('runtime generated'));
|
||||
|
||||
const backend = await import('../../src/lib/server/backends/native.js');
|
||||
const request = vi.spyOn(backend, 'nativeRequest').mockImplementation(async (_instance, kind, payload = {}) => {
|
||||
if (kind === 'files.manifest') {
|
||||
const roots = payload.roots as string[];
|
||||
const ignored = payload.ignoreNames as string[];
|
||||
return { files: [...contents].filter(([path]) => roots.some(root => path.startsWith(`${root}/`))
|
||||
&& !path.split('/').some(part => ignored.includes(part)))
|
||||
.map(([path, content]) => ({ path, size: content.length, sha256: hash(content) })) } as never;
|
||||
}
|
||||
if (capable) throw new Error(`Manifest scan requested an extra remote operation: ${kind}`);
|
||||
const path = String(payload.path);
|
||||
const content = contents.get(path);
|
||||
if (kind === 'files.read') return { contentBase64: content?.toString('base64') ?? null } as never;
|
||||
if (kind === 'files.stat') {
|
||||
const isDirectory = path === '.' || [...contents.keys()].some(file => file.startsWith(`${path}/`));
|
||||
return { exists: content !== undefined || isDirectory, size: content?.length ?? 0,
|
||||
mtime: new Date().toISOString(), isDirectory, isFile: content !== undefined } as never;
|
||||
}
|
||||
if (kind === 'files.list') {
|
||||
const entries = new Map<string, { name: string; isDirectory: boolean; isFile: boolean }>();
|
||||
for (const file of contents.keys()) {
|
||||
if (!file.startsWith(`${path}/`)) continue;
|
||||
const [name, ...nested] = file.slice(path.length + 1).split('/');
|
||||
entries.set(name, { name, isDirectory: nested.length > 0, isFile: nested.length === 0 });
|
||||
const request = vi
|
||||
.spyOn(backend, 'nativeRequest')
|
||||
.mockImplementation(async (_instance, kind, payload = {}) => {
|
||||
if (kind === 'files.manifest') {
|
||||
const roots = payload.roots as string[];
|
||||
const ignored = payload.ignoreNames as string[];
|
||||
return {
|
||||
files: [...contents]
|
||||
.filter(
|
||||
([path]) =>
|
||||
roots.some((root) => path.startsWith(`${root}/`)) &&
|
||||
!path.split('/').some((part) => ignored.includes(part))
|
||||
)
|
||||
.map(([path, content]) => ({
|
||||
path,
|
||||
size: content.length,
|
||||
sha256: hash(content)
|
||||
}))
|
||||
} as never;
|
||||
}
|
||||
return { entries: [...entries.values()] } as never;
|
||||
}
|
||||
throw new Error(`Unexpected remote operation: ${kind}`);
|
||||
});
|
||||
if (capable)
|
||||
throw new Error(`Manifest scan requested an extra remote operation: ${kind}`);
|
||||
const path = String(payload.path);
|
||||
const content = contents.get(path);
|
||||
if (kind === 'files.read')
|
||||
return { contentBase64: content?.toString('base64') ?? null } as never;
|
||||
if (kind === 'files.stat') {
|
||||
const isDirectory =
|
||||
path === '.' ||
|
||||
[...contents.keys()].some((file) => file.startsWith(`${path}/`));
|
||||
return {
|
||||
exists: content !== undefined || isDirectory,
|
||||
size: content?.length ?? 0,
|
||||
mtime: new Date().toISOString(),
|
||||
isDirectory,
|
||||
isFile: content !== undefined
|
||||
} as never;
|
||||
}
|
||||
if (kind === 'files.list') {
|
||||
const entries = new Map<
|
||||
string,
|
||||
{ name: string; isDirectory: boolean; isFile: boolean }
|
||||
>();
|
||||
for (const file of contents.keys()) {
|
||||
if (!file.startsWith(`${path}/`)) continue;
|
||||
const [name, ...nested] = file.slice(path.length + 1).split('/');
|
||||
entries.set(name, {
|
||||
name,
|
||||
isDirectory: nested.length > 0,
|
||||
isFile: nested.length === 0
|
||||
});
|
||||
}
|
||||
return { entries: [...entries.values()] } as never;
|
||||
}
|
||||
throw new Error(`Unexpected remote operation: ${kind}`);
|
||||
});
|
||||
const drift = await import('../../src/lib/server/fleet/drift.js');
|
||||
return { ...drift, database, request, contents, script, plugin, baseline, promoted, mapConfigFile: MAP_CONFIG_FILE };
|
||||
return {
|
||||
...drift,
|
||||
database,
|
||||
request,
|
||||
contents,
|
||||
script,
|
||||
plugin,
|
||||
baseline,
|
||||
promoted,
|
||||
mapConfigFile: MAP_CONFIG_FILE
|
||||
};
|
||||
}
|
||||
|
||||
function expectOneManifest(request: Awaited<ReturnType<typeof setup>>['request']) {
|
||||
expect(request).toHaveBeenCalledTimes(1);
|
||||
expect(request.mock.calls[0][1]).toBe('files.manifest');
|
||||
expect(request.mock.calls[0][2]).toEqual({
|
||||
roots: [scriptRoot, 'configs', pluginRoot], ignoreNames: ['dedicated.cfg'],
|
||||
paths: [`${scriptRoot}/managed.gsc`, `${pluginRoot}/managed.dll`, 'configs/dedicated_sp.cfg', 'configs/electric_cherry_map.cfg']
|
||||
roots: [scriptRoot, 'configs', pluginRoot],
|
||||
ignoreNames: ['dedicated.cfg'],
|
||||
paths: [
|
||||
`${scriptRoot}/managed.gsc`,
|
||||
`${pluginRoot}/managed.dll`,
|
||||
'configs/dedicated_sp.cfg',
|
||||
'configs/electric_cherry_map.cfg'
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
describe('native drift manifests', () => {
|
||||
it.each([true, false])('reconciles verified startup password changes without writing server files (manifest: %s)', async capable => {
|
||||
const api = await setup(capable);
|
||||
const { applyRuntimePassword } = await import('../../src/lib/server/fleet/runtime-config.js');
|
||||
const { getServerScript, recordScript } = await import('../../src/lib/server/fleet/inventory.js');
|
||||
const promoted = Buffer.from('set rcon_password "password"\nset sv_maprotation "map zombie_theater"\n');
|
||||
const { uploadNewVersion } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
// Replace the promoted baseline bytes, retaining a legacy inventory hash.
|
||||
const next = uploadNewVersion(api.baseline.script.id, promoted);
|
||||
api.database.prepare('UPDATE scripts SET current_version_id=? WHERE id=?').run(next.id, api.baseline.script.id);
|
||||
const password = 'a'.repeat(64);
|
||||
api.database.prepare('UPDATE ec_host_operations SET payload=? WHERE id=?').run(JSON.stringify({ rconPassword: password }), 'launch');
|
||||
recordScript(server, 'dedicated_sp.cfg', null, hash(promoted), promoted.length, 'native-create');
|
||||
const effective = Buffer.from(applyRuntimePassword(promoted.toString(), password));
|
||||
expect(effective.length - promoted.length).toBe(56);
|
||||
api.contents.set('configs/dedicated_sp.cfg', effective);
|
||||
it.each([true, false])(
|
||||
'reconciles verified startup password changes without writing server files (manifest: %s)',
|
||||
async (capable) => {
|
||||
const api = await setup(capable);
|
||||
const { applyRuntimePassword } =
|
||||
await import('../../src/lib/server/fleet/runtime-config.js');
|
||||
const { getServerScript, recordScript } =
|
||||
await import('../../src/lib/server/fleet/inventory.js');
|
||||
const promoted = Buffer.from(
|
||||
'set rcon_password "password"\nset sv_maprotation "map zombie_theater"\n'
|
||||
);
|
||||
const { uploadNewVersion } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
// Replace the promoted baseline bytes, retaining a legacy inventory hash.
|
||||
const next = uploadNewVersion(api.baseline.script.id, promoted);
|
||||
api.database
|
||||
.prepare('UPDATE scripts SET current_version_id=? WHERE id=?')
|
||||
.run(next.id, api.baseline.script.id);
|
||||
const password = 'a'.repeat(64);
|
||||
api.database
|
||||
.prepare('UPDATE ec_host_operations SET payload=? WHERE id=?')
|
||||
.run(JSON.stringify({ rconPassword: password }), 'launch');
|
||||
recordScript(
|
||||
server,
|
||||
'dedicated_sp.cfg',
|
||||
null,
|
||||
hash(promoted),
|
||||
promoted.length,
|
||||
'native-create'
|
||||
);
|
||||
const effective = Buffer.from(applyRuntimePassword(promoted.toString(), password));
|
||||
expect(effective.length - promoted.length).toBe(56);
|
||||
api.contents.set('configs/dedicated_sp.cfg', effective);
|
||||
|
||||
expect(await api.scanServerDrift(server)).toEqual([]);
|
||||
expect(getServerScript(server, 'dedicated_sp.cfg')).toMatchObject({ sha256: hash(effective), size: effective.length,
|
||||
source: 'runtime-config-reconciliation' });
|
||||
if (capable) expectOneManifest(api.request);
|
||||
expect((await api.prepareDriftCampaign(server)).plan.files).toEqual([]);
|
||||
expect(await api.scanServerDrift(server)).toEqual([]);
|
||||
expect(getServerScript(server, 'dedicated_sp.cfg')).toMatchObject({
|
||||
sha256: hash(effective),
|
||||
size: effective.length,
|
||||
source: 'runtime-config-reconciliation'
|
||||
});
|
||||
if (capable) expectOneManifest(api.request);
|
||||
expect((await api.prepareDriftCampaign(server)).plan.files).toEqual([]);
|
||||
|
||||
// A wrong password of the same length is still real drift.
|
||||
api.contents.set('configs/dedicated_sp.cfg', Buffer.from(applyRuntimePassword(promoted.toString(), 'b'.repeat(64))));
|
||||
expect(await api.scanServerDrift(server)).toEqual([expect.objectContaining({ relPath: 'dedicated_sp.cfg', driftKind: 'replace' })]);
|
||||
if (!capable) {
|
||||
const repair = await api.prepareDriftCampaign(server);
|
||||
expect(repair.files[0].after).toEqual(effective);
|
||||
// A wrong password of the same length is still real drift.
|
||||
api.contents.set(
|
||||
'configs/dedicated_sp.cfg',
|
||||
Buffer.from(applyRuntimePassword(promoted.toString(), 'b'.repeat(64)))
|
||||
);
|
||||
expect(await api.scanServerDrift(server)).toEqual([
|
||||
expect.objectContaining({ relPath: 'dedicated_sp.cfg', driftKind: 'replace' })
|
||||
]);
|
||||
if (!capable) {
|
||||
const repair = await api.prepareDriftCampaign(server);
|
||||
expect(repair.files[0].after).toEqual(effective);
|
||||
}
|
||||
api.contents.set(
|
||||
'configs/dedicated_sp.cfg',
|
||||
Buffer.concat([effective, Buffer.from('set sv_voice "0"\n')])
|
||||
);
|
||||
expect(await api.scanServerDrift(server)).toHaveLength(1);
|
||||
}
|
||||
api.contents.set('configs/dedicated_sp.cfg', Buffer.concat([effective, Buffer.from('set sv_voice "0"\n')]));
|
||||
expect(await api.scanServerDrift(server)).toHaveLength(1);
|
||||
});
|
||||
);
|
||||
|
||||
it('reports unavailable credentials as a scan failure rather than scheduling a destructive repair', async () => {
|
||||
const api = await setup();
|
||||
api.database.prepare("UPDATE ec_host_operations SET history_expired_at='expired',payload='{}' WHERE id='launch'").run();
|
||||
await expect(api.scanServerDrift(server)).rejects.toThrow('launch credentials are unavailable');
|
||||
api.database
|
||||
.prepare(
|
||||
"UPDATE ec_host_operations SET history_expired_at='expired',payload='{}' WHERE id='launch'"
|
||||
)
|
||||
.run();
|
||||
await expect(api.scanServerDrift(server)).rejects.toThrow(
|
||||
'launch credentials are unavailable'
|
||||
);
|
||||
});
|
||||
|
||||
it('checks managed files, composed config, generated config, and untracked trees in one request', async () => {
|
||||
@@ -141,7 +277,10 @@ describe('native drift manifests', () => {
|
||||
expectOneManifest(api.request);
|
||||
|
||||
api.request.mockClear();
|
||||
expect(await api.prepareDriftCampaign(server)).toMatchObject({ plan: { files: [] }, files: [] });
|
||||
expect(await api.prepareDriftCampaign(server)).toMatchObject({
|
||||
plan: { files: [] },
|
||||
files: []
|
||||
});
|
||||
expectOneManifest(api.request);
|
||||
});
|
||||
|
||||
@@ -155,23 +294,49 @@ describe('native drift manifests', () => {
|
||||
|
||||
const changes = await api.scanServerDrift(server);
|
||||
expect(changes).toHaveLength(4);
|
||||
expect(changes).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: 'script', relPath: 'managed.gsc', driftKind: 'replace', sha256: api.script.version.sha256 }),
|
||||
expect.objectContaining({ kind: 'plugin', relPath: 'managed.dll', driftKind: 'missing', sha256: api.plugin.version.sha256 }),
|
||||
expect.objectContaining({ kind: 'config', relPath: 'dedicated_sp.cfg', driftKind: 'replace' }),
|
||||
expect.objectContaining({ kind: 'script', relPath: 'custom/stray.gsc', driftKind: 'untracked', size: stray.length, sha256: hash(stray) })
|
||||
]));
|
||||
expect(changes).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: 'script',
|
||||
relPath: 'managed.gsc',
|
||||
driftKind: 'replace',
|
||||
sha256: api.script.version.sha256
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: 'plugin',
|
||||
relPath: 'managed.dll',
|
||||
driftKind: 'missing',
|
||||
sha256: api.plugin.version.sha256
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: 'config',
|
||||
relPath: 'dedicated_sp.cfg',
|
||||
driftKind: 'replace'
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: 'script',
|
||||
relPath: 'custom/stray.gsc',
|
||||
driftKind: 'untracked',
|
||||
size: stray.length,
|
||||
sha256: hash(stray)
|
||||
})
|
||||
])
|
||||
);
|
||||
expectOneManifest(api.request);
|
||||
});
|
||||
|
||||
it('still repairs missing inventory and generated map config', async () => {
|
||||
const api = await setup();
|
||||
api.database.prepare('DELETE FROM server_scripts WHERE server=? AND rel_path=?').run(server, 'managed.gsc');
|
||||
api.database
|
||||
.prepare('DELETE FROM server_scripts WHERE server=? AND rel_path=?')
|
||||
.run(server, 'managed.gsc');
|
||||
api.contents.delete(`configs/${api.mapConfigFile}`);
|
||||
expect(await api.scanServerDrift(server)).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ relPath: 'managed.gsc', driftKind: 'replace' }),
|
||||
expect.objectContaining({ relPath: api.mapConfigFile, driftKind: 'missing' })
|
||||
]));
|
||||
expect(await api.scanServerDrift(server)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ relPath: 'managed.gsc', driftKind: 'replace' }),
|
||||
expect.objectContaining({ relPath: api.mapConfigFile, driftKind: 'missing' })
|
||||
])
|
||||
);
|
||||
expectOneManifest(api.request);
|
||||
});
|
||||
|
||||
@@ -180,20 +345,37 @@ describe('native drift manifests', () => {
|
||||
api.promoted('plugin', 'Managed ignored name', Buffer.from('expected'), 'dedicated.cfg');
|
||||
api.contents.set(`${pluginRoot}/dedicated.cfg`, Buffer.from('changed'));
|
||||
expect(await api.scanServerDrift(server)).toEqual([
|
||||
expect.objectContaining({ kind: 'plugin', relPath: 'dedicated.cfg', driftKind: 'replace' })
|
||||
expect.objectContaining({
|
||||
kind: 'plugin',
|
||||
relPath: 'dedicated.cfg',
|
||||
driftKind: 'replace'
|
||||
})
|
||||
]);
|
||||
expect(api.request).toHaveBeenCalledTimes(1);
|
||||
expect(api.request.mock.calls[0][2]).toMatchObject({ ignoreNames: [], paths: expect.arrayContaining([`${pluginRoot}/dedicated.cfg`]) });
|
||||
expect(api.request.mock.calls[0][2]).toMatchObject({
|
||||
ignoreNames: [],
|
||||
paths: expect.arrayContaining([`${pluginRoot}/dedicated.cfg`])
|
||||
});
|
||||
});
|
||||
|
||||
it.each(['running', 'ready-promote'])('skips remote reads while a %s beta owns the server', async state => {
|
||||
const api = await setup();
|
||||
const beta = api.database.prepare("INSERT INTO betas(script_id,script_version_id,trigger_kind,state) VALUES(?,?,'manual',?)")
|
||||
.run(api.script.script.id, api.script.version.id, state).lastInsertRowid;
|
||||
api.database.prepare("INSERT INTO beta_servers(beta_id,server,had_original,stage) VALUES(?,?,0,'awaiting-empty')").run(beta, server);
|
||||
expect(await api.scanServerDrift(server)).toEqual([]);
|
||||
expect(api.request).not.toHaveBeenCalled();
|
||||
});
|
||||
it.each(['running', 'ready-promote'])(
|
||||
'skips remote reads while a %s beta owns the server',
|
||||
async (state) => {
|
||||
const api = await setup();
|
||||
const beta = api.database
|
||||
.prepare(
|
||||
"INSERT INTO betas(script_id,script_version_id,trigger_kind,state) VALUES(?,?,'manual',?)"
|
||||
)
|
||||
.run(api.script.script.id, api.script.version.id, state).lastInsertRowid;
|
||||
api.database
|
||||
.prepare(
|
||||
"INSERT INTO beta_servers(beta_id,server,had_original,stage) VALUES(?,?,0,'awaiting-empty')"
|
||||
)
|
||||
.run(beta, server);
|
||||
expect(await api.scanServerDrift(server)).toEqual([]);
|
||||
expect(api.request).not.toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
|
||||
it('preserves unavailable evidence as a failed scan without falling back to per-file reads', async () => {
|
||||
const api = await setup();
|
||||
@@ -213,12 +395,16 @@ describe('native drift manifests', () => {
|
||||
const api = await setup(false);
|
||||
api.contents.set(`${scriptRoot}/managed.gsc`, Buffer.from('old script'));
|
||||
api.contents.set('configs/stray.cfg', Buffer.from('untracked config'));
|
||||
expect(await api.scanServerDrift(server)).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ relPath: 'managed.gsc', driftKind: 'replace' }),
|
||||
expect.objectContaining({ relPath: 'stray.cfg', driftKind: 'untracked' })
|
||||
]));
|
||||
expect(await api.scanServerDrift(server)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ relPath: 'managed.gsc', driftKind: 'replace' }),
|
||||
expect.objectContaining({ relPath: 'stray.cfg', driftKind: 'untracked' })
|
||||
])
|
||||
);
|
||||
const operations = api.request.mock.calls.map(([, kind]) => kind);
|
||||
expect(operations).not.toContain('files.manifest');
|
||||
expect(operations).toEqual(expect.arrayContaining(['files.stat', 'files.list', 'files.read']));
|
||||
expect(operations).toEqual(
|
||||
expect.arrayContaining(['files.stat', 'files.list', 'files.read'])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user