feat: split online update into explicit stages
Release / release (push) Canceled after 0s

This commit is contained in:
Qiufeng
2026-08-18 09:01:34 +08:00
parent 3c7847c8f5
commit 6d4ae00ae0
5 changed files with 246 additions and 84 deletions
+102 -21
View File
@@ -7,7 +7,7 @@ function envelope(data: unknown) {
return { data, requestId: '01M00000000000000000000090' };
}
function session() {
function session(canExecute = true) {
return {
authenticated: true,
user: {
@@ -19,12 +19,13 @@ function session() {
},
roles: [{ code: 'SYSTEM_ADMIN', name: '系统管理员', workbenchRoute: '/governance/settings' }],
activeRole: 'SYSTEM_ADMIN',
permissions,
permissions: canExecute ? permissions : permissions.filter((permission) => permission !== 'admin:update:execute'),
};
}
function updateView(state: string, checked: boolean, enabled = true, recoveryPending = false) {
const busy = [
'QUEUED',
'DOWNLOAD_QUEUED',
'INSTALL_QUEUED',
'VERIFYING',
@@ -69,6 +70,12 @@ function updateView(state: string, checked: boolean, enabled = true, recoveryPen
};
}
interface FixtureOptions {
canExecute?: boolean;
failDownloadResponse?: boolean;
failInstallResponse?: boolean;
}
async function installFixture(
page: Page,
downloadBodies: unknown[],
@@ -78,7 +85,9 @@ async function installFixture(
completeAfterInstall = false,
initialState?: string,
completeBusyAfterFirstRead = false,
options: FixtureOptions = {},
) {
const { canExecute = true, failDownloadResponse = false, failInstallResponse = false } = options;
let checked = recoveryPending || Boolean(initialState);
let state = recoveryPending ? 'FAILED' : initialState || 'IDLE';
const history: Array<Record<string, unknown>> = [];
@@ -93,7 +102,7 @@ async function installFixture(
objectType: 'SYSTEM_UPDATE',
objectPublicId: 'SYSTEM_UPDATE',
resultCode: 'SUCCESS',
reason: actionCode === 'SYSTEM_UPDATE_REQUEST' ? '管理员确认立即重启' : null,
reason: actionCode === 'SYSTEM_UPDATE_REQUEST' ? '管理员确认立即更新并重启' : null,
beforeJson: null,
afterJson: JSON.stringify({ targetVersion }),
occurredAt: '2026-08-16T00:02:00Z',
@@ -103,9 +112,9 @@ async function installFixture(
await page.route('**/api/v1/**', async (route: Route) => {
const request = route.request();
const pathname = new URL(request.url()).pathname;
if (pathname === '/api/v1/auth/session') return route.fulfill({ json: envelope(session()) });
if (pathname === '/api/v1/auth/profile') return route.fulfill({ json: envelope(session().user) });
if (pathname === '/api/v1/auth/roles') return route.fulfill({ json: envelope(session().roles) });
if (pathname === '/api/v1/auth/session') return route.fulfill({ json: envelope(session(canExecute)) });
if (pathname === '/api/v1/auth/profile') return route.fulfill({ json: envelope(session(canExecute).user) });
if (pathname === '/api/v1/auth/roles') return route.fulfill({ json: envelope(session(canExecute).roles) });
if (pathname === '/api/v1/auth/csrf') {
return route.fulfill({
headers: { 'set-cookie': 'XSRF-TOKEN=update-csrf; Path=/; SameSite=Lax' },
@@ -129,19 +138,23 @@ async function installFixture(
if (pathname === '/api/v1/admin/system-update/check' && request.method() === 'POST') {
checked = true;
recordHistory('SYSTEM_UPDATE_CHECK');
return route.fulfill({ json: envelope(updateView(state, checked, enabled, recoveryPending)) });
const response = updateView(state, checked, enabled, recoveryPending);
if (!canExecute) response.allowedActions = ['CHECK'];
return route.fulfill({ json: envelope(response) });
}
if (pathname === '/api/v1/admin/system-update/download' && request.method() === 'POST') {
downloadBodies.push(request.postDataJSON());
recordHistory('SYSTEM_UPDATE_DOWNLOAD_REQUEST');
state = 'READY';
if (failDownloadResponse) return route.abort('connectionreset');
return route.fulfill({ json: envelope(updateView('DOWNLOAD_QUEUED', true, enabled)) });
}
if (pathname === '/api/v1/admin/system-update/install' && request.method() === 'POST') {
installBodies.push(request.postDataJSON());
recordHistory('SYSTEM_UPDATE_REQUEST');
const queued = updateView('INSTALL_QUEUED', true, enabled);
state = completeAfterInstall ? 'SUCCEEDED' : 'INSTALL_QUEUED';
state = failInstallResponse || completeAfterInstall ? 'SUCCEEDED' : 'INSTALL_QUEUED';
if (failInstallResponse) return route.abort('connectionreset');
return route.fulfill({ json: envelope(queued) });
}
if (pathname === '/api/v1/audit/logs' && request.method() === 'GET') {
@@ -158,7 +171,7 @@ async function installFixture(
});
}
test('system administrator checks and queues a signed online update', async ({ page }) => {
test('system administrator checks, downloads, and explicitly installs a signed online update', async ({ page }) => {
const downloadBodies: unknown[] = [];
const installBodies: unknown[] = [];
await installFixture(page, downloadBodies, installBodies);
@@ -166,16 +179,77 @@ test('system administrator checks and queues a signed online update', async ({ p
await expect(page.locator('h1.page-title-sr-only')).toHaveText('系统更新');
await expect(page.locator('.version-item').filter({ hasText: '当前版本' })).toContainText('1.0.0-preview.1');
await page.getByRole('button', { name: '获取更新', exact: true }).click();
await page.getByRole('button', { name: '获取版本', exact: true }).click();
await expect(page.locator('.version-item').filter({ hasText: '最新版本' })).toContainText('1.0.0-preview.2');
await expect(page.getByRole('button', { name: '立即重启', exact: true })).toBeVisible({ timeout: 5_000 });
expect(downloadBodies).toEqual([{ version: '1.0.0-preview.2' }]);
await expect(page.getByText('下载更新包', { exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: '立即更新', exact: true })).toBeVisible({ timeout: 5_000 });
await expect(page.getByRole('button', { name: '立即更新并重启', exact: true })).toHaveCount(0);
expect(downloadBodies).toEqual([]);
await page.getByRole('button', { name: '立即重启', exact: true }).click();
await page.getByRole('button', { name: '立即更新', exact: true }).click();
expect(downloadBodies).toEqual([{ version: '1.0.0-preview.2' }]);
const updateState = page.locator('.version-item').filter({ hasText: '更新状态' });
await expect(updateState.getByText('下载已排队', { exact: true })).toBeVisible();
await expect(updateState.getByText('下载完成', { exact: true })).toBeVisible({ timeout: 8_000 });
await expect(page.getByRole('button', { name: '立即更新并重启', exact: true })).toBeVisible();
await page.getByRole('button', { name: '立即更新并重启', exact: true }).click();
await expect(page.getByText('安装请求已排队', { exact: true })).toBeVisible();
expect(installBodies).toEqual([{ version: '1.0.0-preview.2', reason: '管理员确认立即重启' }]);
expect(installBodies).toEqual([{ version: '1.0.0-preview.2', reason: '管理员确认立即更新并重启' }]);
});
test('rechecking a downloaded package prompts installation instead of downloading again', async ({ page }) => {
await installFixture(page, [], [], true, false, false, 'READY');
await page.goto('/governance/update');
await page.getByRole('button', { name: '获取版本', exact: true }).click();
await expect(page.getByText('更新包已下载完成,请点击“立即更新并重启”安装新版本', { exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: '立即更新', exact: true })).toHaveCount(0);
await expect(page.getByRole('button', { name: '立即更新并重启', exact: true })).toBeVisible();
});
test('view-only administrators see a download permission message', async ({ page }) => {
await installFixture(page, [], [], true, false, false, undefined, false, { canExecute: false });
await page.goto('/governance/update');
await page.getByRole('button', { name: '获取版本', exact: true }).click();
await expect(page.getByText('发现新版本,但当前账号没有下载权限', { exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: '立即更新', exact: true })).toHaveCount(0);
await expect(page.getByRole('button', { name: '立即更新并重启', exact: true })).toHaveCount(0);
});
test('download and install continue polling after an accepted command loses its response', async ({ page }) => {
await installFixture(page, [], [], true, false, false, undefined, false, {
failDownloadResponse: true,
});
await page.goto('/governance/update');
await page.getByRole('button', { name: '获取版本', exact: true }).click();
await page.getByRole('button', { name: '立即更新', exact: true }).click();
await expect(
page.locator('.version-item').filter({ hasText: '更新状态' }).getByText('下载完成', { exact: true }),
).toBeVisible({ timeout: 8_000 });
await expect(page.getByRole('button', { name: '立即更新并重启', exact: true })).toBeVisible();
});
test('accepted install continues to the refresh countdown after its response is lost', async ({ page }) => {
await installFixture(page, [], [], true, false, false, undefined, false, {
failInstallResponse: true,
});
await page.goto('/governance/update');
await page.getByRole('button', { name: '获取版本', exact: true }).click();
await page.getByRole('button', { name: '立即更新', exact: true }).click();
await expect(
page.locator('.version-item').filter({ hasText: '更新状态' }).getByText('下载完成', { exact: true }),
).toBeVisible({ timeout: 8_000 });
await page.getByRole('button', { name: '立即更新并重启', exact: true }).click();
await expect(page.getByText('新版本已通过健康检查,页面将在 10 秒后自动刷新。', { exact: true })).toBeVisible({
timeout: 8_000,
});
});
test('brand logo returns the active identity to its workbench', async ({ page }) => {
@@ -193,8 +267,9 @@ test('disabled online updates expose status without an executable check action',
await page.goto('/governance/update');
await expect(page.getByText('在线更新未配置', { exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: '获取更新', exact: true })).toBeDisabled();
await expect(page.getByRole('button', { name: '立即重启', exact: true })).toHaveCount(0);
await expect(page.getByRole('button', { name: '获取版本', exact: true })).toBeDisabled();
await expect(page.getByRole('button', { name: '立即更新', exact: true })).toHaveCount(0);
await expect(page.getByRole('button', { name: '立即更新并重启', exact: true })).toHaveCount(0);
});
test('rollback recovery keeps the failure visible without an install action', async ({ page }) => {
@@ -202,15 +277,19 @@ test('rollback recovery keeps the failure visible without an install action', as
await page.goto('/governance/update');
await expect(page.getByText('回滚未完成,更新服务将重试', { exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: '立即重启', exact: true })).toHaveCount(0);
await expect(page.getByRole('button', { name: '立即更新并重启', exact: true })).toHaveCount(0);
});
test('successful installation starts the ten-second automatic refresh countdown', async ({ page }) => {
await installFixture(page, [], [], true, false, true);
await page.goto('/governance/update');
await page.getByRole('button', { name: '获取更新', exact: true }).click();
await page.getByRole('button', { name: '立即重启', exact: true }).click({ timeout: 5_000 });
await page.getByRole('button', { name: '获取版本', exact: true }).click();
await page.getByRole('button', { name: '立即更新', exact: true }).click({ timeout: 5_000 });
await expect(
page.locator('.version-item').filter({ hasText: '更新状态' }).getByText('下载完成', { exact: true }),
).toBeVisible({ timeout: 8_000 });
await page.getByRole('button', { name: '立即更新并重启', exact: true }).click();
await expect(page.getByText('新版本已通过健康检查,页面将在 10 秒后自动刷新。', { exact: true })).toBeVisible({
timeout: 5_000,
@@ -221,7 +300,9 @@ test('reloading during installation resumes polling and starts the refresh count
await installFixture(page, [], [], true, false, false, 'INSTALLING', true);
await page.goto('/governance/update');
await expect(page.getByText('安装中', { exact: true })).toBeVisible();
await expect(
page.locator('.version-item').filter({ hasText: '更新状态' }).getByText('安装中', { exact: true }),
).toBeVisible();
await expect(page.getByText('新版本已通过健康检查,页面将在 10 秒后自动刷新。', { exact: true })).toBeVisible({
timeout: 6_000,
});