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

112 lines
5.5 KiB
JavaScript

import assert from 'node:assert/strict'
import { chmod, lstat, mkdir, mkdtemp, readFile, readlink, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import test from 'node:test'
import { PersistentStateSnapshots } from '../../control-plane/modules/updater/lib/snapshots.mjs'
import { SnapshotClient } from '../../control-plane/modules/updater/lib/snapshot-client.mjs'
import { UserPluginSnapshots } from '../../control-plane/modules/plugin-manager/user-snapshots.mjs'
import { LocalApiClient } from '../../control-plane/modules/updater/lib/client.mjs'
import { createSnapshotServer, listenSnapshots } from '../stage0/lib/snapshot-server.mjs'
async function fixture(options = {}) {
const root = await mkdtemp(join(tmpdir(), 'dsh-snapshots-'))
const sourceRoot = join(root, 'home')
await mkdir(join(sourceRoot, 'nested'), { recursive: true })
return {
root,
sourceRoot,
snapshots: new PersistentStateSnapshots({ root: join(root, 'snapshots'), sourceRoot, ...options }),
}
}
const state = {
id: 'before-rc8',
runtimeId: '0.1.0-rc.7-1',
environmentVersion: '1.0.0',
dshVersion: '0.1.0-rc.7',
}
test('snapshots and restores complete DSH_HOME contents and permissions', async () => {
const { sourceRoot, snapshots } = await fixture({ now: () => new Date('2026-08-19T00:00:00.000Z') })
await writeFile(join(sourceRoot, '.config'), 'before')
await writeFile(join(sourceRoot, 'nested', 'database'), 'rows')
await chmod(join(sourceRoot, 'nested', 'database'), 0o640)
await symlink('nested/database', join(sourceRoot, 'database-link'))
const createProgress = []
const snapshot = await snapshots.create({ ...state, onProgress: value => { createProgress.push(value) } })
assert.equal(snapshot.createdAt, '2026-08-19T00:00:00.000Z')
assert.equal(createProgress.at(-1).processedItems, createProgress.at(-1).totalItems)
await writeFile(join(sourceRoot, '.config'), 'after')
await writeFile(join(sourceRoot, 'new-data'), 'remove me')
const restoreProgress = []
await snapshots.restore(state.id, { onProgress: value => { restoreProgress.push(value) } })
assert.equal(await readFile(join(sourceRoot, '.config'), 'utf8'), 'before')
assert.equal(await readFile(join(sourceRoot, 'nested', 'database'), 'utf8'), 'rows')
assert.equal((await lstat(join(sourceRoot, 'nested', 'database'))).mode & 0o777, 0o640)
assert.equal(await readlink(join(sourceRoot, 'database-link')), 'nested/database')
await assert.rejects(readFile(join(sourceRoot, 'new-data')), { code: 'ENOENT' })
assert.equal(restoreProgress.at(-1).processedItems, restoreProgress.at(-1).totalItems)
assert.equal((await snapshots.list())[0].id, state.id)
})
test('verifies a snapshot before clearing current DSH_HOME', async () => {
const { sourceRoot, snapshots } = await fixture()
await writeFile(join(sourceRoot, 'database'), 'before')
const snapshot = await snapshots.create(state)
await writeFile(join(snapshot.path, 'data.tar.gz'), 'corrupt')
await writeFile(join(sourceRoot, 'database'), 'current')
await assert.rejects(snapshots.restore(state.id), { code: 'TRUST_ARTIFACT_MISMATCH' })
assert.equal(await readFile(join(sourceRoot, 'database'), 'utf8'), 'current')
})
test('does not publish a partial snapshot when archive creation fails', async () => {
const { snapshots } = await fixture({ runImpl: async () => { throw new Error('no space left') } })
await assert.rejects(snapshots.create(state), /no space/)
assert.deepEqual(await snapshots.list(), [])
})
test('removing an absent snapshot is idempotent while corrupt snapshots remain errors', async () => {
const { snapshots } = await fixture()
assert.equal(await snapshots.remove('already-removed'), false)
const snapshot = await snapshots.create(state)
await writeFile(join(snapshot.path, 'data.tar.gz'), 'corrupt')
await assert.rejects(snapshots.remove(state.id), { code: 'TRUST_ARTIFACT_MISMATCH' })
})
test('Stage-0 serves only fixed DSH and Web Profile snapshot scopes', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-snapshot-server-'))
const dshHome = join(root, 'dsh')
const profileRoot = join(dshHome, 'profiles', 'web')
await mkdir(profileRoot, { recursive: true })
await writeFile(join(dshHome, 'settings.yaml'), 'before')
await writeFile(join(profileRoot, 'package.json'), '{"before":true}')
const server = createSnapshotServer({
dshSnapshots: new PersistentStateSnapshots({ root: join(root, 'snapshots'), sourceRoot: dshHome }),
userPluginSnapshots: new UserPluginSnapshots({ root: join(root, 'user-plugin-snapshots'), profileRoot }),
})
const socket = join(root, 'snapshot.sock')
await listenSnapshots(server, socket)
const api = new LocalApiClient(socket)
const dsh = new SnapshotClient(api, 'dsh')
const userPlugin = new SnapshotClient(api, 'user-plugin')
try {
const created = await dsh.create(state)
assert.equal(created.id, state.id)
assert.equal(created.path, undefined)
await writeFile(join(dshHome, 'settings.yaml'), 'after')
await dsh.restore(state.id)
assert.equal(await readFile(join(dshHome, 'settings.yaml'), 'utf8'), 'before')
const plugin = await userPlugin.create('profile-before-change')
assert.equal(plugin.id, 'profile-before-change')
assert.equal(plugin.path, undefined)
await writeFile(join(profileRoot, 'package.json'), '{"after":true}')
await userPlugin.restore('profile-before-change')
assert.equal(await readFile(join(profileRoot, 'package.json'), 'utf8'), '{"before":true}')
} finally {
await new Promise(resolve => server.close(resolve))
}
})