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 }) }