dsh 0.2.0-rc.2: in-container update (experimental channel), path-agnostic compose, upstream patches adapted to 0.1.x/0.2.x
This commit is contained in:
commit
e6dcef9bf5
302 files changed
+75178
No files matched your search
@@ -0,0 +1,148 @@
|
||||
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)
|
||||
})
|
||||
Reference in new issue
Block a user