Files
dsh/upstream/container/platform/test/rollback.test.mjs
T

149 lines
6.4 KiB
JavaScript

import assert from 'node:assert/strict'
import { mkdtemp } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import test from 'node:test'
import { UpdateJournal } from '../../control-plane/modules/updater/lib/journal.mjs'
import { CompleteStateRecovery } from '../../control-plane/modules/updater/lib/rollback.mjs'
async function fixture() {
const root = await mkdtemp(join(tmpdir(), 'dsh-complete-rollback-'))
const journal = new UpdateJournal(join(root, 'journal.json'))
const from = {
dsh: '0.1.0-rc.7', environment: 'env-1', runtime: 'runtime-a', dataSnapshot: null,
receiptTokens: ['stable-b', 'stable-a'],
}
const to = { dsh: '0.1.0-rc.8', environment: 'env-1', runtime: 'runtime-b' }
await journal.begin({ transactionId: 'transaction-a', mode: 'experimental', from, to })
await journal.transition('candidate-ready', { receiptTokens: ['experimental'] })
await journal.transition('suspended')
await journal.transition('snapshot-created', { snapshotId: 'snapshot-a' })
await journal.transition('switched')
await journal.transition('probation', { probationUntil: '2026-08-19T00:02:00.000Z' })
await journal.transition('committed')
const calls = []
const current = { ...to, dataSnapshot: null, receiptTokens: ['experimental', 'stable-a', 'stable-b'] }
const activator = {
currentDeployment: async () => current,
rollbackDeployments: async () => ({
current: {
id: current.runtime,
dshVersion: current.dsh,
environmentVersion: current.environment,
receiptTokens: current.receiptTokens,
},
previous: {
id: 'runtime-a-materialized',
dshVersion: from.dsh,
environmentVersion: from.environment,
receiptTokens: from.receiptTokens,
},
}),
suspendDsh: async () => calls.push('suspend'),
restoreDeployment: async (value, options) => calls.push(`restore:${value.runtime}:${String(options.resume)}`),
resumeDsh: async () => calls.push('resume'),
}
const snapshots = {
inspect: async id => ({
id, createdAt: '2026-08-19T00:00:00.000Z', archiveSha256: 'a'.repeat(64), archiveSize: 42,
runtimeId: from.runtime, environmentVersion: from.environment, dshVersion: from.dsh,
}),
restore: async id => calls.push(`data:${id}`),
}
return { journal, from, to, current, activator, snapshots, calls, recovery: new CompleteStateRecovery({ journal, snapshots, activator }) }
}
test('binds a complete rollback plan to current Runtime, receipts, and verified snapshot', async () => {
const value = await fixture()
const plan = await value.recovery.plan()
assert.equal(plan.previous.runtime, 'runtime-a-materialized')
assert.deepEqual(plan.previous.receiptTokens, ['stable-a', 'stable-b'])
value.current.runtime = 'different-runtime'
assert.equal((await value.recovery.plan()).snapshot, null)
})
test('restores the previous complete state only with the current plan ID', async () => {
const value = await fixture()
const plan = await value.recovery.plan()
const progress = []
await assert.rejects(value.recovery.restore('stale'), /stale/)
await assert.rejects(value.recovery.restore(plan.planId, { requireConfirmation: true }), /confirmation/)
const result = await value.recovery.restore(plan.planId, {
requireConfirmation: true,
confirmDataLoss: true,
onProgress: async (phase, percent) => progress.push([phase, percent]),
})
assert.equal(result.status, 'rolled-back')
assert.deepEqual(progress, [['stopping', 15], ['switching', 35], ['restoring-data', 65], ['verifying', 85]])
assert.deepEqual(value.calls, ['suspend', 'restore:runtime-a-materialized:false', 'data:snapshot-a', 'resume'])
assert.equal((await value.journal.read()).phase, 'rolled-back')
assert.equal((await value.recovery.plan()).snapshot, null)
})
test('rejects a valid snapshot archive bound to a different deployment', async () => {
const value = await fixture()
const inspect = value.snapshots.inspect
value.snapshots.inspect = async id => ({ ...await inspect(id), environmentVersion: 'other-env' })
await assert.rejects(value.recovery.plan(), /does not describe/)
})
test('binds Stable rollback to the exact Bootstrap previous slot without a data snapshot', async () => {
const calls = []
const progress = []
const references = {
environment: { sha256: 'a'.repeat(64) },
pristine: { sha256: 'b'.repeat(64) },
runtime: { sha256: 'c'.repeat(64) },
systemPlugins: { sha256: 'd'.repeat(64) },
}
const current = {
id: 'deployment-record-current', authority: 'stable', targetSequence: 2,
dshVersion: '0.1.0-rc.8', environmentVersion: 'env-2', receiptTokens: ['stable-2'], ...references,
}
const previous = {
id: 'deployment-record-previous', authority: 'stable', targetSequence: 1,
dshVersion: '0.1.0-rc.7', environmentVersion: 'env-1', receiptTokens: ['stable-1'], ...references,
}
const recovery = new CompleteStateRecovery({
journal: { read: async () => undefined },
snapshots: {},
activator: {
rollbackDeployments: async () => ({ current, previous }),
rollback: async recordId => calls.push(recordId),
},
})
const plan = await recovery.plan()
assert.equal(plan.mode, 'stable')
assert.equal(plan.snapshot, null)
assert.equal(plan.previous.runtime, previous.id)
await assert.rejects(recovery.restore(plan.planId, {
requireConfirmation: true,
confirmDataLoss: true,
}), /snapshot/)
assert.deepEqual(calls, [])
assert.deepEqual(await recovery.restore(plan.planId, {
onProgress: async (phase, percent) => progress.push([phase, percent]),
}), { status: 'rolled-back', transactionId: null })
assert.deepEqual(calls, [previous.id])
assert.deepEqual(progress, [['switching', 35], ['verifying', 90]])
})
test('does not expose a rollback between Image and Managed Records with identical content', async () => {
const reference = { sha256: 'a'.repeat(64) }
const base = {
authority: 'stable', targetSequence: 1, dshVersion: '0.1.0-rc.7', environmentVersion: 'env-1',
environment: reference, pristine: reference, runtime: reference, systemPlugins: reference, receiptTokens: [],
}
const recovery = new CompleteStateRecovery({
journal: { read: async () => undefined },
snapshots: {},
activator: {
rollbackDeployments: async () => ({
current: { ...base, id: 'deployment-record-image' },
previous: { ...base, id: 'deployment-record-materialized' },
}),
},
})
assert.equal(await recovery.plan(), null)
})