e2e: dispatch backup via source-group API

Per-host Run-backup is gone — the host_chrome partial still
renders the button but it's hard-disabled with a tooltip
pointing to per-source-group Run-now. The smoke test was
clicking that disabled button and waiting forever for a URL
change that would never happen.

Replace the navigation-based dispatch with two API calls:
create a source group covering the agent's /source mount,
then POST to /api/hosts/{id}/source-groups/{gid}/run. The
backup-status assertion at the end is unchanged — host record
is still the source of truth.
This commit is contained in:
2026-05-08 22:16:57 +01:00
parent b9439da467
commit 41def51977
2 changed files with 50 additions and 10 deletions
+37
View File
@@ -107,6 +107,43 @@ export async function waitForHostStatus(
throw new Error(`waitForHostStatus: timeout. Last seen: ${JSON.stringify(last)}`);
}
export async function createSourceGroup(
request: APIRequestContext,
cookie: string,
hostID: string,
body: { name: string; includes: string[]; excludes?: string[] },
): Promise<string> {
const res = await request.post(`${baseURL}/api/hosts/${hostID}/source-groups`, {
headers: { cookie, 'content-type': 'application/json' },
data: {
name: body.name,
includes: body.includes,
excludes: body.excludes ?? [],
retention_policy: {},
retry_max: 0,
retry_backoff_seconds: 0,
},
});
if (!res.ok()) throw new Error(`createSourceGroup: ${res.status()} ${await res.text()}`);
const created = (await res.json()) as { id?: string; group?: { id?: string } };
const id = created.id ?? created.group?.id;
if (!id) throw new Error(`createSourceGroup: no id in response: ${JSON.stringify(created)}`);
return id;
}
export async function runSourceGroup(
request: APIRequestContext,
cookie: string,
hostID: string,
groupID: string,
): Promise<void> {
const res = await request.post(
`${baseURL}/api/hosts/${hostID}/source-groups/${groupID}/run`,
{ headers: { cookie } },
);
if (!res.ok()) throw new Error(`runSourceGroup: ${res.status()} ${await res.text()}`);
}
export async function getSessionCookie(page: Page): Promise<string> {
const cookies = await page.context().cookies();
const c = cookies.find((c) => c.name === 'rm_session');
+13 -10
View File
@@ -14,6 +14,8 @@ import {
waitForPendingHostID,
acceptPending,
waitForHostStatus,
createSourceGroup,
runSourceGroup,
getSessionCookie,
} from './lib/server';
@@ -53,18 +55,19 @@ test.describe('smoke: enrol-via-announce → backup', () => {
);
expect(readyHost.id).toBeTruthy();
// Trigger a backup via the UI form-post (HX-Redirect to /jobs/{id}).
await page.goto(`${baseURL}/hosts/${readyHost.id}`);
await Promise.all([
page.waitForURL(/\/jobs\//),
page.locator('form[action$="/run-backup"] button[type="submit"]').first().click(),
]);
// Per-host Run-now is gone; backups are dispatched per
// source-group now. Create one that maps to the agent's
// /source mount, then kick it via the JSON API.
const groupID = await createSourceGroup(request, cookie, readyHost.id, {
name: 'default',
includes: ['/source'],
});
await runSourceGroup(request, cookie, readyHost.id, groupID);
// Wait for the host's last_backup_status to flip to 'succeeded'.
// The job page itself is harder to assert on (it uses
// server-pushed updates and a reload-on-finish pattern); the
// host record is the source of truth and is what the dashboard
// surfaces.
// The host record is the source of truth: it's what the
// dashboard projects from job-completion events on the WS
// channel.
const finishedHost = await waitForHostStatus(
request, cookie,
(h) => h.id === readyHost.id && h.last_backup_status === 'succeeded',