Use user timezone for maintenance and official Plutonium manifest
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
||||
import { nextMaintenanceAt, parseChangelogRevision, parsePlutoniumRevision } from '../../src/lib/plutonium.js';
|
||||
import { maintenanceTimeInZone, nextMaintenanceAt, parsePlutoniumManifest, parsePlutoniumRevision, PLUTONIUM_MANIFEST_URL } from '../../src/lib/plutonium.js';
|
||||
|
||||
vi.mock('../../src/lib/server/integrations/komodo.js', () => ({ komodoRestart: vi.fn(), komodoInspectHealthy: vi.fn() }));
|
||||
vi.mock('../../src/lib/server/integrations/rcon.js', () => ({ rconGetPlayerCount: vi.fn(), rconStatusResult: vi.fn(), rconPlutoniumVersion: vi.fn() }));
|
||||
@@ -15,7 +15,7 @@ beforeEach(async () => {
|
||||
vi.stubEnv('KOMODO_ADDRESS', 'http://komodo.test');
|
||||
vi.stubEnv('KOMODO_API_KEY', 'test'); vi.stubEnv('KOMODO_API_SECRET', 'test');
|
||||
vi.stubEnv('RCON_PASSWORD', 'test'); vi.stubEnv('DEPLOYMENT_RESTART_CONCURRENCY', '1');
|
||||
vi.stubGlobal('fetch', vi.fn().mockImplementation(async () => new Response('<h2 id="r5354">r5354</h2><h2>r5300</h2>')));
|
||||
vi.stubGlobal('fetch', vi.fn().mockImplementation(async () => Response.json({ product: 'plutonium-core-prod', revision: 5354, files: [] })));
|
||||
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
|
||||
await seedServerRegistry();
|
||||
const k = await import('../../src/lib/server/integrations/komodo.js');
|
||||
@@ -30,11 +30,20 @@ afterEach(async () => { await fixture.cleanup(); vi.unstubAllEnvs(); vi.unstubAl
|
||||
const api = () => import('../../src/lib/server/fleet/plutonium.js');
|
||||
|
||||
describe('Plutonium versions', () => {
|
||||
it('parses explicit revisions and release headings without interpreting engine numbers or links as releases', () => {
|
||||
it('uses the official production manifest without requesting the challenged changelog page', async () => {
|
||||
const m = await api(); await m.checkPlutonium();
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(fetch).toHaveBeenCalledWith(PLUTONIUM_MANIFEST_URL, expect.any(Object));
|
||||
expect(m.plutoniumStatus().release).toMatchObject({ revision: 5354, error: null });
|
||||
expect(parsePlutoniumManifest({ product: 'plutonium-core-prod', revision: 5354 })).toBe(5354);
|
||||
for (const value of [null, {}, { ref: '1.0.221' }, { product: 'plutonium-core-dev', revision: 6000 },
|
||||
{ product: 'plutonium-core-prod', revision: '5354' }, { product: 'plutonium-core-prod', revision: -1 }]) {
|
||||
expect(() => parsePlutoniumManifest(value)).toThrow();
|
||||
}
|
||||
});
|
||||
it('parses explicit running revisions without interpreting engine numbers as releases', () => {
|
||||
expect(parsePlutoniumRevision('print\nPlutonium T6 r5354 win-x86')).toBe(5354);
|
||||
expect(parsePlutoniumRevision('version 1.0 build 20260921')).toBeNull();
|
||||
expect(parseChangelogRevision('<a href="#r9999">r9999</a><h2><a>r5354</a></h2>')).toBe(5354);
|
||||
expect(() => parseChangelogRevision('<h1>Service unavailable</h1>')).toThrow();
|
||||
});
|
||||
it('preserves the cached release with a visible error and clears failed running-version observations', async () => {
|
||||
const m = await api(); await m.checkPlutonium();
|
||||
@@ -45,6 +54,55 @@ describe('Plutonium versions', () => {
|
||||
expect(m.plutoniumStatus().release).toMatchObject({ revision: 5354, error: 'Offline' });
|
||||
expect(m.plutoniumStatus().servers.every(s => s.revision === null && s.error === 'Timeout')).toBe(true);
|
||||
});
|
||||
it.each([
|
||||
() => new Response('Blocked', { status: 403 }),
|
||||
() => new Response('<html>Browser challenge</html>'),
|
||||
() => Response.json({ product: 'plutonium-core-dev', revision: 9999 })
|
||||
])('keeps the cached revision when the manifest is unavailable or invalid', async response => {
|
||||
const m = await api(); await m.checkPlutonium();
|
||||
vi.mocked(fetch).mockImplementation(async () => response());
|
||||
await m.checkPlutonium();
|
||||
expect(m.plutoniumStatus().release.revision).toBe(5354);
|
||||
expect(m.plutoniumStatus().release.error).toBeTruthy();
|
||||
m.queueMaintenance([servers[0]]);
|
||||
expect(m.plutoniumStatus().jobs[0].target_revision).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('maintenance timezones', () => {
|
||||
it.each([
|
||||
['06:00', '2026-09-21T05:59:00Z', 'America/New_York', '2026-09-21T10:00:00.000Z'],
|
||||
['06:00', '2026-12-21T05:59:00Z', 'America/New_York', '2026-12-21T11:00:00.000Z'],
|
||||
['06:00', '2026-09-21T00:00:00Z', 'Asia/Kathmandu', '2026-09-21T00:15:00.000Z'],
|
||||
['06:00', '2026-09-21T00:15:01Z', 'Asia/Kathmandu', '2026-09-22T00:15:00.000Z'],
|
||||
['02:30', '2026-03-08T05:00:00Z', 'America/New_York', '2026-03-08T07:00:00.000Z'],
|
||||
['01:30', '2026-11-01T04:00:00Z', 'America/New_York', '2026-11-01T05:30:00.000Z'],
|
||||
['01:30', '2026-11-01T05:31:00Z', 'America/New_York', '2026-11-02T06:30:00.000Z'],
|
||||
['02:15', '2026-10-03T13:00:00Z', 'Australia/Lord_Howe', '2026-10-03T15:30:00.000Z']
|
||||
])('schedules %s after %s in %s', (time, now, zone, expected) => {
|
||||
expect(nextMaintenanceAt(time, new Date(now), zone)).toBe(expected);
|
||||
});
|
||||
it('preserves legacy UTC schedules and displays their next occurrence in the user timezone', async () => {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
db().prepare('INSERT INTO settings (key, value) VALUES (?, ?)').run('plutonium_schedule', JSON.stringify({
|
||||
enabled: true, time: '06:00', nextAt: '2026-09-21T06:00:00.000Z'
|
||||
}));
|
||||
const m = await api();
|
||||
const schedule = m.plutoniumStatus().schedule;
|
||||
expect(schedule.timeZone).toBe('UTC');
|
||||
expect(maintenanceTimeInZone(schedule, 'America/New_York')).toBe('02:00');
|
||||
expect(m.plutoniumStatus().schedule.nextAt).toBe('2026-09-21T06:00:00.000Z');
|
||||
});
|
||||
it('persists the zone and advances in local calendar days across DST', async () => {
|
||||
vi.setSystemTime(new Date('2026-03-07T10:59:00Z'));
|
||||
const m = await api(); await m.checkPlutonium();
|
||||
m.saveMaintenanceSchedule(true, '06:00', 'America/New_York');
|
||||
const { closeDb } = await import('../../src/lib/server/db/index.js'); closeDb();
|
||||
expect(m.plutoniumStatus().schedule.nextAt).toBe('2026-03-07T11:00:00.000Z');
|
||||
vi.setSystemTime(new Date('2026-03-07T11:00:00Z'));
|
||||
await m.maintenanceTick();
|
||||
expect(m.plutoniumStatus().schedule).toMatchObject({ timeZone: 'America/New_York', nextAt: '2026-03-08T10:00:00.000Z' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('persistent maintenance', () => {
|
||||
@@ -130,22 +188,25 @@ describe('persistent maintenance', () => {
|
||||
expect((await post({ action: 'schedule', enabled: true, time: '26:99' })).status).toBe(400);
|
||||
expect((await post({ action: 'restart', servers: ['missing'] })).status).toBe(400);
|
||||
expect((await post({ action: 'cancel', id: 100 })).status).toBe(409);
|
||||
expect((await post({ action: 'schedule', enabled: true, time: '06:00' })).status).toBe(200);
|
||||
expect((await post({ action: 'schedule', enabled: true, time: '06:00' })).status).toBe(400);
|
||||
expect((await post({ action: 'schedule', enabled: true, time: '06:00', timeZone: 'invalid' })).status).toBe(400);
|
||||
expect((await post({ action: 'schedule', enabled: true, time: '06:00', timeZone: 'America/New_York' })).status).toBe(200);
|
||||
const m = await api();
|
||||
expect(m.plutoniumStatus().jobs).toEqual([]);
|
||||
expect(m.plutoniumStatus().schedule.enabled).toBe(true);
|
||||
expect(m.plutoniumStatus().schedule).toMatchObject({ timeZone: 'America/New_York', nextAt: '2026-09-21T10:00:00.000Z' });
|
||||
});
|
||||
it('runs a daily UTC schedule once and coalesces missed days', async () => {
|
||||
expect(nextMaintenanceAt('06:00', new Date('2026-09-21T06:00:00Z'))).toBe('2026-09-22T06:00:00.000Z');
|
||||
expect(() => nextMaintenanceAt('24:00')).toThrow();
|
||||
const m = await api(); await m.checkPlutonium();
|
||||
expect(m.plutoniumStatus().schedule.enabled).toBe(false);
|
||||
m.saveMaintenanceSchedule(true, '06:00');
|
||||
m.saveMaintenanceSchedule(true, '06:00', 'UTC');
|
||||
vi.setSystemTime(new Date('2026-09-24T07:00:00Z'));
|
||||
await m.maintenanceTick(); await m.maintenanceTick();
|
||||
expect(m.plutoniumStatus().jobs).toHaveLength(2);
|
||||
expect(m.plutoniumStatus().schedule.nextAt).toBe('2026-09-25T06:00:00.000Z');
|
||||
m.saveMaintenanceSchedule(false, '06:00');
|
||||
m.saveMaintenanceSchedule(false, '06:00', 'UTC');
|
||||
expect(m.plutoniumStatus().schedule.nextAt).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user