From fd7511cae3247a4638311b21b500e7f4bb3662c0 Mon Sep 17 00:00:00 2001 From: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com> Date: Mon, 19 Oct 2026 15:36:56 +0800 Subject: [PATCH 1/2] fix(dsh): keep Windows version readback arguments single-line Signed-off-by: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com> --- .github/workflows/dsh-plugin.yml | 2 + packages/dsh-loopx-plugin/install.sh | 7 +-- .../smoke/dsh-installer-smoke.mjs | 51 ++++++++++++++++--- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/.github/workflows/dsh-plugin.yml b/.github/workflows/dsh-plugin.yml index f78bddb0ea..16ebb464ab 100644 --- a/.github/workflows/dsh-plugin.yml +++ b/.github/workflows/dsh-plugin.yml @@ -63,6 +63,8 @@ jobs: node -e 'require("node:fs").appendFileSync(process.env.GITHUB_ENV, "DSH_ARTIFACT=output/dsh-loopx-plugin-" + require("./package.json").version + ".tgz\n")' - name: Qualify the real package-name install and uninstall run: node smoke/dsh-registry-smoke.mjs --tarball "$DSH_ARTIFACT" + - name: Qualify the source installer and installed-version rejection + run: pnpm smoke:install - name: Qualify packed Client and real web runtime if: runner.os == 'Linux' run: | diff --git a/packages/dsh-loopx-plugin/install.sh b/packages/dsh-loopx-plugin/install.sh index 4439cbe14d..bb73d38519 100755 --- a/packages/dsh-loopx-plugin/install.sh +++ b/packages/dsh-loopx-plugin/install.sh @@ -138,12 +138,7 @@ printf '%s' "$profile_dump" | node -e ' exit 1 } -"$dsh_bin" plugin --profile "$PROFILE_NAME" exec node -e ' - const manifest = require("dsh-loopx-plugin/package.json") - if (manifest.name !== "dsh-loopx-plugin" || manifest.version !== process.argv[1]) { - throw new Error(`installed plugin version does not match ${process.argv[1]}`) - } -' "$PACKAGE_VERSION" || { +"$dsh_bin" plugin --profile "$PROFILE_NAME" exec node -e 'const manifest = require("dsh-loopx-plugin/package.json"); if (manifest.name !== "dsh-loopx-plugin" || manifest.version !== process.argv[1]) { throw new Error(`installed plugin version does not match ${process.argv[1]}`); }' "$PACKAGE_VERSION" || { echo 'install: DSH installed package did not match the built version' >&2 exit 1 } diff --git a/packages/dsh-loopx-plugin/smoke/dsh-installer-smoke.mjs b/packages/dsh-loopx-plugin/smoke/dsh-installer-smoke.mjs index 9caadcc921..7d754a3e5c 100644 --- a/packages/dsh-loopx-plugin/smoke/dsh-installer-smoke.mjs +++ b/packages/dsh-loopx-plugin/smoke/dsh-installer-smoke.mjs @@ -2,6 +2,8 @@ import assert from 'node:assert/strict' import { spawnSync } from 'node:child_process' +import { createHash } from 'node:crypto' +import { createRequire } from 'node:module' import { chmod, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { dirname, join, resolve } from 'node:path' @@ -9,7 +11,16 @@ import { fileURLToPath } from 'node:url' const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') const manifest = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8')) -const dshBin = process.env.DSH_BIN || join(packageRoot, 'node_modules', '.bin', 'dsh') +const dshBin = (process.env.DSH_BIN || join(packageRoot, 'node_modules', '.bin', 'dsh')) + .replaceAll('\\', '/') +const require = createRequire(import.meta.url) +// Windows cannot spawn the .bin shell shim directly. Match the registry smoke +// for direct DSH operations; the shipped installer still runs through Bash. +const dshScript = process.env.DSH_CLI_JS + || join(dirname(require.resolve('@deepseek-ai/dsh/package.json')), 'lib', 'bin.js') +const installerSource = await readFile(join(packageRoot, 'install.sh'), 'utf8') +const versionVerifier = installerSource.match(/exec node -e '([^']*)' "\$PACKAGE_VERSION"/u)?.[1] +assert(versionVerifier, 'installer must verify the installed package version') const args = process.argv.slice(2) assert(args.length === 0 || (args.length === 2 && args[0] === '--previous-tarball'), 'usage: dsh-installer-smoke.mjs [--previous-tarball PATH]') @@ -22,7 +33,7 @@ const installedManifest = join(home, 'profiles', 'web', 'node_modules', manifest function run(file, argv, environment = env) { return spawnSync(file, argv, { cwd: temp, env: environment, encoding: 'utf8', shell: false, - timeout: 180_000, maxBuffer: 8 * 1024 * 1024, + timeout: 600_000, maxBuffer: 8 * 1024 * 1024, }) } @@ -31,9 +42,13 @@ function expectSuccess(result) { assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`) } +function runDsh(argv) { + return run(process.execPath, [dshScript, ...argv]) +} + try { if (previousTarball) { - expectSuccess(run(dshBin, ['plugin', '--profile', 'web', 'add', previousTarball, + expectSuccess(runDsh(['plugin', '--profile', 'web', 'add', previousTarball, '--prefer-offline', '--ignore-scripts'])) const previous = JSON.parse(await readFile(installedManifest, 'utf8')) assert.equal(previous.name, manifest.name) @@ -45,11 +60,33 @@ try { const installed = run('bash', [join(packageRoot, 'install.sh')]) expectSuccess(installed) assert(installed.stdout.includes(`installed and verified ${manifest.name}@${manifest.version}`)) + process.stdout.write(`${installed.stdout.split(/\r?\n/u) + .find(line => line.includes(`installed and verified ${manifest.name}@${manifest.version}`))}\n`) const installedText = await readFile(installedManifest, 'utf8') const readback = JSON.parse(installedText) assert.equal(readback.name, manifest.name) assert.equal(readback.version, manifest.version) + const profileBefore = runDsh(['--profile', 'web', '--dump-config']) + expectSuccess(profileBefore) + assert.deepEqual([...profileBefore.stdout.matchAll(/^\s*name:\s+(dsh-loopx-plugin(?:\/\S+)?)\s*$/gmu)] + .map(match => match[1]), [manifest.name, `${manifest.name}/init-command`, + `${manifest.name}/driver`, `${manifest.name}/observer`]) + expectSuccess(runDsh(['plugin', '--profile', 'web', 'exec', 'node', '-e', + versionVerifier, manifest.version])) + const staleVersion = '0.0.0-installer-smoke' + const stale = runDsh(['plugin', '--profile', 'web', 'exec', 'node', '-e', + versionVerifier, staleVersion]) + assert.ifError(stale.error) + assert.notEqual(stale.status, 0, 'installed-version verifier accepted a stale version') + assert(`${stale.stdout}\n${stale.stderr}`.includes(`installed plugin version does not match ${staleVersion}`)) + assert.equal(await readFile(installedManifest, 'utf8'), installedText) + const profileAfter = runDsh(['--profile', 'web', '--dump-config']) + expectSuccess(profileAfter) + assert.equal(profileAfter.stdout, profileBefore.stdout, 'version verification changed the profile') + const profileHash = createHash('sha256').update(profileAfter.stdout).digest('hex') + process.stdout.write(`installed profile unchanged after version checks (sha256 ${profileHash})\n`) + // An incompatible version must fail before any DSH profile operation. The // shim represents only host version reporting, never a successful runtime. const unsupported = join(temp, 'unsupported-dsh') @@ -57,7 +94,7 @@ try { await writeFile(unsupported, `#!/bin/sh\nif [ "$1" = '--version' ]; then\n printf '%s\\n' '0.2.1-alpha.1'\nelse\n touch "$LOOPX_INSTALLER_SMOKE_MARKER"\n exit 1\nfi\n`) await chmod(unsupported, 0o755) const rejected = run('bash', [join(packageRoot, 'install.sh')], { - ...env, DSH_BIN: unsupported, LOOPX_INSTALLER_SMOKE_MARKER: marker, + ...env, DSH_BIN: unsupported.replaceAll('\\', '/'), LOOPX_INSTALLER_SMOKE_MARKER: marker, }) assert.ifError(rejected.error) assert.equal(rejected.status, 2) @@ -65,11 +102,11 @@ try { assert.equal(await stat(marker).then(() => true, () => false), false) assert.equal(await readFile(installedManifest, 'utf8'), installedText) - expectSuccess(run(dshBin, ['plugin', '--profile', 'web', 'remove', manifest.name])) - const removed = run(dshBin, ['--profile', 'web', '--dump-config']) + expectSuccess(runDsh(['plugin', '--profile', 'web', 'remove', manifest.name])) + const removed = runDsh(['--profile', 'web', '--dump-config']) expectSuccess(removed) assert(!removed.stdout.includes('name: dsh-loopx-plugin')) - process.stdout.write(`dsh-loopx installer smoke passed (${previousTarball ? 'upgrade' : 'fresh install'}, incompatible-host rejection, removal)\n`) + process.stdout.write(`dsh-loopx installer smoke passed (${previousTarball ? 'upgrade' : 'fresh install'}, stale-version rejection, incompatible-host rejection, removal)\n`) } finally { await rm(temp, { recursive: true, force: true }) } From a9e9629c3eb5adad6d029da4aa429103a4db0870 Mon Sep 17 00:00:00 2001 From: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:55:02 +0800 Subject: [PATCH 2/2] fix(dsh): preserve selected host through installer qualification Signed-off-by: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com> --- .github/workflows/dsh-plugin.yml | 4 +- .../dsh-installer-selected-host-smoke.mjs | 49 +++++++++++++++++++ .../smoke/dsh-installer-smoke.mjs | 10 ++-- 3 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 packages/dsh-loopx-plugin/smoke/dsh-installer-selected-host-smoke.mjs diff --git a/.github/workflows/dsh-plugin.yml b/.github/workflows/dsh-plugin.yml index f2643fd20f..919d7c123f 100644 --- a/.github/workflows/dsh-plugin.yml +++ b/.github/workflows/dsh-plugin.yml @@ -88,7 +88,9 @@ jobs: - name: Qualify the real package-name install and uninstall run: node smoke/dsh-registry-smoke.mjs --tarball "$DSH_ARTIFACT" - name: Qualify the source installer and installed-version rejection - run: pnpm smoke:install + run: | + pnpm smoke:install + node smoke/dsh-installer-selected-host-smoke.mjs - name: Qualify packed Client and real web runtime if: runner.os == 'Linux' run: | diff --git a/packages/dsh-loopx-plugin/smoke/dsh-installer-selected-host-smoke.mjs b/packages/dsh-loopx-plugin/smoke/dsh-installer-selected-host-smoke.mjs new file mode 100644 index 0000000000..0c49edf52b --- /dev/null +++ b/packages/dsh-loopx-plugin/smoke/dsh-installer-selected-host-smoke.mjs @@ -0,0 +1,49 @@ +#!/usr/bin/env node + +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const temp = await mkdtemp(join(tmpdir(), 'dsh-selected-host-')) +const selectedHost = join(temp, 'selected host') +const calls = join(temp, 'calls.log') +const slash = path => path.replaceAll('\\', '/') + +try { + // Delegate every real operation except removal. The complete installer smoke + // must observe this selected host's refusal rather than switch to a local CLI. + await writeFile(selectedHost, `#!/usr/bin/env bash +printf '%s\\n' "$*" >> "$DSH_SELECTED_HOST_CALLS" +if [[ "$1" == plugin && "$4" == remove ]]; then + echo selected-host-removal-refused >&2 + exit 37 +fi +exec "$DSH_SELECTED_HOST_DELEGATE" "$@" +`) + await chmod(selectedHost, 0o755) + const result = spawnSync(process.execPath, [join(packageRoot, 'smoke/dsh-installer-smoke.mjs')], { + cwd: temp, encoding: 'utf8', shell: false, + timeout: 900_000, maxBuffer: 8 * 1024 * 1024, + env: { + ...process.env, + DSH_BIN: slash(selectedHost), + DSH_SELECTED_HOST_CALLS: slash(calls), + DSH_SELECTED_HOST_DELEGATE: slash(process.env.DSH_BIN + || join(packageRoot, 'node_modules/.bin/dsh')), + }, + }) + assert.ifError(result.error) + const observed = await readFile(calls, 'utf8') + assert(observed.includes('plugin --profile web remove dsh-loopx-plugin'), + `the selected host did not receive removal (status ${result.status})\n${result.stdout}\n${result.stderr}`) + assert.notEqual(result.status, 0, 'the installer smoke falsely accepted selected-host failure') + assert(result.stderr.includes('selected-host-removal-refused'), result.stderr) + assert(!result.stdout.includes('dsh-loopx installer smoke passed')) + process.stdout.write('selected-host removal refusal correctly fails the complete installer smoke\n') +} finally { + await rm(temp, { recursive: true, force: true }) +} diff --git a/packages/dsh-loopx-plugin/smoke/dsh-installer-smoke.mjs b/packages/dsh-loopx-plugin/smoke/dsh-installer-smoke.mjs index 7d754a3e5c..803ba7efac 100644 --- a/packages/dsh-loopx-plugin/smoke/dsh-installer-smoke.mjs +++ b/packages/dsh-loopx-plugin/smoke/dsh-installer-smoke.mjs @@ -3,7 +3,6 @@ import assert from 'node:assert/strict' import { spawnSync } from 'node:child_process' import { createHash } from 'node:crypto' -import { createRequire } from 'node:module' import { chmod, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { dirname, join, resolve } from 'node:path' @@ -13,11 +12,6 @@ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') const manifest = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8')) const dshBin = (process.env.DSH_BIN || join(packageRoot, 'node_modules', '.bin', 'dsh')) .replaceAll('\\', '/') -const require = createRequire(import.meta.url) -// Windows cannot spawn the .bin shell shim directly. Match the registry smoke -// for direct DSH operations; the shipped installer still runs through Bash. -const dshScript = process.env.DSH_CLI_JS - || join(dirname(require.resolve('@deepseek-ai/dsh/package.json')), 'lib', 'bin.js') const installerSource = await readFile(join(packageRoot, 'install.sh'), 'utf8') const versionVerifier = installerSource.match(/exec node -e '([^']*)' "\$PACKAGE_VERSION"/u)?.[1] assert(versionVerifier, 'installer must verify the installed package version') @@ -43,7 +37,9 @@ function expectSuccess(result) { } function runDsh(argv) { - return run(process.execPath, [dshScript, ...argv]) + // Use the same selected executable as install.sh. Bash handles Windows .bin + // shims while "$@" preserves argument boundaries, including paths with spaces. + return run('bash', ['-c', 'exec "$@"', 'dsh-installer-smoke', dshBin, ...argv]) } try {