Rebuild beta and campaign file activations
This commit is contained in:
@@ -39,7 +39,7 @@ const inventory = () => ({
|
||||
memoryTotal: 64 * 1024 ** 3,
|
||||
memoryAvailable: 60 * 1024 ** 3,
|
||||
protocolVersion: 1,
|
||||
capabilities: ['instance.unhealthy-recovery'],
|
||||
capabilities: ['instance.unhealthy-recovery', 'instance.rebuild-files'],
|
||||
dockerRunning: true,
|
||||
directoriesWritable: true,
|
||||
storageAvailable: 100_000_000_000,
|
||||
@@ -1725,6 +1725,96 @@ describe('native instance provisioning', () => {
|
||||
const { getHost } = await import('../../src/lib/server/hosts/index.js');
|
||||
expect(getHost(hostId)?.mode).toBe('active');
|
||||
});
|
||||
it('builds campaign replacements with requested plugins, configuration, additions and removals', async () => {
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
for (const path of ['replace.dll', 'remove.dll']) {
|
||||
const file = createScript({
|
||||
name: path,
|
||||
kind: 'plugin',
|
||||
targetRelPath: path,
|
||||
scopeType: 'game',
|
||||
scopeValue: 't5',
|
||||
content: Buffer.from('promoted plugin')
|
||||
});
|
||||
db()
|
||||
.prepare('UPDATE scripts SET current_version_id=? WHERE id=?')
|
||||
.run(file.version.id, file.script.id);
|
||||
}
|
||||
const job = await queue();
|
||||
await advance();
|
||||
await acknowledge(job.deploymentId);
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO deployments(id,action,started_at) VALUES('files-rebuild','promote',?)"
|
||||
)
|
||||
.run(new Date().toISOString());
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO deployment_servers(deployment_id,server,files_changed,restart_state) VALUES('files-rebuild',?,4,'pending')"
|
||||
)
|
||||
.run(job.name);
|
||||
const { queueFileChange } = await import('../../src/lib/server/fleet/staged-files.js');
|
||||
const changes = [
|
||||
{ kind: 'plugin', relPath: 'replace.dll', content: Buffer.from('requested plugin') },
|
||||
{ kind: 'plugin', relPath: 'remove.dll', content: null },
|
||||
{ kind: 'script', relPath: 'new.gsc', content: Buffer.from('new script') },
|
||||
{ kind: 'config', relPath: 'custom.cfg', content: Buffer.from('set custom 1') }
|
||||
] as const;
|
||||
for (const change of changes)
|
||||
queueFileChange({
|
||||
sourceKind: 'campaign',
|
||||
sourceId: 'files-rebuild',
|
||||
server: job.name,
|
||||
inventorySource: 'campaign',
|
||||
...change
|
||||
});
|
||||
const { requestRestart } = await import('../../src/lib/server/provisioning/requests.js');
|
||||
const rebuild = requestRestart(job.name, { kind: 'campaign', id: 'files-rebuild' });
|
||||
await advance();
|
||||
const { getHostOperation, reportHostOperation } =
|
||||
await import('../../src/lib/server/hosts/index.js');
|
||||
const staged = db()
|
||||
.prepare('SELECT stage_operation_id FROM native_provisioning WHERE deployment_id=?')
|
||||
.get(rebuild.id) as { stage_operation_id: string };
|
||||
reportHostOperation(hostId, staged.stage_operation_id, { state: 'completed' });
|
||||
await advance();
|
||||
const operation = (await operationFor(rebuild.id))!;
|
||||
expect(operation.kind).toBe('instance.rebuild');
|
||||
expect(operation.payload).not.toHaveProperty('unhealthyRecovery');
|
||||
const { recipeForDeployment } =
|
||||
await import('../../src/lib/server/fleet/launch-recipes.js');
|
||||
const recipe = recipeForDeployment(rebuild.id)!.recipe;
|
||||
for (const change of changes) {
|
||||
const actual = recipe.files.find(
|
||||
(file) => file.kind === change.kind && file.path === change.relPath
|
||||
);
|
||||
if (change.content === null) expect(actual).toBeUndefined();
|
||||
else expect(Buffer.from(actual!.content!, 'base64')).toEqual(change.content);
|
||||
}
|
||||
const files = getHostOperation(staged.stage_operation_id)!.payload.files as Array<{
|
||||
id: number;
|
||||
path: string;
|
||||
}>;
|
||||
reportHostOperation(hostId, operation.id, {
|
||||
state: 'completed',
|
||||
result: { before: files.map((file) => ({ ...file, contentBase64: null })) }
|
||||
});
|
||||
remote.state = 'unhealthy';
|
||||
await advance();
|
||||
expect((await operationFor(rebuild.id))!.kind).toBe('instance.restart');
|
||||
await acknowledge(rebuild.id);
|
||||
const recovery = (await operationFor(rebuild.id))!;
|
||||
expect(recovery.kind).toBe('instance.rebuild');
|
||||
expect(recovery.payload).not.toHaveProperty('stageId');
|
||||
expect(recovery.payload.files).toEqual(operation.payload.files);
|
||||
remote.state = 'running';
|
||||
await acknowledge(rebuild.id);
|
||||
expect((await detail(rebuild.id)).state).toBe('done');
|
||||
const { nativeInstance } = await import('../../src/lib/server/backends/native.js');
|
||||
expect(nativeInstance(job.name)?.generation).toBe(3);
|
||||
});
|
||||
|
||||
it('stages campaign bytes while occupied and records agent before images only after safe activation', async () => {
|
||||
const job = await queue();
|
||||
await advance();
|
||||
@@ -1761,6 +1851,19 @@ describe('native instance provisioning', () => {
|
||||
});
|
||||
const { requestRestart } = await import('../../src/lib/server/provisioning/requests.js');
|
||||
const restart = requestRestart(job.name, { kind: 'campaign', id: 'campaign' });
|
||||
expect(restart.action).toBe('rebuild');
|
||||
// Upgrade queued work from older controllers, but wait for a capable host.
|
||||
db().prepare("UPDATE server_deployments SET action='restart' WHERE id=?").run(restart.id);
|
||||
const { heartbeat } = await import('../../src/lib/server/hosts/index.js');
|
||||
heartbeat(hostId, { ...inventory(), capabilities: [] });
|
||||
await advance();
|
||||
expect(await detail(restart.id)).toMatchObject({
|
||||
action: 'rebuild',
|
||||
state: 'waiting',
|
||||
stage: expect.stringContaining('Update the host agent')
|
||||
});
|
||||
expect(await operationFor(restart.id)).toBeNull();
|
||||
heartbeat(hostId, inventory());
|
||||
remote.players = 2;
|
||||
await advance();
|
||||
const staged = db()
|
||||
@@ -1811,9 +1914,18 @@ describe('native instance provisioning', () => {
|
||||
await advance();
|
||||
const activation = (await operationFor(restart.id))!;
|
||||
expect(activation).toMatchObject({
|
||||
kind: 'instance.restart',
|
||||
kind: 'instance.rebuild',
|
||||
generation: 1,
|
||||
payload: { stageId: restart.id }
|
||||
payload: {
|
||||
stageId: restart.id,
|
||||
nextGeneration: 2,
|
||||
files: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: 'runtime/plutonium/storage/t5/raw/scripts/sp/hud.gsc',
|
||||
sha256: createHash('sha256').update(after).digest('hex')
|
||||
})
|
||||
])
|
||||
}
|
||||
});
|
||||
expect(
|
||||
db()
|
||||
@@ -3819,6 +3931,12 @@ describe('cancelled native mutation bookkeeping', () => {
|
||||
])
|
||||
).rejects.toThrow();
|
||||
const activation = (await operationFor(restart.id))!;
|
||||
expect(activation.kind).toBe('instance.rebuild');
|
||||
expect(activation.payload).toMatchObject({
|
||||
stageId: restart.id,
|
||||
nextGeneration: activation.generation! + 1
|
||||
});
|
||||
expect(activation.payload).not.toHaveProperty('unhealthyRecovery');
|
||||
reportHostOperation(hostId, activation.id, {
|
||||
state: 'completed',
|
||||
result: {
|
||||
|
||||
Reference in New Issue
Block a user