feat(runtime): upgrade DSH and bundled plugins with startup migration

This commit is contained in:
2026-10-18 02:14:27 +08:00
parent eb7e809999
commit 2e31e74972
18 changed files with 501 additions and 80 deletions
+30 -12
View File
@@ -2,27 +2,38 @@ 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 { join, basename } from 'node:path';
import { execFileSync } from 'node:child_process';
import { initProfile } from './init-profile.mjs';
import { BUILTIN_DEPENDENCIES, BUILTIN_OVERRIDES } from './reconcile-profile.mjs';
function archives(home) {
const archiveDirectory = join(home, 'archives');
mkdirSync(archiveDirectory, { recursive: true });
for (const value of [...Object.values(BUILTIN_DEPENDENCIES), ...Object.values(BUILTIN_OVERRIDES)]) {
writeFileSync(join(archiveDirectory, basename(value)), 'fixture');
}
return { archiveDirectory };
}
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}});
const options = archives(home);
const run = () => initProfile(home, options);
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.1.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).dependencies['dsh-chat-manager'], 'file:/usr/local/lib/dsh-mgr/dsh-chat-manager-1.5.16-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).dependencies['dsh-free-search'], 'file:/usr/local/lib/dsh-mgr/dsh-free-search-0.9.2.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');
assert.equal(JSON.parse(original).dependencies['dsh-web-mobile'], 'file:/usr/local/lib/dsh-mgr/dsh-web-mobile-3.1.0.tgz');
const workspace = join(home, 'profiles/web/pnpm-workspace.yaml');
assert.match(readFileSync(workspace, 'utf8'), /autoInstallPeers: false/);
assert.match(readFileSync(workspace, 'utf8'), /nodeLinker: hoisted/);
@@ -51,20 +62,27 @@ test('new profile enables bundle once and preserves existing configuration', ()
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', () => {
test('chat-manager-only restored profile gets overrides and offline peer settings', () => {
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' } });
const manifest = JSON.stringify({ dependencies: { 'dsh-chat-manager': 'file:/usr/local/lib/dsh-mgr/dsh-chat-manager-1.5.16-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);
initProfile(home, archives(home));
assert.deepEqual(JSON.parse(readFileSync(join(profile, 'package.json'))), { ...JSON.parse(manifest), pnpm: { overrides: { 'dsh-chat-manager>clsx': BUILTIN_OVERRIDES['dsh-chat-manager>clsx'] } } });
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];
const predicate = entrypoint.match(/if node -e '([^']+)'/)[1].replace('/usr/local/lib/dsh-mgr/reconcile-profile.mjs', new URL('./reconcile-profile.mjs', import.meta.url).href);
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' } }));
for (const custom of [
{ dependencies: { 'dsh-chat-manager': BUILTIN_DEPENDENCIES['dsh-chat-manager'] }, pnpm: { overrides: { 'dsh-chat-manager>clsx': '^2.0.0' } } },
{ dependencies: { 'dsh-chat-manager': '^1.5.16' }, optionalDependencies: { 'dsh-chat-manager': BUILTIN_DEPENDENCIES['dsh-chat-manager'] } },
]) {
writeFileSync(join(profile, 'package.json'), JSON.stringify(custom));
assert.throws(() => execFileSync(process.execPath, ['-e', predicate], { env: { ...process.env, DSH_HOME: home }, stdio: 'pipe' }));
}
writeFileSync(join(profile, 'package.json'), JSON.stringify({ dependencies: { 'dsh-chat-manager': '^1.5.16' } }));
assert.throws(() => execFileSync(process.execPath, ['-e', predicate], { env: { ...process.env, DSH_HOME: home }, stdio: 'pipe' }));
} finally { rmSync(home, { recursive: true, force: true }); }
});
@@ -74,7 +92,7 @@ test('nonempty legacy profiles are not automatically injected', () => {
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 } });
initProfile(home, archives(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 }); }