Simplify campaign list and add searchable status filters
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
||||
import { campaignOutcome, type CampaignSummary } from '../../src/lib/ui/campaigns.js';
|
||||
|
||||
let fixture: Fixture;
|
||||
beforeEach(async () => { vi.resetModules(); fixture = await newFixture([]); });
|
||||
afterEach(async () => { await fixture.cleanup(); });
|
||||
|
||||
async function setup() {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const handle = db();
|
||||
const { GET } = await import('../../src/routes/api/campaigns/+server.js');
|
||||
const get = async (query = ''): Promise<{ deployments: (CampaignSummary & { id: string })[]; total: number }> =>
|
||||
(await GET({ url: new URL(`http://localhost/api/campaigns?${query}`) })).json();
|
||||
const add = (id: string, states: string[], action = 'promote', deleted = false) => {
|
||||
handle.prepare('INSERT INTO deployments(id, action, started_at) VALUES(?,?,?)').run(id, action, '2026-10-06T12:00:00Z');
|
||||
for (const [index, state] of states.entries()) {
|
||||
handle.prepare('INSERT INTO deployment_servers(deployment_id, server, files_changed, restart_state, server_deleted_at) VALUES(?,?,1,?,?)')
|
||||
.run(id, `t5_zombie_theater_${index + 1}`, state, deleted ? '2026-10-06T13:00:00Z' : null);
|
||||
}
|
||||
};
|
||||
return { handle, get, add };
|
||||
}
|
||||
|
||||
it('searches script names, campaign IDs and friendly server names across all retained history before pagination', async () => {
|
||||
const { handle, get, add } = await setup();
|
||||
add('old-multi-script', ['done']);
|
||||
add('old-legacy-script', ['done']);
|
||||
handle.exec(`INSERT INTO scripts(id,name,target_rel_path) VALUES(1,'Double Tap','double.gsc'),(2,'100% power','power.gsc');
|
||||
INSERT INTO script_versions(id,script_id,version,sha256,size) VALUES(1,1,2,'hash',0);
|
||||
INSERT INTO deployment_scripts(deployment_id,script_id,script_version_id) VALUES('old-multi-script',1,1);
|
||||
UPDATE deployments SET script_id=2 WHERE id='old-legacy-script';`);
|
||||
for (let i = 0; i < 30; i++) add(`newer-${i}`, ['done']);
|
||||
expect((await get('q=DOUBLE%20TAP&limit=1')).deployments.map(row => row.id)).toEqual(['old-multi-script']);
|
||||
expect((await get('q=old-legacy&limit=1')).deployments.map(row => row.id)).toEqual(['old-legacy-script']);
|
||||
expect((await get('q=%25')).deployments.map(row => row.id)).toEqual(['old-legacy-script']);
|
||||
const friendly = await get('q=Kino%20Der%20Toten&limit=1');
|
||||
expect(friendly.total).toBe(32);
|
||||
expect(friendly.deployments).toHaveLength(1);
|
||||
expect((await get('q=not-found')).total).toBe(0);
|
||||
});
|
||||
|
||||
it('keeps API status filters and active/history partitions consistent with displayed outcomes', async () => {
|
||||
const { handle, get, add } = await setup();
|
||||
add('complete', ['done']);
|
||||
add('deleted-complete', ['done'], 'promote', true);
|
||||
add('pending', ['done', 'pending']);
|
||||
add('held', ['held']);
|
||||
add('manual', ['manual']);
|
||||
add('failed', ['failed', 'pending']);
|
||||
add('deleted-failed', ['failed'], 'promote', true);
|
||||
add('withdrawn', ['skipped', 'cancelled']);
|
||||
add('empty', []);
|
||||
add('rollback', ['done'], 'rollback');
|
||||
add('reversed', ['done']);
|
||||
handle.prepare("UPDATE deployments SET rolled_back_by='rollback' WHERE id='reversed'").run();
|
||||
const all = (await get('limit=100')).deployments;
|
||||
for (const status of ['attention', 'progress', 'complete', 'rolled-back', 'inactive']) {
|
||||
const expected = all.filter(row => campaignOutcome(row).status === status).map(row => row.id);
|
||||
const filtered = await get(`status=${status}&limit=100`);
|
||||
expect(filtered.total).toBe(expected.length);
|
||||
expect(filtered.deployments.map(row => row.id)).toEqual(expected);
|
||||
}
|
||||
const active = await get('active=true');
|
||||
expect(active.deployments.map(row => row.id).sort()).toEqual(['failed', 'held', 'manual', 'pending']);
|
||||
const history = await get('view=history');
|
||||
expect(history.total + active.total).toBe(all.length);
|
||||
expect(history.deployments.some(row => active.deployments.some(candidate => candidate.id === row.id))).toBe(false);
|
||||
});
|
||||
|
||||
it('combines search, type and status filters before counting and paginating history', async () => {
|
||||
const { get, add } = await setup();
|
||||
for (let i = 0; i < 30; i++) add(`repair-${i}`, ['done'], 'drift');
|
||||
add('promote', ['done']);
|
||||
add('incomplete-repair', ['pending'], 'drift');
|
||||
const query = 'q=maintenance%20repair&action=drift&status=complete&view=history&limit=25';
|
||||
const first = await get(query);
|
||||
const second = await get(`${query}&offset=25`);
|
||||
expect(first.total).toBe(30);
|
||||
expect(first.deployments).toHaveLength(25);
|
||||
expect(second.deployments).toHaveLength(5);
|
||||
expect(new Set([...first.deployments, ...second.deployments].map(row => row.id)).size).toBe(30);
|
||||
expect((await get('action=rollback')).total).toBe(0);
|
||||
});
|
||||
@@ -1,13 +1,32 @@
|
||||
import { afterEach, expect, it, vi } from 'vitest';
|
||||
import { campaignServerStatus, fetchCampaigns } from '../../src/lib/ui/campaigns.js';
|
||||
import { campaignOutcome, campaignServerStatus, campaignTitle, fetchCampaigns } from '../../src/lib/ui/campaigns.js';
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
it('names campaigns with their script or beta and gives maintenance a readable title', () => {
|
||||
const campaign = { action: 'promote', beta_id: 74, rolled_back_by: null, script_name: null, script_version: null, scripts: [], servers: [] };
|
||||
expect(campaignTitle(campaign)).toBe('Promote · Beta #74');
|
||||
expect(campaignTitle({ ...campaign, scripts: [{ script_name: 'Double Tap', script_version: 2 }] })).toBe('Promote · Double Tap v2');
|
||||
expect(campaignTitle({ ...campaign, scripts: Array(7).fill({ script_name: null, script_version: null }) })).toBe('Promote · 7 scripts');
|
||||
expect(campaignTitle({ ...campaign, action: 'rollback' })).toBe('Rollback · Beta #74');
|
||||
expect(campaignTitle({ ...campaign, action: 'drift' })).toBe('Maintenance repair');
|
||||
});
|
||||
|
||||
it('summarizes progress without letting deleted servers create false alarms or false completion', () => {
|
||||
const campaign = { rolled_back_by: null, servers: [{ restart_state: 'done' }, { restart_state: 'failed', server_deleted_at: '2026-10-06' }] };
|
||||
expect(campaignOutcome(campaign)).toMatchObject({ status: 'inactive', label: '1/2 complete', tone: 'neutral' });
|
||||
expect(campaignOutcome({ ...campaign, servers: [{ restart_state: 'done' }, { restart_state: 'pending' }] })).toMatchObject({ status: 'progress', label: '1/2 complete', completed: 1 });
|
||||
expect(campaignOutcome({ ...campaign, servers: [{ restart_state: 'failed' }] })).toMatchObject({ status: 'attention', tone: 'danger' });
|
||||
expect(campaignOutcome({ ...campaign, servers: [{ restart_state: 'held' }] })).toMatchObject({ status: 'attention', tone: 'warning' });
|
||||
expect(campaignOutcome({ ...campaign, rolled_back_by: 'rollback' })).toMatchObject({ status: 'rolled-back', label: 'Rolled back' });
|
||||
});
|
||||
|
||||
it('distinguishes queued work, file application, and health verification without treating stage messages as failures', () => {
|
||||
const server = { restart_state: 'pending', restarted_at: null, error: 'Queued to restart; waiting for another server to recover' };
|
||||
expect(campaignServerStatus(server, [{ apply_state: 'pending' }])).toEqual({ group: 'queued', label: 'Queued', tone: 'neutral' });
|
||||
expect(campaignServerStatus(server, [{ apply_state: 'applying' }])).toMatchObject({ group: 'processing', label: 'Applying files' });
|
||||
expect(campaignServerStatus({ ...server, restarted_at: '2026-10-06T20:08:29Z', error: 'Checking container state with host agent' }, [{ apply_state: 'applied' }]))
|
||||
const verifying = { ...server, restarted_at: '2026-10-06T20:08:29Z', error: 'Checking container state with host agent' };
|
||||
expect(campaignServerStatus(verifying, [{ apply_state: 'applied' }]))
|
||||
.toEqual({ group: 'processing', label: 'Verifying health', tone: 'info' });
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user