import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync, symlinkSync, existsSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { execFileSync } from 'node:child_process'; test('new profile enables bundle once and preserves existing configuration', () => { const home = mkdtempSync(join(tmpdir(), 'dsh-plugin-init-')); try { const run = () => execFileSync(process.execPath, [new URL('./init-profile.mjs', import.meta.url).pathname], {env:{...process.env, DSH_HOME:home}}); run(); const file=join(home,'profiles/web/package.json'); const original=readFileSync(file,'utf8'); assert.equal(JSON.parse(original).dependencies['dsh-preview-download'], 'file:/usr/local/lib/dsh-mgr/dsh-preview-download-1.0.0.tgz'); assert.equal(JSON.parse(original).dsh.profile.bundles.filter(name => name === 'dsh-preview-download').length, 1); assert.ok(JSON.parse(original).dsh.profile.bundles.includes('dsh-chat-manager')); assert.equal(JSON.parse(original).dependencies['dsh-chat-manager'], 'file:/usr/local/lib/dsh-mgr/dsh-chat-manager-1.5.12-dsh-mgr.1.tgz'); assert.equal(JSON.parse(original).pnpm.overrides['dsh-chat-manager>clsx'], 'file:/usr/local/lib/dsh-mgr/clsx-2.1.1.tgz'); assert.ok(JSON.parse(original).dsh.profile.bundles.includes('dsh-web-mobile')); assert.ok(JSON.parse(original).dsh.profile.bundles.includes('dsh-free-search')); assert.ok(JSON.parse(original).dsh.profile.bundles.includes('dsh-mgr-workspace')); assert.equal(JSON.parse(original).dependencies['dsh-mgr-workspace'], 'file:/usr/local/lib/dsh-mgr/dsh-mgr-workspace-1.0.0.tgz'); assert.equal(JSON.parse(original).dependencies['dsh-free-search'], 'file:/usr/local/lib/dsh-mgr/dsh-free-search-0.8.5.tgz'); assert.equal(JSON.parse(original).pnpm.overrides['dsh-free-search>@deepseek-ai/schemastery'], 'file:/usr/local/lib/dsh-mgr/schemastery-3.18.4.tgz'); assert.equal(JSON.parse(original).dependencies['dsh-web-mobile'], 'file:/usr/local/lib/dsh-mgr/dsh-web-mobile-3.0.5.tgz'); const workspace = join(home, 'profiles/web/pnpm-workspace.yaml'); assert.match(readFileSync(workspace, 'utf8'), /autoInstallPeers: false/); assert.match(readFileSync(workspace, 'utf8'), /nodeLinker: hoisted/); run();assert.equal(readFileSync(file,'utf8'),original); const legacy = JSON.parse(original); delete legacy.dependencies['dsh-mgr-workspace']; delete legacy.dependencies['dsh-chat-manager']; delete legacy.pnpm.overrides['dsh-chat-manager>clsx']; legacy.dsh.profile.bundles = legacy.dsh.profile.bundles.filter(name => name !== 'dsh-chat-manager'); legacy.dsh.profile.bundles = legacy.dsh.profile.bundles.filter(name => name !== 'dsh-mgr-workspace'); writeFileSync(file, JSON.stringify(legacy)); run(); assert.deepEqual(JSON.parse(readFileSync(file)), legacy); const disabled = JSON.parse(original); disabled.dsh.profile.bundles = disabled.dsh.profile.bundles.filter(name => name !== 'dsh-chat-manager'); disabled.dsh.profile.bundles = disabled.dsh.profile.bundles.filter(name => name !== 'dsh-web-mobile'); writeFileSync(file, JSON.stringify(disabled)); writeFileSync(workspace, 'custom workspace'); const patch = join(home, 'profiles/web/cordis.patch.yml'); writeFileSync(patch, '[]'); run(); assert.deepEqual(JSON.parse(readFileSync(file)), disabled); assert.equal(readFileSync(workspace, 'utf8'), 'custom workspace'); assert.equal(readFileSync(patch, 'utf8'), '[]'); const custom=JSON.stringify({custom:true}); writeFileSync(file,custom);run();assert.equal(readFileSync(file,'utf8'),custom); } finally { rmSync(home,{recursive:true,force:true}); } }); test('chat-manager-only restored profile gets offline peer settings without manifest changes', () => { const home = mkdtempSync(join(tmpdir(), 'dsh-chat-restore-')); try { const profile = join(home, 'profiles/web'); mkdirSync(profile, { recursive: true }); const manifest = JSON.stringify({ dependencies: { 'dsh-chat-manager': 'file:/usr/local/lib/dsh-mgr/dsh-chat-manager-1.5.12-dsh-mgr.1.tgz' } }); writeFileSync(join(profile, 'package.json'), manifest); execFileSync(process.execPath, [new URL('./init-profile.mjs', import.meta.url).pathname], { env: { ...process.env, DSH_HOME: home } }); assert.equal(readFileSync(join(profile, 'package.json'), 'utf8'), manifest); assert.match(readFileSync(join(profile, 'pnpm-workspace.yaml'), 'utf8'), /autoInstallPeers: false/); const entrypoint = readFileSync(new URL('./entrypoint.sh', import.meta.url), 'utf8'); const predicate = entrypoint.match(/if node -e '([^']+)'/)[1]; execFileSync(process.execPath, ['-e', predicate], { env: { ...process.env, DSH_HOME: home } }); writeFileSync(join(profile, 'package.json'), JSON.stringify({ dependencies: { 'dsh-chat-manager': '^1.5.12' } })); assert.throws(() => execFileSync(process.execPath, ['-e', predicate], { env: { ...process.env, DSH_HOME: home }, stdio: 'pipe' })); } finally { rmSync(home, { recursive: true, force: true }); } }); test('nonempty legacy profiles are not automatically injected', () => { const home = mkdtempSync(join(tmpdir(), 'dsh-plugin-legacy-')); try { const profile = join(home, 'profiles/web'); mkdirSync(profile, { recursive: true }); writeFileSync(join(profile, 'cordis.patch.yml'), '[]'); execFileSync(process.execPath, [new URL('./init-profile.mjs', import.meta.url).pathname], { env: { ...process.env, DSH_HOME: home } }); assert.equal(existsSync(join(profile, 'package.json')), false); assert.equal(existsSync(join(profile, 'pnpm-workspace.yaml')), false); } finally { rmSync(home, { recursive: true, force: true }); } }); test('rejects symlinked profile ancestors', () => { const home=mkdtempSync(join(tmpdir(),'dsh-plugin-init-')); const outside=mkdtempSync(join(tmpdir(),'dsh-plugin-outside-')); try { symlinkSync(outside,join(home,'profiles')); assert.throws(()=>execFileSync(process.execPath,[new URL('./init-profile.mjs',import.meta.url).pathname],{env:{...process.env,DSH_HOME:home},stdio:'pipe'})); assert.equal(existsSync(join(outside,'web/package.json')),false); } finally {rmSync(home,{recursive:true,force:true});rmSync(outside,{recursive:true,force:true});} });