import { test } from 'node:test'; import assert from 'node:assert/strict'; import fs, { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync, symlinkSync, readdirSync, statSync, linkSync } from 'node:fs'; import { syncBuiltinESMExports } from 'node:module'; import { tmpdir } from 'node:os'; import { join, basename } from 'node:path'; import { BUILTIN_DEPENDENCIES, BUILTIN_OVERRIDES, reconcileManifest, reconcileProfile } from './reconcile-profile.mjs'; import { initProfile } from './init-profile.mjs'; const old = name => 'file:/usr/local/lib/dsh-mgr/' + name + '-0.0.1-rc.1+build.2.tgz'; test('runtime-7.9 plugin references migrate to runtime-7.10 without changing bundle choices', t => { const bundles = ['dsh-web-mobile', 'dsh-chat-manager']; const f = fixture(t, { dependencies: { 'dsh-web-mobile': 'file:/usr/local/lib/dsh-mgr/dsh-web-mobile-3.1.0.tgz', 'dsh-free-search': 'file:/usr/local/lib/dsh-mgr/dsh-free-search-0.9.2.tgz', 'dsh-chat-manager': 'file:/usr/local/lib/dsh-mgr/dsh-chat-manager-1.5.16-dsh-mgr.1.tgz', }, dsh: { profile: { bundles } }, }); const result = f.run(); assert.equal(result.changed, true); assert.equal(readFileSync(result.backupPath, 'utf8'), f.original); const current = JSON.parse(readFileSync(f.path)); for (const name of Object.keys(current.dependencies)) assert.equal(current.dependencies[name], BUILTIN_DEPENDENCIES[name]); assert.deepEqual(current.dsh.profile.bundles, bundles); assert.deepEqual(f.run(), { changed: false }); }); function fixture(t, manifest) { const root = mkdtempSync(join(tmpdir(), 'dsh-reconcile-')); t.after(() => rmSync(root, { recursive: true, force: true })); const profile = join(root, 'profiles/web'); const archiveDirectory = join(root, 'archives'); mkdirSync(profile, { recursive: true }); mkdirSync(archiveDirectory); for (const reference of [...Object.values(BUILTIN_DEPENDENCIES), ...Object.values(BUILTIN_OVERRIDES)]) { writeFileSync(join(archiveDirectory, basename(reference)), 'fixture'); } const path = join(profile, 'package.json'); const original = typeof manifest === 'string' ? manifest : JSON.stringify(manifest, null, 4) + '\n'; writeFileSync(path, original); return { root, profile, path, original, archiveDirectory, run: () => reconcileProfile(profile, { archiveDirectory }) }; } test('migrates all sections, backs up exact bytes, replaces atomically with 0600, and is idempotent', t => { const f = fixture(t, { dependencies: { 'dsh-web-mobile': old('dsh-web-mobile') }, devDependencies: { 'dsh-chat-manager': old('dsh-chat-manager') }, optionalDependencies: { 'dsh-free-search': old('dsh-free-search') }, dsh: { profile: { bundles: [] } }, }); const originalLink = join(f.root, 'original-link'); linkSync(f.path, originalLink); const first = f.run(); assert.equal(first.changed, true); assert.equal(readFileSync(first.backupPath, 'utf8'), f.original); assert.equal(readFileSync(originalLink, 'utf8'), f.original); assert.equal(statSync(f.path).mode & 0o777, 0o600); assert.equal(statSync(first.backupPath).mode & 0o777, 0o600); const current = JSON.parse(readFileSync(f.path)); for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) { for (const name of Object.keys(current[section])) assert.equal(current[section][name], BUILTIN_DEPENDENCIES[name]); } assert.deepEqual(current.pnpm.overrides, BUILTIN_OVERRIDES); assert.deepEqual(current.dsh.profile.bundles, []); const before = statSync(f.path); assert.deepEqual(f.run(), { changed: false }); assert.equal(statSync(f.path).ino, before.ino); assert.equal(statSync(f.path).mtimeMs, before.mtimeMs); assert.equal(readdirSync(f.profile).length, 3); assert.equal(readFileSync(join(f.profile, '.dsh-lockfile-pending'), 'utf8'), 'builtin profile migration\n'); writeFileSync(f.path, f.original); assert.notEqual(f.run().backupPath, first.backupPath); assert.equal(readFileSync(first.backupPath, 'utf8'), f.original); }); function withSyncTrace(f, callback, failAt) { const originals = Object.fromEntries( ['openSync', 'writeFileSync', 'fsyncSync', 'closeSync', 'renameSync'].map(name => [name, fs[name]]), ); const descriptors = new Map(); const events = []; let directoryOpens = 0; fs.openSync = (path, ...args) => { const fd = originals.openSync(path, ...args); const target = path === f.profile ? `directory-${++directoryOpens}` : String(path).startsWith(`${f.path}.backup-`) ? 'backup' : String(path).startsWith(`${f.path}.tmp-`) ? 'temporary' : undefined; if (target) { descriptors.set(fd, target); events.push(`open:${target}`); } return fd; }; for (const [name, operation] of [['writeFileSync', 'write'], ['fsyncSync', 'fsync'], ['closeSync', 'close']]) { fs[name] = (fd, ...args) => { const target = descriptors.get(fd); if (target) { const event = `${operation}:${target}`; events.push(event); if (event === failAt) throw new Error(`injected ${event} failure`); } const result = originals[name](fd, ...args); if (name === 'closeSync') descriptors.delete(fd); return result; }; } fs.renameSync = (source, destination) => { assert.ok(source.startsWith(`${f.path}.tmp-`)); assert.equal(destination, f.path); events.push('rename:manifest'); return originals.renameSync(source, destination); }; syncBuiltinESMExports(); try { callback(events); } finally { Object.assign(fs, originals); syncBuiltinESMExports(); } } const durableWriteOrder = [ 'open:backup', 'write:backup', 'fsync:backup', 'close:backup', 'open:directory-1', 'fsync:directory-1', 'close:directory-1', 'open:temporary', 'write:temporary', 'fsync:temporary', 'close:temporary', 'open:directory-2', 'fsync:directory-2', 'close:directory-2', 'rename:manifest', 'open:directory-3', 'fsync:directory-3', 'close:directory-3', ]; test('syncs backup and directory before replacement, then syncs the renamed manifest directory', t => { const f = fixture(t, { dependencies: { 'dsh-web-mobile': old('dsh-web-mobile') } }); withSyncTrace(f, events => { const result = f.run(); assert.equal(result.changed, true); assert.deepEqual(events, durableWriteOrder); assert.equal(readFileSync(result.backupPath, 'utf8'), f.original); assert.equal(JSON.parse(readFileSync(f.path)).dependencies['dsh-web-mobile'], BUILTIN_DEPENDENCIES['dsh-web-mobile']); }); }); for (const target of ['backup', 'directory-1', 'temporary']) { test(`${target} fsync failure preserves the original manifest and does not rename`, t => { const f = fixture(t, { dependencies: { 'dsh-web-mobile': old('dsh-web-mobile') } }); const before = statSync(f.path); const failAt = `fsync:${target}`; withSyncTrace(f, events => { assert.throws(f.run, { message: `injected ${failAt} failure` }); assert.deepEqual(events, [ ...durableWriteOrder.slice(0, durableWriteOrder.indexOf(failAt) + 1), `close:${target}`, ]); }, failAt); assert.equal(readFileSync(f.path, 'utf8'), f.original); assert.equal(statSync(f.path).ino, before.ino); assert.equal(statSync(f.path).mtimeMs, before.mtimeMs); const backups = readdirSync(f.profile).filter(name => name.startsWith('package.json.backup-')); assert.equal(backups.length, 1); assert.equal(readFileSync(join(f.profile, backups[0]), 'utf8'), f.original); assert.deepEqual(readdirSync(f.profile).sort(), ['package.json', ...backups].sort()); }); } test('preserves deleted plugins, disabled bundles, custom dependencies and overrides', t => { const custom = { dependencies: { 'dsh-chat-manager': old('dsh-chat-manager'), 'dsh-web-mobile': '^3.0.0', other: old('other') }, devDependencies: { 'dsh-free-search': 'file:/custom/dsh-free-search-0.1.0.tgz' }, optionalDependencies: { 'dsh-preview-download': 'file:/usr/local/lib/dsh-mgr/not-the-same-1.0.0.tgz' }, pnpm: { overrides: { 'dsh-chat-manager>clsx': '^2.0.0', unrelated: 'custom' }, other: true }, dsh: { profile: { bundles: ['user-plugin'] } }, }; const f = fixture(t, custom); writeFileSync(join(f.profile, 'pnpm-workspace.yaml'), 'custom workspace'); writeFileSync(join(f.profile, 'cordis.patch.yml'), 'custom patch'); f.run(); const expected = structuredClone(custom); expected.dependencies['dsh-chat-manager'] = BUILTIN_DEPENDENCIES['dsh-chat-manager']; assert.deepEqual(JSON.parse(readFileSync(f.path)), expected); assert.equal(readFileSync(join(f.profile, 'pnpm-workspace.yaml'), 'utf8'), 'custom workspace'); assert.equal(readFileSync(join(f.profile, 'cordis.patch.yml'), 'utf8'), 'custom patch'); }); test('strict archive matching leaves non-semver references untouched', () => { for (const version of ['latest', '01.0.0', '1.0', '1.0.0-01', '1.0.0.tgz/extra']) { const input = { dependencies: { 'dsh-web-mobile': 'file:/usr/local/lib/dsh-mgr/dsh-web-mobile-' + version + '.tgz' } }; assert.equal(reconcileManifest(input).changed, false); } }); test('current defaults and user-only manifests remain byte-identical', t => { for (const manifest of [ { dependencies: BUILTIN_DEPENDENCIES, pnpm: { overrides: BUILTIN_OVERRIDES } }, { dependencies: { 'dsh-chat-manager': '^1.0.0' }, custom: true }, { dsh: { profile: { bundles: [] } } }, ]) { const f = fixture(t, manifest); assert.equal(f.run().changed, false); assert.equal(readFileSync(f.path, 'utf8'), f.original); assert.deepEqual(readdirSync(f.profile), ['package.json']); } }); test('missing archives fail before writes, even when already current', t => { for (const reference of [old('dsh-web-mobile'), BUILTIN_DEPENDENCIES['dsh-web-mobile']]) { const f = fixture(t, { dependencies: { 'dsh-web-mobile': reference } }); rmSync(join(f.archiveDirectory, basename(BUILTIN_DEPENDENCIES['dsh-web-mobile']))); assert.throws(f.run, /ENOENT/); assert.equal(readFileSync(f.path, 'utf8'), f.original); assert.deepEqual(readdirSync(f.profile), ['package.json']); } }); test('invalid JSON and malformed sections fail without changing original', t => { for (const input of ['{ broken', 'null', '[]', JSON.stringify({ dependencies: [] }), JSON.stringify({ pnpm: { overrides: null } })]) { const f = fixture(t, input); assert.throws(f.run); assert.equal(readFileSync(f.path, 'utf8'), input); assert.deepEqual(readdirSync(f.profile), ['package.json']); } }); test('rejects manifest/workspace symlinks, dangling links and directories', t => { for (const name of ['package.json', 'pnpm-workspace.yaml', 'pnpm-lock.yaml', '.dsh-lockfile-pending']) { for (const kind of ['symlink', 'dangling', 'directory']) { const f = fixture(t, { dependencies: { 'dsh-web-mobile': old('dsh-web-mobile') } }); const target = join(f.root, 'outside'); if (kind === 'symlink') writeFileSync(target, f.original); const path = join(f.profile, name); rmSync(path, { force: true }); if (kind === 'directory') mkdirSync(path); else symlinkSync(target, path); const before = readdirSync(f.profile); assert.throws(f.run, /unsafe profile file/); assert.deepEqual(readdirSync(f.profile), before); if (kind === 'symlink') assert.equal(readFileSync(target, 'utf8'), f.original); if (name !== 'package.json') assert.equal(readFileSync(f.path, 'utf8'), f.original); } } }); test('rejects symlinked profile and ancestors', t => { const f = fixture(t, '{}'); const link = join(f.root, 'linked'); symlinkSync(join(f.root, 'profiles'), link); assert.throws(() => reconcileProfile(join(link, 'web'), { archiveDirectory: f.archiveDirectory }), /unsafe profile directory/); const direct = join(f.root, 'direct'); symlinkSync(f.profile, direct); assert.throws(() => reconcileProfile(direct), /unsafe profile directory/); }); test('init migrates before restoring workspace and rejects unsafe workspace before writes', t => { const f = fixture(t, { optionalDependencies: { 'dsh-web-mobile': old('dsh-web-mobile') } }); const run = () => initProfile(f.root, { archiveDirectory: f.archiveDirectory }); symlinkSync(join(f.root, 'missing'), join(f.profile, 'pnpm-workspace.yaml')); assert.throws(run, /unsafe profile file/); assert.equal(readFileSync(f.path, 'utf8'), f.original); rmSync(join(f.profile, 'pnpm-workspace.yaml')); run(); assert.equal(JSON.parse(readFileSync(f.path)).optionalDependencies['dsh-web-mobile'], BUILTIN_DEPENDENCIES['dsh-web-mobile']); assert.match(readFileSync(join(f.profile, 'pnpm-workspace.yaml'), 'utf8'), /autoInstallPeers: false/); });