feat: restart to apply update, privileged helper, rollback

This commit is contained in:
fsylvain committed 2026-10-01 21:36:40 +02:00
1 parent 70460e4170
commit 54e8fd5fed
63 files changed
+8635 -164

No files matched your search

@@ -8,7 +8,15 @@ import type { UpdateStatus } from '@/composables/useUpdate'
import { DISMISSED_VERSION_KEY, useUpdateStore } from '@/stores/update'
import { useSessionStore } from '@/stores/session'
import HomeView from '../HomeView.vue'
import { NEW_TAG, downloading, newerAvailable, ready, startable, verifying } from './updateTestKit'
import {
NEW_TAG,
downloading,
newerAvailable,
ready,
restarting,
startable,
verifying,
} from './updateTestKit'
function mockResponse(status: number, body: unknown = null) {
return {
@@ -145,6 +153,22 @@ describe('Home banner, ready to install (owner)', () => {
})
})
describe('Home banner while restarting', () => {
it('shows no update banner and does not poll', async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'] })
await mountHome('owner', restarting())
expect(banner()).toBeNull()
const reads = () =>
vi.mocked(global.fetch).mock.calls.filter(([url]) => url === '/v1/system/update')
const readsAfterMount = reads().length
await vi.advanceTimersByTimeAsync(5000)
expect(reads()).toHaveLength(readsAfterMount)
vi.useRealTimers()
})
})
describe('Sidebar pill and mobile dot during a job', () => {
it.each([
{ name: 'downloading', status: downloading() },
File diff suppressed because it is too large. Load diff
@@ -290,7 +290,7 @@ describe('Settings, owner, Verifying', () => {
})
describe('Settings, owner, Ready to install', () => {
it('shows the success block, a disabled Restart, the notes and the hint, and stops polling', async () => {
it('shows the success block, an enabled Restart, the notes and the hint, and stops polling', async () => {
useTimers()
await mountSettings({
role: 'owner',
@@ -310,18 +310,15 @@ describe('Settings, owner, Ready to install', () => {
)
expect(findButton("What's new in 2.3.0-beta")!.getAttribute('aria-expanded')).toBe('false')
const restart = findButton('Restart') as HTMLButtonElement
expect(restart.disabled).toBe(true)
expect(restart.disabled).toBe(false)
const readsBefore = callsTo(STATUS_PATH).length
restart.click()
await flushPromises()
await tick(3000)
expect(callsTo(STATUS_PATH)).toHaveLength(readsBefore)
expect(requestMock.mock.calls.map(([path]) => path)).not.toContain(
'/v1/system/update/restart',
)
expect(document.body.querySelector('[role="dialog"]')).toBeNull()
})
})
@@ -11,7 +11,6 @@ import {
CHECK_PATH,
NEW_TAG,
NOTE_ITEMS,
RELEASE_URL,
RUNNING_VERSION,
STATUS_PATH,
findButton,
@@ -257,7 +256,9 @@ describe('Settings, owner, newer version, not prepared', () => {
const link = Array.from(updatesCard().querySelectorAll('a')).find(
(anchor) => anchor.textContent?.trim() === 'How to update',
)!
expect(link.getAttribute('href')).toBe(RELEASE_URL)
expect(link.getAttribute('href')).toBe(
'https://git.example/fsylvain/RenewCMS/src/branch/main/docs/deployment.md#browser-updates',
)
expect(link.getAttribute('target')).toBe('_blank')
expect(link.getAttribute('rel')).toContain('noopener')
expect(cardButtons()).toEqual(['Copy'])
@@ -441,7 +442,7 @@ describe('A check runs on the first open after 24 hours, for the owner only', ()
})
})
describe('No Update, Restart or progress element exists in this slice', () => {
describe('No Update, Restart or progress element exists when the server is not prepared or for admins', () => {
it.each([
{ name: 'up to date', role: 'owner' as const, status: upToDate() },
{ name: 'not prepared', role: 'owner' as const, status: newerAvailable() },
+39
View File
@@ -14,6 +14,7 @@ export const NOTE_ITEMS = [
export const STATUS_PATH = '/v1/system/update'
export const CHECK_PATH = '/v1/system/update/check'
export const DOWNLOAD_PATH = '/v1/system/update/download'
export const RESTART_PATH = '/v1/system/update/restart'
export const BOARD_TOTAL_BYTES = 27262976
export const BOARD_DONE_BYTES = 11534336
@@ -35,6 +36,7 @@ export function upToDate(overrides: Partial<UpdateStatus> = {}): UpdateStatus {
progress: null,
error: null,
started_at: null,
finished_at: null,
...overrides,
}
}
@@ -94,3 +96,40 @@ export function ready(): UpdateStatus {
export function failedWith(error: 'download' | 'verification'): UpdateStatus {
return startable({ state: 'failed', error, started_at: '2026-09-30T09:51:00Z' })
}
export const RESTART_STARTED_AT = '2026-09-30T09:51:00Z'
export const RESTART_FINISHED_AT = '2026-09-30T09:52:00Z'
export function restarting(): UpdateStatus {
return startable({ state: 'restarting', started_at: RESTART_STARTED_AT })
}
export function updateSucceeded(): UpdateStatus {
return upToDate({
current: NEW_TAG,
latest: NEW_TAG,
available: false,
can_apply: true,
state: 'succeeded',
started_at: RESTART_STARTED_AT,
finished_at: RESTART_FINISHED_AT,
})
}
export function rolledBack(error: 'rolled_back' | 'install' = 'rolled_back'): UpdateStatus {
return startable({
state: 'failed',
error,
started_at: RESTART_STARTED_AT,
finished_at: RESTART_FINISHED_AT,
})
}
export function plainRestartDone(): UpdateStatus {
return upToDate({
can_apply: true,
state: 'idle',
started_at: null,
finished_at: RESTART_FINISHED_AT,
})
}