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)) } })