Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -2,22 +2,36 @@ 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(); vi.restoreAllMocks(); });
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
fixture = await newFixture();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await fixture.cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('provisioning detail projection', () => {
|
||||
it('records identical structured and legacy stage text once while preserving changed observations', async () => {
|
||||
const { beginDeployment, deploymentStage, getDeployment } = await import('../../src/lib/server/provisioning/history.js');
|
||||
const { beginDeployment, deploymentStage, getDeployment } =
|
||||
await import('../../src/lib/server/provisioning/history.js');
|
||||
const id = beginDeployment('server', 'restart');
|
||||
const update = { phase: 'eligibility' as const,
|
||||
blocker: { code: 'players', message: 'Waiting for 3 players', automatic: true } };
|
||||
const update = {
|
||||
phase: 'eligibility' as const,
|
||||
blocker: { code: 'players', message: 'Waiting for 3 players', automatic: true }
|
||||
};
|
||||
deploymentStage(id, update.blocker.message, update);
|
||||
deploymentStage(id, update.blocker.message, update);
|
||||
expect(getDeployment(id)!.events.filter(event => event.message === update.blocker.message)).toHaveLength(1);
|
||||
deploymentStage(id, 'Waiting for 2 players', { ...update, blocker: { ...update.blocker, message: 'Waiting for 2 players' } });
|
||||
expect(
|
||||
getDeployment(id)!.events.filter((event) => event.message === update.blocker.message)
|
||||
).toHaveLength(1);
|
||||
deploymentStage(id, 'Waiting for 2 players', {
|
||||
...update,
|
||||
blocker: { ...update.blocker, message: 'Waiting for 2 players' }
|
||||
});
|
||||
const events = getDeployment(id)!.events;
|
||||
expect(events.filter(event => event.message === 'Waiting for 2 players')).toHaveLength(1);
|
||||
expect(events.filter(event => event.kind === 'blocker')).toHaveLength(1);
|
||||
expect(events.filter((event) => event.message === 'Waiting for 2 players')).toHaveLength(1);
|
||||
expect(events.filter((event) => event.kind === 'blocker')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('keeps legacy full activity while the page and opt-in API return a bounded snapshot without probing', async () => {
|
||||
@@ -27,21 +41,32 @@ describe('provisioning detail projection', () => {
|
||||
const inspect = vi.spyOn(backend, 'nativeHealth');
|
||||
const status = vi.spyOn(rcon, 'rconStatusResult');
|
||||
const id = history.beginDeployment('t5_zombie_theater_1', 'create');
|
||||
for (let index = 0; index < 40; index++) history.recordDeploymentEvent(id, `Recorded activity ${index}`);
|
||||
for (let index = 0; index < 40; index++)
|
||||
history.recordDeploymentEvent(id, `Recorded activity ${index}`);
|
||||
history.finishDeployment(id, { ok: false, message: 'A recorded startup failure' });
|
||||
for (let index = 0; index < 8; index++) history.recordDeploymentEvent(id, `Later observation ${index}`);
|
||||
for (let index = 0; index < 8; index++)
|
||||
history.recordDeploymentEvent(id, `Later observation ${index}`);
|
||||
|
||||
const full = history.getDeployment(id)!;
|
||||
const recent = history.getDeployment(id, { events: 'recent' })!;
|
||||
expect(full.events).toHaveLength(50);
|
||||
expect(recent.events).toEqual(full.events.slice(-5));
|
||||
expect(recent.activity).toMatchObject({ total: 50, latestId: full.events.at(-1)!.id,
|
||||
failureExcerpt: { message: 'Failed: A recorded startup failure', kind: 'outcome' } });
|
||||
expect(recent.activity).toMatchObject({
|
||||
total: 50,
|
||||
latestId: full.events.at(-1)!.id,
|
||||
failureExcerpt: { message: 'Failed: A recorded startup failure', kind: 'outcome' }
|
||||
});
|
||||
expect(recent.progress?.phase).toBe('requested');
|
||||
expect(recent.progress?.checkpoints.map(point => point.key)).toEqual(['requested']);
|
||||
expect(recent.progress?.checkpoints.map((point) => point.key)).toEqual(['requested']);
|
||||
const { GET } = await import('../../src/routes/api/provisioning/[id]/+server.js');
|
||||
expect((await (await GET({ params: { id } })).json()).events).toHaveLength(50);
|
||||
expect((await (await GET({ params: { id }, url: new URL('http://localhost/?events=recent') })).json()).events).toHaveLength(5);
|
||||
expect(
|
||||
(
|
||||
await (
|
||||
await GET({ params: { id }, url: new URL('http://localhost/?events=recent') })
|
||||
).json()
|
||||
).events
|
||||
).toHaveLength(5);
|
||||
const { load } = await import('../../src/routes/provisioning/[id]/+page.server.js');
|
||||
expect(load({ params: { id } })?.detail.events).toHaveLength(5);
|
||||
expect(inspect).not.toHaveBeenCalled();
|
||||
@@ -51,61 +76,129 @@ describe('provisioning detail projection', () => {
|
||||
it('retains missing sources, normalizes malformed warnings, and leaves legacy evidence unrecorded', async () => {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { getDeployment } = await import('../../src/lib/server/provisioning/history.js');
|
||||
db().prepare(`INSERT INTO server_deployments (id,server,action,state,stage,started_at,triggered_by,warnings)
|
||||
VALUES ('legacy','old-server','restart','done','Old custom stage','2026-01-01','ui',?)`).run('["Check files",7,null,{"bad":true}]');
|
||||
db().prepare("INSERT INTO provisioning_sources VALUES ('legacy','campaign','missing','old-server')").run();
|
||||
db().prepare("INSERT INTO provisioning_sources VALUES ('legacy','watchdog','incident','old-server')").run();
|
||||
db().prepare(`INSERT INTO watchdog_incidents (id,server,kind,state,summary,first_seen_at,last_seen_at)
|
||||
VALUES ('incident','old-server','health','resolved','Recovered game service','2026-01-01','2026-01-01')`).run();
|
||||
db()
|
||||
.prepare(
|
||||
`INSERT INTO server_deployments (id,server,action,state,stage,started_at,triggered_by,warnings)
|
||||
VALUES ('legacy','old-server','restart','done','Old custom stage','2026-01-01','ui',?)`
|
||||
)
|
||||
.run('["Check files",7,null,{"bad":true}]');
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO provisioning_sources VALUES ('legacy','campaign','missing','old-server')"
|
||||
)
|
||||
.run();
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO provisioning_sources VALUES ('legacy','watchdog','incident','old-server')"
|
||||
)
|
||||
.run();
|
||||
db()
|
||||
.prepare(
|
||||
`INSERT INTO watchdog_incidents (id,server,kind,state,summary,first_seen_at,last_seen_at)
|
||||
VALUES ('incident','old-server','health','resolved','Recovered game service','2026-01-01','2026-01-01')`
|
||||
)
|
||||
.run();
|
||||
const detail = getDeployment('legacy')!;
|
||||
expect(detail.progress).toBeNull();
|
||||
expect(detail.events).toEqual([]);
|
||||
expect(detail.activity).toEqual({ total: 0, latestId: null, failureExcerpt: null });
|
||||
expect(detail.warningMessages).toEqual(['Check files', 'Some recorded warnings could not be read.']);
|
||||
expect(detail.sources).toEqual([
|
||||
{ kind: 'campaign', source_id: 'missing', server: 'old-server', label: 'Campaign unavailable', href: null },
|
||||
{ kind: 'watchdog', source_id: 'incident', server: 'old-server', label: 'Recovered game service', href: '/maintenance/incident' }
|
||||
expect(detail.warningMessages).toEqual([
|
||||
'Check files',
|
||||
'Some recorded warnings could not be read.'
|
||||
]);
|
||||
expect(detail.sources).toEqual([
|
||||
{
|
||||
kind: 'campaign',
|
||||
source_id: 'missing',
|
||||
server: 'old-server',
|
||||
label: 'Campaign unavailable',
|
||||
href: null
|
||||
},
|
||||
{
|
||||
kind: 'watchdog',
|
||||
source_id: 'incident',
|
||||
server: 'old-server',
|
||||
label: 'Recovered game service',
|
||||
href: '/maintenance/incident'
|
||||
}
|
||||
]);
|
||||
db()
|
||||
.prepare("UPDATE server_deployments SET warnings = 'broken json' WHERE id = 'legacy'")
|
||||
.run();
|
||||
expect(getDeployment('legacy')!.warningMessages).toEqual([
|
||||
'Recorded warnings could not be read.'
|
||||
]);
|
||||
db().prepare("UPDATE server_deployments SET warnings = 'broken json' WHERE id = 'legacy'").run();
|
||||
expect(getDeployment('legacy')!.warningMessages).toEqual(['Recorded warnings could not be read.']);
|
||||
});
|
||||
|
||||
it.each(['create', 'rebuild', 'start', 'restart'] as const)('shows live capacity only for active %s operations, excluding historical work on the same server', async action => {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { beginDeployment, getDeployment, finishDeployment } = await import('../../src/lib/server/provisioning/history.js');
|
||||
db().prepare("INSERT INTO servers (name) VALUES ('server')").run();
|
||||
const old = beginDeployment('server', action);
|
||||
finishDeployment(old, { ok: false, message: 'Timeout' });
|
||||
const current = beginDeployment('server', action);
|
||||
db().prepare("INSERT INTO restart_rollout (server,requested_at,operation_id) VALUES ('server','2026-01-01',?)").run(current);
|
||||
const kinds = action === 'create' ? ['startup'] : action === 'rebuild' ? ['startup', 'restart'] : ['restart'];
|
||||
expect(getDeployment(current)!.capacity.pools).toEqual(kinds.map(kind => expect.objectContaining({
|
||||
kind, used: 1, ownsSlot: true, holders: [{ id: current, server: 'server' }]
|
||||
})));
|
||||
for (const state of ['done', 'warning', 'failed', 'interrupted', 'cancelled']) {
|
||||
db().prepare('UPDATE server_deployments SET state = ? WHERE id = ?').run(state, old);
|
||||
expect(getDeployment(old)!.capacity.pools, state).toEqual([]);
|
||||
it.each(['create', 'rebuild', 'start', 'restart'] as const)(
|
||||
'shows live capacity only for active %s operations, excluding historical work on the same server',
|
||||
async (action) => {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { beginDeployment, getDeployment, finishDeployment } =
|
||||
await import('../../src/lib/server/provisioning/history.js');
|
||||
db().prepare("INSERT INTO servers (name) VALUES ('server')").run();
|
||||
const old = beginDeployment('server', action);
|
||||
finishDeployment(old, { ok: false, message: 'Timeout' });
|
||||
const current = beginDeployment('server', action);
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO restart_rollout (server,requested_at,operation_id) VALUES ('server','2026-01-01',?)"
|
||||
)
|
||||
.run(current);
|
||||
const kinds =
|
||||
action === 'create'
|
||||
? ['startup']
|
||||
: action === 'rebuild'
|
||||
? ['startup', 'restart']
|
||||
: ['restart'];
|
||||
expect(getDeployment(current)!.capacity.pools).toEqual(
|
||||
kinds.map((kind) =>
|
||||
expect.objectContaining({
|
||||
kind,
|
||||
used: 1,
|
||||
ownsSlot: true,
|
||||
holders: [{ id: current, server: 'server' }]
|
||||
})
|
||||
)
|
||||
);
|
||||
for (const state of ['done', 'warning', 'failed', 'interrupted', 'cancelled']) {
|
||||
db()
|
||||
.prepare('UPDATE server_deployments SET state = ? WHERE id = ?')
|
||||
.run(state, old);
|
||||
expect(getDeployment(old)!.capacity.pools, state).toEqual([]);
|
||||
}
|
||||
expect(getDeployment(old)!.progress!.recovery).toBeNull();
|
||||
db().prepare('UPDATE restart_rollout SET healthy_at = ?').run(new Date().toISOString());
|
||||
expect(getDeployment(old)!.capacity.pools).toEqual([]);
|
||||
for (const state of ['queued', 'waiting', 'running', 'awaiting-health']) {
|
||||
db()
|
||||
.prepare('UPDATE server_deployments SET state = ? WHERE id = ?')
|
||||
.run(state, current);
|
||||
expect(
|
||||
getDeployment(current)!.capacity.pools.map((pool) => pool.kind),
|
||||
state
|
||||
).toEqual(kinds);
|
||||
}
|
||||
}
|
||||
expect(getDeployment(old)!.progress!.recovery).toBeNull();
|
||||
db().prepare('UPDATE restart_rollout SET healthy_at = ?').run(new Date().toISOString());
|
||||
expect(getDeployment(old)!.capacity.pools).toEqual([]);
|
||||
for (const state of ['queued', 'waiting', 'running', 'awaiting-health']) {
|
||||
db().prepare('UPDATE server_deployments SET state = ? WHERE id = ?').run(state, current);
|
||||
expect(getDeployment(current)!.capacity.pools.map(pool => pool.kind), state).toEqual(kinds);
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
it('labels submission order independently of startup reservations and retains queued recipe status', async () => {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { beginDeployment, getDeployment } = await import('../../src/lib/server/provisioning/history.js');
|
||||
const { beginDeployment, getDeployment } =
|
||||
await import('../../src/lib/server/provisioning/history.js');
|
||||
const { recordProgress } = await import('../../src/lib/server/provisioning/progress.js');
|
||||
const first = beginDeployment('first', 'create');
|
||||
const next = beginDeployment('next', 'create');
|
||||
db().prepare("UPDATE server_deployments SET state = 'queued' WHERE id = ?").run(next);
|
||||
recordProgress(next, { phase: 'startup-queue', recipeStatus: 'queued', observedAt: null });
|
||||
const detail = getDeployment(next)!;
|
||||
expect(detail.capacity.pools[0]).toMatchObject({ kind: 'startup', used: 1, requestOrder: 1, ownsSlot: false,
|
||||
holders: [{ id: first, server: 'first' }] });
|
||||
expect(detail.capacity.pools[0]).toMatchObject({
|
||||
kind: 'startup',
|
||||
used: 1,
|
||||
requestOrder: 1,
|
||||
ownsSlot: false,
|
||||
holders: [{ id: first, server: 'first' }]
|
||||
});
|
||||
expect(detail.progress).toMatchObject({ recipeStatus: 'queued', observedAt: null });
|
||||
});
|
||||
|
||||
@@ -114,37 +207,71 @@ describe('provisioning detail projection', () => {
|
||||
const history = await import('../../src/lib/server/provisioning/history.js');
|
||||
db().prepare("INSERT INTO servers (name,game_port) VALUES ('server',28960)").run();
|
||||
const id = history.beginDeployment('server', 'create');
|
||||
db().prepare("INSERT INTO server_creation_jobs VALUES (?, 'server',28960,'zombie','theater','zombie_theater')").run(id);
|
||||
history.deploymentStage(id, 'Waiting for game evidence', { phase: 'verify-game', checks: [
|
||||
{ key: 'game', label: 'Game status', result: 'waiting', source: null }
|
||||
] });
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO server_creation_jobs VALUES (?, 'server',28960,'zombie','theater','zombie_theater')"
|
||||
)
|
||||
.run(id);
|
||||
history.deploymentStage(id, 'Waiting for game evidence', {
|
||||
phase: 'verify-game',
|
||||
checks: [{ key: 'game', label: 'Game status', result: 'waiting', source: null }]
|
||||
});
|
||||
history.finishDeployment(id, { ok: false, message: 'Observation interrupted' });
|
||||
const failed = history.getDeployment(id)!;
|
||||
expect(failed.progress?.phase).toBe('verify-game');
|
||||
expect(failed.capacity.pools).toEqual([]);
|
||||
expect(history.resumeVerification(id)).toBe(true);
|
||||
const resumed = history.getDeployment(id)!;
|
||||
expect(resumed.deployment).toMatchObject({ id, state: 'awaiting-health', finished_at: null, error: null });
|
||||
expect(resumed.progress).toMatchObject({ phase: 'verify-container', checks: [], observedAt: null });
|
||||
expect(resumed.capacity.pools).toEqual([expect.objectContaining({ kind: 'startup', ownsSlot: true })]);
|
||||
const outcomes = resumed.events.filter(event => event.kind === 'outcome');
|
||||
expect(resumed.deployment).toMatchObject({
|
||||
id,
|
||||
state: 'awaiting-health',
|
||||
finished_at: null,
|
||||
error: null
|
||||
});
|
||||
expect(resumed.progress).toMatchObject({
|
||||
phase: 'verify-container',
|
||||
checks: [],
|
||||
observedAt: null
|
||||
});
|
||||
expect(resumed.capacity.pools).toEqual([
|
||||
expect.objectContaining({ kind: 'startup', ownsSlot: true })
|
||||
]);
|
||||
const outcomes = resumed.events.filter((event) => event.kind === 'outcome');
|
||||
expect(outcomes).toHaveLength(1);
|
||||
expect(JSON.parse(outcomes[0].details_json!)).toMatchObject({ state: 'failed', finishedAt: failed.deployment.finished_at,
|
||||
error: 'Observation interrupted' });
|
||||
expect(resumed.events.some(event => event.kind === 'resumed')).toBe(true);
|
||||
expect(JSON.parse(outcomes[0].details_json!)).toMatchObject({
|
||||
state: 'failed',
|
||||
finishedAt: failed.deployment.finished_at,
|
||||
error: 'Observation interrupted'
|
||||
});
|
||||
expect(resumed.events.some((event) => event.kind === 'resumed')).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves a legacy cleanup failure and keeps deleted cleanup capability authoritative', async () => {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { getDeployment, resumeCleanup, REMOVING_SERVER } = await import('../../src/lib/server/provisioning/history.js');
|
||||
db().prepare(`INSERT INTO server_deployments (id,server,server_deleted_at,action,state,stage,started_at,finished_at,triggered_by,error)
|
||||
VALUES ('cleanup','old-server','2026-01-02','destroy','interrupted',?,'2026-01-01','2026-01-02','ui','App restarted')`).run(REMOVING_SERVER);
|
||||
expect(getDeployment('cleanup')).toMatchObject({ canResumeCleanup: true, deployment: { server_exists: 0 } });
|
||||
const { getDeployment, resumeCleanup, REMOVING_SERVER } =
|
||||
await import('../../src/lib/server/provisioning/history.js');
|
||||
db()
|
||||
.prepare(
|
||||
`INSERT INTO server_deployments (id,server,server_deleted_at,action,state,stage,started_at,finished_at,triggered_by,error)
|
||||
VALUES ('cleanup','old-server','2026-01-02','destroy','interrupted',?,'2026-01-01','2026-01-02','ui','App restarted')`
|
||||
)
|
||||
.run(REMOVING_SERVER);
|
||||
expect(getDeployment('cleanup')).toMatchObject({
|
||||
canResumeCleanup: true,
|
||||
deployment: { server_exists: 0 }
|
||||
});
|
||||
expect(resumeCleanup('cleanup')).toBe(true);
|
||||
const result = getDeployment('cleanup')!;
|
||||
expect(result.deployment).toMatchObject({ state: 'waiting', server_deleted_at: '2026-01-02' });
|
||||
const outcome = result.events.find(event => event.kind === 'outcome')!;
|
||||
expect(JSON.parse(outcome.details_json!)).toMatchObject({ state: 'interrupted', finishedAt: '2026-01-02', error: 'App restarted' });
|
||||
expect(result.deployment).toMatchObject({
|
||||
state: 'waiting',
|
||||
server_deleted_at: '2026-01-02'
|
||||
});
|
||||
const outcome = result.events.find((event) => event.kind === 'outcome')!;
|
||||
expect(JSON.parse(outcome.details_json!)).toMatchObject({
|
||||
state: 'interrupted',
|
||||
finishedAt: '2026-01-02',
|
||||
error: 'App restarted'
|
||||
});
|
||||
expect(result.progress?.phase).toBe('cleanup');
|
||||
});
|
||||
});
|
||||
@@ -152,11 +279,15 @@ describe('provisioning detail projection', () => {
|
||||
describe('provisioning activity pagination', () => {
|
||||
it('keeps polling payloads bounded for a retained history with thousands of events', async () => {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { beginDeployment, getDeployment } = await import('../../src/lib/server/provisioning/history.js');
|
||||
const { beginDeployment, getDeployment } =
|
||||
await import('../../src/lib/server/provisioning/history.js');
|
||||
const id = beginDeployment('server', 'restart');
|
||||
const insert = db().prepare('INSERT INTO server_deployment_events (deployment_id,created_at,message) VALUES (?,?,?)');
|
||||
const insert = db().prepare(
|
||||
'INSERT INTO server_deployment_events (deployment_id,created_at,message) VALUES (?,?,?)'
|
||||
);
|
||||
db().transaction(() => {
|
||||
for (let index = 0; index < 5000; index++) insert.run(id, '2026-09-28T12:00:00Z', `Observation ${index} ${'x'.repeat(250)}`);
|
||||
for (let index = 0; index < 5000; index++)
|
||||
insert.run(id, '2026-09-28T12:00:00Z', `Observation ${index} ${'x'.repeat(250)}`);
|
||||
})();
|
||||
const full = getDeployment(id)!;
|
||||
const recent = getDeployment(id, { events: 'recent' })!;
|
||||
@@ -168,47 +299,70 @@ describe('provisioning activity pagination', () => {
|
||||
if (process.env.PROVISIONING_BENCHMARK) {
|
||||
const start = performance.now();
|
||||
for (let index = 0; index < 30; index++) getDeployment(id, { events: 'recent' });
|
||||
console.info({ events: full.events.length, fullBytes, recentBytes, meanRecentReadMs: (performance.now() - start) / 30 });
|
||||
console.info({
|
||||
events: full.events.length,
|
||||
fullBytes,
|
||||
recentBytes,
|
||||
meanRecentReadMs: (performance.now() - start) / 30
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it.each(['newest', 'oldest'] as const)('keeps stable %s pages under concurrent appends and searches literal text', async order => {
|
||||
const { beginDeployment, recordDeploymentEvent } = await import('../../src/lib/server/provisioning/history.js');
|
||||
const { deploymentActivity } = await import('../../src/lib/server/provisioning/activity.js');
|
||||
const id = beginDeployment('server', 'stop');
|
||||
const other = beginDeployment('other', 'stop');
|
||||
for (let index = 0; index < 67; index++) {
|
||||
recordDeploymentEvent(id, index === 30 ? 'literal %_needle' : `Event ${index}`);
|
||||
recordDeploymentEvent(other, 'Unrelated event');
|
||||
it.each(['newest', 'oldest'] as const)(
|
||||
'keeps stable %s pages under concurrent appends and searches literal text',
|
||||
async (order) => {
|
||||
const { beginDeployment, recordDeploymentEvent } =
|
||||
await import('../../src/lib/server/provisioning/history.js');
|
||||
const { deploymentActivity } =
|
||||
await import('../../src/lib/server/provisioning/activity.js');
|
||||
const id = beginDeployment('server', 'stop');
|
||||
const other = beginDeployment('other', 'stop');
|
||||
for (let index = 0; index < 67; index++) {
|
||||
recordDeploymentEvent(id, index === 30 ? 'literal %_needle' : `Event ${index}`);
|
||||
recordDeploymentEvent(other, 'Unrelated event');
|
||||
}
|
||||
let page = deploymentActivity(id, { order });
|
||||
const watermark = page.watermark;
|
||||
const seen = [...page.events];
|
||||
expect(page.events).toHaveLength(25);
|
||||
recordDeploymentEvent(id, 'New event during reading');
|
||||
while (page.nextCursor !== null) {
|
||||
page = deploymentActivity(id, { order, watermark, cursor: page.nextCursor });
|
||||
seen.push(...page.events);
|
||||
}
|
||||
expect(seen).toHaveLength(68);
|
||||
expect(new Set(seen.map((event) => event.id)).size).toBe(68);
|
||||
expect(seen.every((event) => event.id <= watermark)).toBe(true);
|
||||
expect(page.total).toBe(68);
|
||||
expect(page.totalEvents).toBe(69);
|
||||
expect(
|
||||
deploymentActivity(id, { query: '%_NEEDLE' }).events.map((event) => event.message)
|
||||
).toEqual(['literal %_needle']);
|
||||
expect(deploymentActivity(id, { query: 'not found' }).total).toBe(0);
|
||||
}
|
||||
let page = deploymentActivity(id, { order });
|
||||
const watermark = page.watermark;
|
||||
const seen = [...page.events];
|
||||
expect(page.events).toHaveLength(25);
|
||||
recordDeploymentEvent(id, 'New event during reading');
|
||||
while (page.nextCursor !== null) {
|
||||
page = deploymentActivity(id, { order, watermark, cursor: page.nextCursor });
|
||||
seen.push(...page.events);
|
||||
}
|
||||
expect(seen).toHaveLength(68);
|
||||
expect(new Set(seen.map(event => event.id)).size).toBe(68);
|
||||
expect(seen.every(event => event.id <= watermark)).toBe(true);
|
||||
expect(page.total).toBe(68);
|
||||
expect(page.totalEvents).toBe(69);
|
||||
expect(deploymentActivity(id, { query: '%_NEEDLE' }).events.map(event => event.message)).toEqual(['literal %_needle']);
|
||||
expect(deploymentActivity(id, { query: 'not found' }).total).toBe(0);
|
||||
});
|
||||
);
|
||||
|
||||
it('validates cursor/search requests and caps page size without changing default legacy responses', async () => {
|
||||
const { beginDeployment, recordDeploymentEvent } = await import('../../src/lib/server/provisioning/history.js');
|
||||
const { beginDeployment, recordDeploymentEvent } =
|
||||
await import('../../src/lib/server/provisioning/history.js');
|
||||
const { GET } = await import('../../src/routes/api/provisioning/[id]/events/+server.js');
|
||||
const id = beginDeployment('server', 'stop');
|
||||
for (let index = 0; index < 130; index++) recordDeploymentEvent(id, `Event ${index}`);
|
||||
const response = GET({ params: { id }, url: new URL('http://localhost/?limit=999') });
|
||||
expect((await response.json()).events).toHaveLength(100);
|
||||
for (const query of ['cursor=-1', 'watermark=NaN', 'limit=0', 'order=invalid', 'cursor=9007199254740992']) {
|
||||
expect(() => GET({ params: { id }, url: new URL(`http://localhost/?${query}`) })).toThrow();
|
||||
for (const query of [
|
||||
'cursor=-1',
|
||||
'watermark=NaN',
|
||||
'limit=0',
|
||||
'order=invalid',
|
||||
'cursor=9007199254740992'
|
||||
]) {
|
||||
expect(() =>
|
||||
GET({ params: { id }, url: new URL(`http://localhost/?${query}`) })
|
||||
).toThrow();
|
||||
}
|
||||
expect(() => GET({ params: { id: 'missing' }, url: new URL('http://localhost/') })).toThrow();
|
||||
expect(() =>
|
||||
GET({ params: { id: 'missing' }, url: new URL('http://localhost/') })
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user