mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 03:03:24 +08:00
Add profile-scoped npm updates for Desktop and Web, protect source installations, and keep restarts manual. Cover stale status recovery, request races, and dialog focus with regression tests. Verified with npm run check (1626 tests) and isolated Desktop/Web npm installation flows. Refs #61
366 lines
16 KiB
JavaScript
366 lines
16 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { mkdir, mkdtemp, readFile, realpath, rm, symlink, unlink, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { PassThrough } from 'node:stream';
|
|
import test from 'node:test';
|
|
import { pathToFileURL } from 'node:url';
|
|
|
|
import { createUpdateRuntime, NPM_REGISTRY, PACKAGE_NAME } from '../plugin-src/host/update-runtime.mjs';
|
|
|
|
async function json(filename, value) {
|
|
await mkdir(join(filename, '..'), { recursive: true });
|
|
await writeFile(filename, JSON.stringify(value));
|
|
}
|
|
|
|
async function plugin(directory, version = '3.0.8') {
|
|
await mkdir(join(directory, 'lib'), { recursive: true });
|
|
await json(join(directory, 'package.json'), {
|
|
name: PACKAGE_NAME,
|
|
version,
|
|
main: './lib/index.js',
|
|
exports: { '.': './lib/index.js', './client': './lib/client.js' },
|
|
dsh: { bundle: { patch: './cordis.patch.yml' } },
|
|
});
|
|
await Promise.all(['lib/index.js', 'lib/client.js', 'cordis.patch.yml']
|
|
.map((entry) => writeFile(join(directory, entry), '')));
|
|
}
|
|
|
|
function operation({ output = '', errors = '', exitCode = 0, signal, pending = false, onCancel } = {}) {
|
|
const stdout = new PassThrough();
|
|
const stderr = new PassThrough();
|
|
let finish;
|
|
let closed = false;
|
|
const done = new Promise((resolve) => { finish = resolve; });
|
|
const settle = (code, terminationSignal = null) => {
|
|
if (closed) return;
|
|
closed = true;
|
|
signal?.removeEventListener('abort', cancel);
|
|
stdout.end(output);
|
|
stderr.end(errors);
|
|
finish({ exitCode: code, signal: terminationSignal });
|
|
};
|
|
const cancel = () => {
|
|
onCancel?.();
|
|
settle(null, 'SIGTERM');
|
|
};
|
|
signal?.addEventListener('abort', cancel, { once: true });
|
|
if (!pending) setImmediate(() => settle(exitCode));
|
|
return { stdout, stderr, done, cancel, terminate: cancel, waitForExit: () => done };
|
|
}
|
|
|
|
async function fixture(t, { desktop = false, name = 'update-test', hoisted = false } = {}) {
|
|
const root = await realpath(await mkdtemp(join(tmpdir(), 'dsh-im-update-runtime-')));
|
|
t.after(() => rm(root, { recursive: true, force: true }));
|
|
const homeDir = join(root, 'dsh-home');
|
|
const profileDir = join(homeDir, 'profiles', name);
|
|
const installedLink = join(profileDir, 'node_modules', PACKAGE_NAME);
|
|
const installedDir = hoisted ? installedLink
|
|
: join(profileDir, 'node_modules/.pnpm/@xmanrui+dsh-im@3.0.8/node_modules', PACKAGE_NAME);
|
|
await plugin(installedDir);
|
|
if (!hoisted) {
|
|
await mkdir(join(installedLink, '..'), { recursive: true });
|
|
await symlink(installedDir, installedLink, 'dir');
|
|
}
|
|
const manifest = { name: `dsh-profile-${name}`, private: true, dependencies: { [PACKAGE_NAME]: '3.0.8' } };
|
|
await json(join(profileDir, 'package.json'), manifest);
|
|
await writeFile(join(profileDir, 'pnpm-lock.yaml'), "lockfileVersion: '9.0'\n");
|
|
const cliRoot = join(root, 'harness');
|
|
const cliEntry = join(cliRoot, 'lib/bin.js');
|
|
await json(join(cliRoot, 'package.json'), { name: '@deepseek-ai/dsh', bin: { dsh: 'lib/bin.js' } });
|
|
await mkdir(join(cliEntry, '..'), { recursive: true });
|
|
await writeFile(cliEntry, '');
|
|
const calls = [];
|
|
let registry = '';
|
|
let installResult = {};
|
|
const ctx = {
|
|
subprocess: {
|
|
spawn(spec) {
|
|
calls.push(spec);
|
|
return operation({
|
|
...spec.argv.includes('config') ? { output: registry } : installResult,
|
|
signal: spec.signal,
|
|
});
|
|
},
|
|
},
|
|
};
|
|
const options = {
|
|
ctx,
|
|
env: { DSH_HOME: homeDir },
|
|
argv: [process.execPath, cliEntry, '--profile', name],
|
|
execArgv: [],
|
|
execPath: process.execPath,
|
|
cwd: root,
|
|
moduleUrl: pathToFileURL(join(installedDir, 'lib/index.js')).href,
|
|
platform: 'darwin',
|
|
electron: false,
|
|
};
|
|
if (desktop) {
|
|
const desktopRoot = join(root, 'desktop-app');
|
|
const bootstrapPath = join(desktopRoot, 'lib/desktop-cli.js');
|
|
await json(join(desktopRoot, 'package.json'), { name: 'dsh-plugin-desktop', version: '2.0.3' });
|
|
await mkdir(join(bootstrapPath, '..'), { recursive: true });
|
|
await writeFile(bootstrapPath, '');
|
|
ctx.desktopProfiles = { current: { name, dir: profileDir } };
|
|
ctx.desktopPnpmBootstrap = {
|
|
activeProfileName: name,
|
|
activeProfileDir: profileDir,
|
|
homeDir,
|
|
appExecutable: process.execPath,
|
|
dshBootstrapPath: bootstrapPath,
|
|
};
|
|
ctx.desktopPnpm = {
|
|
run(args, signal) {
|
|
calls.push({ method: 'run', args, signal });
|
|
return operation({ output: registry, signal });
|
|
},
|
|
runPlugin(args, cwd, signal) {
|
|
calls.push({ method: 'runPlugin', args, cwd, signal });
|
|
return operation({ ...installResult, signal });
|
|
},
|
|
};
|
|
options.electron = '43.4.0';
|
|
}
|
|
return {
|
|
root, homeDir, profileDir, installedDir, installedLink, manifest, cliEntry, ctx, options, calls,
|
|
runtime: () => createUpdateRuntime(options),
|
|
registry: (value) => { registry = value; },
|
|
installResult: (value) => { installResult = value; },
|
|
};
|
|
}
|
|
|
|
test('read-only inspection accepts normal pnpm symlinks and never starts a process', async (t) => {
|
|
const f = await fixture(t);
|
|
const info = await f.runtime().inspect();
|
|
assert.equal(info.profileName, 'update-test');
|
|
assert.equal(info.environmentKind, 'cli');
|
|
assert.equal(info.installedVersion, '3.0.8');
|
|
assert.equal(info.packageValid, true);
|
|
assert.equal(info.eligible, true);
|
|
assert.match(info.installationKey, /^[a-f0-9]{64}$/u);
|
|
assert.deepEqual(f.calls, []);
|
|
});
|
|
|
|
test('Desktop uses optional services and accepts the hoisted installation layout', async (t) => {
|
|
const f = await fixture(t, { desktop: true, hoisted: true });
|
|
const values = { ...f.ctx };
|
|
f.options.ctx = { get: (name) => values[name] };
|
|
const info = await f.runtime().inspect();
|
|
assert.equal(info.environmentKind, 'desktop');
|
|
assert.equal(info.profileName, 'update-test');
|
|
assert.equal(info.eligible, true);
|
|
assert.deepEqual(f.calls, []);
|
|
});
|
|
|
|
test('profile detection follows only launcher arguments and never guesses from cwd', async (t) => {
|
|
const f = await fixture(t, { name: 'web' });
|
|
for (const args of [['web', '--profile', 'wrong'], ['--profile=web', '--prompt', '--profile', 'wrong']]) {
|
|
f.options.argv = [process.execPath, f.cliEntry, ...args];
|
|
assert.equal((await f.runtime().inspect()).profileName, 'web');
|
|
}
|
|
f.options.argv = [process.execPath, f.cliEntry, '--prompt', '--profile', 'web'];
|
|
assert.equal((await f.runtime().inspect()).blockedReason, 'unknown-profile');
|
|
f.options.argv = [process.execPath, f.cliEntry, '--profile', '../web'];
|
|
assert.equal((await f.runtime().inspect()).blockedReason, 'unknown-profile');
|
|
});
|
|
|
|
test('local, Git, alias and external-directory installations cannot be replaced', async (t) => {
|
|
const f = await fixture(t);
|
|
for (const spec of ['link:/work/dsh-im', 'file:../dsh-im', 'github:xmanrui/dsh-im', 'git+ssh://git@example.com/repo.git', 'npm:another-plugin@1.0.0']) {
|
|
await json(join(f.profileDir, 'package.json'), { ...f.manifest, dependencies: { [PACKAGE_NAME]: spec } });
|
|
assert.equal((await f.runtime().inspect()).blockedReason, 'source-install', spec);
|
|
}
|
|
const sourceDir = join(f.root, 'source-checkout');
|
|
await plugin(sourceDir);
|
|
await unlink(f.installedLink);
|
|
await symlink(sourceDir, f.installedLink, 'dir');
|
|
await json(join(f.profileDir, 'package.json'), f.manifest);
|
|
f.options.moduleUrl = pathToFileURL(join(sourceDir, 'lib/index.js')).href;
|
|
assert.equal((await f.runtime().inspect()).blockedReason, 'source-install');
|
|
assert.deepEqual(f.calls, []);
|
|
});
|
|
|
|
test('a loaded plugin from another profile cannot authorize an installation', async (t) => {
|
|
const f = await fixture(t);
|
|
const otherPackage = join(f.root, 'other-profile-package');
|
|
await plugin(otherPackage);
|
|
f.options.moduleUrl = pathToFileURL(join(otherPackage, 'lib/index.js')).href;
|
|
assert.equal((await f.runtime().inspect()).blockedReason, 'installation-changed');
|
|
});
|
|
|
|
test('disk verification checks package identity and required entry files', async (t) => {
|
|
const f = await fixture(t);
|
|
const runtime = f.runtime();
|
|
assert.equal((await runtime.inspect()).packageValid, true);
|
|
await unlink(join(f.installedDir, 'lib/client.js'));
|
|
const missingClient = await runtime.inspect();
|
|
assert.equal(missingClient.packageValid, false);
|
|
assert.equal(missingClient.blockedReason, 'invalid-installation');
|
|
assert.equal(missingClient.installedVersion, '3.0.8');
|
|
});
|
|
|
|
test('inspection reads new disk versions and snapshots without require cache', async (t) => {
|
|
const f = await fixture(t);
|
|
const runtime = f.runtime();
|
|
const before = await runtime.inspect();
|
|
const newDir = join(f.profileDir, 'node_modules/.pnpm/@xmanrui+dsh-im@3.0.9/node_modules', PACKAGE_NAME);
|
|
await plugin(newDir, '3.0.9');
|
|
await unlink(f.installedLink);
|
|
await symlink(newDir, f.installedLink, 'dir');
|
|
const after = await runtime.inspect();
|
|
assert.equal(after.installedVersion, '3.0.9');
|
|
assert.equal(after.packageValid, true);
|
|
assert.equal(after.blockedReason, 'pending-restart');
|
|
assert.notEqual(after.installationKey, before.installationKey);
|
|
});
|
|
|
|
test('in-place Desktop replacement also requires restart until a new Host loads', async (t) => {
|
|
const f = await fixture(t, { desktop: true, hoisted: true });
|
|
const runtime = f.runtime();
|
|
await runtime.inspect();
|
|
await plugin(f.installedDir, '3.0.9');
|
|
assert.equal((await runtime.inspect()).blockedReason, 'pending-restart');
|
|
assert.equal((await f.runtime().inspect()).eligible, true);
|
|
});
|
|
|
|
test('manifest and lockfile changes invalidate an installation snapshot', async (t) => {
|
|
const f = await fixture(t);
|
|
const runtime = f.runtime();
|
|
const first = await runtime.inspect();
|
|
await writeFile(join(f.profileDir, 'pnpm-lock.yaml'), "lockfileVersion: '9.0'\n# changed\n");
|
|
const second = await runtime.inspect();
|
|
assert.notEqual(first.installationKey, second.installationKey);
|
|
await json(join(f.profileDir, 'package.json'), { ...f.manifest, dependencies: { ...f.manifest.dependencies, another: '1.0.0' } });
|
|
assert.notEqual(second.installationKey, (await runtime.inspect()).installationKey);
|
|
});
|
|
|
|
test('installation rejects changes after service confirmation before starting any process', async (t) => {
|
|
for (const desktop of [false, true]) {
|
|
const f = await fixture(t, { desktop });
|
|
const runtime = f.runtime();
|
|
const confirmed = await runtime.inspect();
|
|
await writeFile(join(f.profileDir, 'pnpm-lock.yaml'), "lockfileVersion: '9.0'\n# external change after confirmation\n");
|
|
await assert.rejects(runtime.install('3.0.9', { expectedInstallationKey: confirmed.installationKey }), {
|
|
code: 'installation-changed',
|
|
});
|
|
assert.deepEqual(f.calls, []);
|
|
}
|
|
});
|
|
|
|
test('Desktop refuses a mismatched bootstrap or a changed active profile', async (t) => {
|
|
const f = await fixture(t, { desktop: true });
|
|
const runtime = f.runtime();
|
|
assert.equal((await runtime.inspect()).eligible, true);
|
|
f.ctx.desktopPnpmBootstrap.activeProfileName = 'wrong';
|
|
assert.equal((await runtime.inspect()).blockedReason, 'executor-unavailable');
|
|
delete f.ctx.desktopPnpm;
|
|
assert.equal((await runtime.inspect()).eligible, false);
|
|
});
|
|
|
|
test('preflight validates the effective scoped registry without exposing credentials', async (t) => {
|
|
const f = await fixture(t, { desktop: true });
|
|
const runtime = f.runtime();
|
|
for (const output of ['', 'undefined\n', 'null\n', JSON.stringify(NPM_REGISTRY)]) {
|
|
f.registry(output);
|
|
assert.equal((await runtime.inspect({ preflight: true })).eligible, true);
|
|
}
|
|
for (const value of ['https://mirror.example/', 'https://user:password@registry.npmjs.org/', 'http://registry.npmjs.org/']) {
|
|
f.registry(JSON.stringify(value));
|
|
const result = await runtime.inspect({ preflight: true });
|
|
assert.equal(result.blockedReason, 'registry-conflict');
|
|
assert.doesNotMatch(JSON.stringify(result), /password|mirror\.example/u);
|
|
}
|
|
f.registry('not valid json');
|
|
assert.equal((await runtime.inspect({ preflight: true })).blockedReason, 'registry-check-failed');
|
|
assert.ok(f.calls.every((call) => call.method === 'run'));
|
|
});
|
|
|
|
test('Desktop installation reuses runPlugin with one pinned npm target and no restart', async (t) => {
|
|
const f = await fixture(t, { desktop: true });
|
|
let restartCalls = 0;
|
|
f.ctx.desktopProfiles.select = () => { restartCalls += 1; };
|
|
f.ctx.desktopActions = { restart: () => { restartCalls += 1; } };
|
|
const runtime = f.runtime();
|
|
const confirmed = await runtime.inspect();
|
|
assert.deepEqual(await runtime.install('3.0.9', { expectedInstallationKey: confirmed.installationKey }), { exitCode: 0, signal: null });
|
|
assert.equal(f.calls.length, 2);
|
|
assert.deepEqual(f.calls[0].args, ['config', 'get', '@xmanrui:registry', '--json']);
|
|
assert.deepEqual(f.calls[1].args, ['add', '-w', '--save-exact', `${PACKAGE_NAME}@3.0.9`, `--registry=${NPM_REGISTRY}`]);
|
|
assert.equal(f.calls[1].cwd, f.profileDir);
|
|
assert.equal(restartCalls, 0);
|
|
});
|
|
|
|
test('CLI installation uses the running Node and verified dsh entry, without shell interpolation', async (t) => {
|
|
const f = await fixture(t);
|
|
f.options.execArgv = ['--expose-internals'];
|
|
await f.runtime().install('3.0.9');
|
|
const [preflight, install] = f.calls;
|
|
assert.deepEqual(preflight.argv, ['pnpm', 'config', 'get', '@xmanrui:registry', '--json']);
|
|
assert.equal(preflight.cwd, f.profileDir);
|
|
assert.deepEqual(install.argv, [process.execPath, '--expose-internals', f.cliEntry, 'plugin', '--profile', 'update-test',
|
|
'add', '-w', '--save-exact', `${PACKAGE_NAME}@3.0.9`, `--registry=${NPM_REGISTRY}`]);
|
|
assert.equal(install.env.DSH_HOME, f.homeDir);
|
|
assert.equal(install.stdio.stdin, 'ignore');
|
|
assert.equal(install.env.CI, 'true');
|
|
assert.equal('shell' in install, false);
|
|
});
|
|
|
|
test('installation rejects ranges, prereleases and argument injection before starting any command', async (t) => {
|
|
const f = await fixture(t);
|
|
const runtime = f.runtime();
|
|
for (const version of ['latest', '^3.0.9', '3.0.9-beta.1', 'v3.0.9', '3.0.9; echo secret', undefined]) {
|
|
await assert.rejects(runtime.install(version), { code: 'invalid-version' });
|
|
}
|
|
assert.deepEqual(f.calls, []);
|
|
});
|
|
|
|
test('a registry conflict prevents the install command', async (t) => {
|
|
const f = await fixture(t, { desktop: true });
|
|
f.registry(JSON.stringify('https://mirror.example/'));
|
|
await assert.rejects(f.runtime().install('3.0.9'), { code: 'registry-conflict' });
|
|
assert.ok(f.calls.every((call) => call.method === 'run'));
|
|
});
|
|
|
|
test('nonzero installation exits return only safe diagnostics', async (t) => {
|
|
const f = await fixture(t, { desktop: true });
|
|
f.installResult({ exitCode: 1, errors: 'secret=abcd /Users/private ERR_PNPM_EACCES https://user:password@example.com/' });
|
|
await assert.rejects(f.runtime().install('3.0.9'), (error) => {
|
|
assert.equal(error.code, 'install-failed');
|
|
assert.equal(error.exitCode, 1);
|
|
assert.equal(error.diagnosticCode, 'ERR_PNPM_EACCES');
|
|
assert.doesNotMatch(JSON.stringify(error) + error.message, /abcd|private|password/u);
|
|
return true;
|
|
});
|
|
});
|
|
|
|
test('timeout and abort use the Host-owned cancellation and await exit', async (t) => {
|
|
for (const mode of ['timeout', 'abort']) {
|
|
const f = await fixture(t, { desktop: true });
|
|
let cancellations = 0;
|
|
f.installResult({ pending: true, onCancel: () => { cancellations += 1; } });
|
|
f.options.installTimeoutMs = mode === 'timeout' ? 20 : 10_000;
|
|
const controller = new AbortController();
|
|
const promise = f.runtime().install('3.0.9', { signal: controller.signal });
|
|
if (mode === 'abort') {
|
|
while (f.calls.length < 2) await new Promise((resolve) => setImmediate(resolve));
|
|
controller.abort();
|
|
}
|
|
await assert.rejects(promise, { code: mode === 'timeout' ? 'install-timeout' : 'install-interrupted' });
|
|
assert.ok(cancellations >= 1);
|
|
}
|
|
});
|
|
|
|
test('Windows CLI without a verified pnpm adapter fails closed', async (t) => {
|
|
const f = await fixture(t);
|
|
f.options.platform = 'win32';
|
|
assert.equal((await f.runtime().inspect()).blockedReason, 'executor-unavailable');
|
|
assert.deepEqual(f.calls, []);
|
|
});
|
|
|
|
test('package validation rejects entry files escaping the installed package', async (t) => {
|
|
const f = await fixture(t);
|
|
const metadata = JSON.parse(await readFile(join(f.installedDir, 'package.json'), 'utf8'));
|
|
metadata.exports['./client'] = '../../../../../outside.js';
|
|
await json(join(f.installedDir, 'package.json'), metadata);
|
|
assert.equal((await f.runtime().inspect()).packageValid, false);
|
|
});
|