From 557fa1790720c76f7bb6a0bf7de6fad2aa9b97b2 Mon Sep 17 00:00:00 2001 From: Cloga Chen Date: Sun, 25 Oct 2026 09:49:11 +0800 Subject: [PATCH 1/6] test: guard packed plugin installation and host loading (#448) Validate isolated release archive installs and regress missing runtime dependencies in development links. Full validation belongs to GitHub CI; local registry access is not an acceptance gate. Assisted-by: GitHub Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 6 + README.md | 9 + README.zh.md | 6 + package.json | 1 + scripts/verify-agent-contract.mjs | 2 +- scripts/verify-install-smoke.mjs | 362 +++++++++++++++++++++++++++ tests/scripts/install-smoke.test.mjs | 91 +++++++ 7 files changed, 476 insertions(+), 1 deletion(-) create mode 100644 scripts/verify-install-smoke.mjs create mode 100644 tests/scripts/install-smoke.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f71fc8b..5e3decf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,6 +132,12 @@ jobs: version="$(node -p "require('./package.json').version")" pnpm verify:tarball -- "artifacts/dsh-github-copilot-$version.tgz" + - name: Install verified archive and load it through the pinned Core host + shell: bash + run: | + version="$(node -p "require('./package.json').version")" + pnpm verify:install-smoke -- --archive "artifacts/dsh-github-copilot-$version.tgz" --host-modules node_modules + published-adapter: name: Published adapter (${{ matrix.os }}) strategy: diff --git a/README.md b/README.md index a341a14..b2dffed 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,15 @@ node package/scripts/check-search-composition.mjs --profile-dir /absolute/profil Supply any launcher patches with repeated `--patch /absolute/file`. Require `supported: true`: the install command does not automatically run this preflight. Unsupported/custom Web composition and `NONEMPTY_DISPOSABLE_PROFILE_ROOT` are stop conditions. Preserve a nonempty root and review whether public composition reconstructs it before any separately approved normalization; never assume it can be cleared. Check effective shared-peer resolution so stale profile-local packages do not shadow Desktop's official peers. +Install the exact, checksum-verified release archive shown below; do not use a +checkout directory as a release package. `pnpm add ` creates a +`link:` development dependency and does not install that directory's production +dependencies. For development, install the checkout's dependencies before +linking it and keep the checkout available at the linked path. A package manager +showing **Installed** confirms its profile record, not that runtime dependencies +resolved or that the plugin loaded; verify the exact DSH/profile startup result +separately. + For a **standalone named profile**: ```sh diff --git a/README.zh.md b/README.zh.md index 9990193..db7ed5c 100644 --- a/README.zh.md +++ b/README.zh.md @@ -132,6 +132,12 @@ node package/scripts/check-search-composition.mjs --profile-dir /absolute/profil 启动 patch 用重复的 `--patch /absolute/file` 提供。必须得到 `supported: true`;安装器不会自动执行此预检。不支持的/自定义 Web 组合及 `NONEMPTY_DISPOSABLE_PROFILE_ROOT` 均须停止。先保全非空 root,并核对公开组合能否完整重建,再另行批准规范化;不能默认清空。检查实际共享 peer 解析,避免旧 profile-local 包遮蔽官方 Desktop peer。 +请安装下方的精确版本、checksum 已核验的发布归档;不要把 checkout +目录当作发布包。`pnpm add ` 会创建 `link:` 开发依赖,不会替该 +目录安装生产依赖。开发时应先安装 checkout 的依赖再建立链接,并确保链接目标持续可用。 +包管理器显示 **Installed** 只表示 profile 记录已接受,不代表运行依赖已解析或插件已成功加载; +还应单独核对目标 DSH/profile 的启动结果。 + **独立具名 profile**: ```sh diff --git a/package.json b/package.json index addc01d..4d34313 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,7 @@ "preflight:search": "node scripts/check-search-composition.mjs", "verify:agent": "node scripts/verify-agent-contract.mjs", "verify:tarball": "node scripts/verify-tarball.mjs", + "verify:install-smoke": "node scripts/verify-install-smoke.mjs", "verify:baseline": "node scripts/verify-deployment-baseline.mjs", "verify:package": "node scripts/verify-package.mjs", "verify:controlled-core": "node scripts/verify-controlled-core.mjs", diff --git a/scripts/verify-agent-contract.mjs b/scripts/verify-agent-contract.mjs index c16e965..823b673 100644 --- a/scripts/verify-agent-contract.mjs +++ b/scripts/verify-agent-contract.mjs @@ -131,7 +131,7 @@ export async function verifyAgentContract(root = repositoryRoot) { require(Array.isArray(task.read) && task.read.length > 0 && Array.isArray(task.tests), `${name} needs read/tests lists`) for (const path of [...task.read, ...task.tests]) await file(path) } - for (const script of ['agent:describe', 'agent:doctor', 'agent:plan', 'verify:agent', 'test:scripts', 'typecheck:tests', 'verify:tarball']) { + for (const script of ['agent:describe', 'agent:doctor', 'agent:plan', 'verify:agent', 'test:scripts', 'typecheck:tests', 'verify:tarball', 'verify:install-smoke']) { require(typeof pkg.scripts[script] === 'string', `missing package script ${script}`) } validateIterationReview(JSON.parse(await readFile(resolve(root, review.report), 'utf8'))) diff --git a/scripts/verify-install-smoke.mjs b/scripts/verify-install-smoke.mjs new file mode 100644 index 0000000..7b6abc4 --- /dev/null +++ b/scripts/verify-install-smoke.mjs @@ -0,0 +1,362 @@ +import { spawnSync } from 'node:child_process' +import { open, mkdtemp, mkdir, readFile, realpath, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path' +import { createHash } from 'node:crypto' +import { fileURLToPath } from 'node:url' +import { readTarball, verifyTarball } from './verify-tarball.mjs' + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const slash = value => value.replaceAll('\\', '/') + +function inside(parent, path) { + const suffix = relative(parent, path) + return suffix === '' || suffix !== '..' && !suffix.startsWith(`..${sep}`) && !isAbsolute(suffix) +} + +function versionParts(value) { + const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/u.exec(value) + if (!match) throw new Error(`Unsupported package version ${JSON.stringify(value)}`) + return match.slice(1, 4).map(Number) +} + +export function versionSatisfiesPeer(actual, range) { + return range.split(/\s+\|\|\s+/u).some(candidate => { + if (!candidate.startsWith('^')) return candidate === actual + const expected = versionParts(candidate.slice(1)) + const found = versionParts(actual) + const minimum = found[0] > expected[0] + || (found[0] === expected[0] && (found[1] > expected[1] + || (found[1] === expected[1] && found[2] >= expected[2]))) + if (!minimum) return false + if (expected[0] > 0) return found[0] === expected[0] + if (expected[1] > 0) return found[0] === 0 && found[1] === expected[1] + return found[0] === 0 && found[1] === 0 && found[2] === expected[2] + }) +} + +async function packageAt(nodeModules, name) { + const directory = join(nodeModules, ...name.split('/')) + try { + const manifestPath = join(directory, 'package.json') + const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) + if (manifest.name !== name || typeof manifest.version !== 'string') return undefined + return { directory: await realpath(directory), manifest } + } catch (error) { + if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return undefined + throw error + } +} + +export async function hostDependencies(pkg, hostModules, runtimeMissing, { provideRuntimeDependencies = true } = {}) { + const dependencies = {} + const hostPeers = {} + for (const [name, range] of Object.entries(pkg.peerDependencies ?? {})) { + const optional = pkg.peerDependenciesMeta?.[name]?.optional === true + const found = await packageAt(hostModules, name) + if (found === undefined) { + if (optional) { + hostPeers[name] = { available: false, optional: true } + continue + } + throw new Error(`MISSING_HOST_PEER: ${name}@${range}`) + } + if (!versionSatisfiesPeer(found.manifest.version, range)) { + throw new Error(`HOST_PEER_VERSION_MISMATCH: ${name}@${found.manifest.version} does not satisfy ${range}`) + } + dependencies[name] = `link:${slash(found.directory)}` + hostPeers[name] = { available: true, version: found.manifest.version, path: found.directory, optional } + } + + for (const name of provideRuntimeDependencies ? Object.keys(pkg.dependencies ?? {}) : []) { + if (runtimeMissing === name) continue + const found = await packageAt(hostModules, name) + if (found === undefined) throw new Error(`MISSING_PLUGIN_RUNTIME_DEPENDENCY_SOURCE: ${name}`) + if (!versionSatisfiesPeer(found.manifest.version, pkg.dependencies[name])) { + throw new Error(`PLUGIN_RUNTIME_DEPENDENCY_VERSION_MISMATCH: ${name}@${found.manifest.version}`) + } + dependencies[name] = `link:${slash(found.directory)}` + } + + const react = await packageAt(hostModules, 'react') + if (react !== undefined && dependencies.react === undefined) dependencies.react = `link:${slash(react.directory)}` + return { dependencies, hostPeers } +} + +async function writeTree(files, destination) { + for (const [name, bytes] of files) { + if (name === 'package/package.json') continue + const output = join(destination, ...name.slice('package/'.length).split('/')) + if (!inside(destination, output)) throw new Error('Archive entry escaped fixture directory') + await mkdir(dirname(output), { recursive: true }) + await writeFile(output, bytes, { flag: 'wx' }) + } + await writeFile(join(destination, 'package.json'), files.get('package/package.json'), { flag: 'wx' }) +} + +function pnpmCommand() { + const cli = process.env.npm_execpath + if (typeof cli !== 'string' || cli.length === 0) { + throw new Error('Run through the repository-pinned pnpm command so npm_execpath is available') + } + if (process.platform === 'win32' && /\.exe$/iu.test(cli)) return { command: cli, args: [] } + return { command: process.execPath, args: [cli] } +} + +async function readVerifiedArchive(path, expectedSha256) { + const handle = await open(path, 'r') + try { + const info = await handle.stat() + if (!info.isFile() || info.size > 32 * 1024 * 1024) throw new Error('Archive is not a bounded regular file') + const bytes = Buffer.alloc(info.size + 1) + let length = 0 + while (length < bytes.length) { + const { bytesRead } = await handle.read(bytes, length, bytes.length - length, length) + if (bytesRead === 0) break + length += bytesRead + } + if (length !== info.size) throw new Error('Archive changed while being read') + const archive = bytes.subarray(0, length) + if (createHash('sha256').update(archive).digest('hex') !== expectedSha256) { + throw new Error('Archive changed after tarball verification') + } + return archive + } finally { + await handle.close() + } +} + +function runPnpm(directory, args) { + const pnpm = pnpmCommand() + const result = spawnSync(pnpm.command, [...pnpm.args, ...args], { + cwd: directory, + stdio: 'inherit', + env: { ...process.env, npm_config_ignore_scripts: 'true' }, + timeout: 180_000, + }) + if (result.error) throw result.error + if (result.status !== 0) throw new Error(`Isolated package install failed with exit code ${result.status}`) +} + +async function installFixture(directory, name, dependencies) { + await mkdir(directory, { recursive: true }) + await writeFile(join(directory, 'package.json'), `${JSON.stringify({ + name, + private: true, + type: 'module', + dependencies, + }, null, 2)}\n`, { flag: 'wx' }) + runPnpm(directory, ['install', '--ignore-scripts', '--strict-peer-dependencies', '--config.auto-install-peers=false']) +} + +const runtimeSmoke = `import assert from 'node:assert/strict' +import { readFile } from 'node:fs/promises' +import { createRequire, findPackageJSON } from 'node:module' +import { dirname, isAbsolute, relative, resolve, sep } from 'node:path' +import { pathToFileURL } from 'node:url' +import vm from 'node:vm' +import { realpath } from 'node:fs/promises' +import { Context } from '@deepseek-ai/cordis' +import { WebRuntime } from '@deepseek-ai/dsh-web' +import * as main from 'dsh-github-copilot' +import * as webDelegate from 'dsh-github-copilot/web-delegate' +import RoutedWeb from 'dsh-github-copilot/routed-web' +import recovery from 'dsh-github-copilot/manual-compaction-recovery' +import remote from 'dsh-github-copilot/remote' +const require = createRequire(import.meta.url) +const pkgPath = findPackageJSON('dsh-github-copilot', import.meta.url) +assert.ok(pkgPath, 'installed package manifest must resolve') +const pkg = JSON.parse(await readFile(pkgPath, 'utf8')) +const installRoot = dirname(pkgPath) +const allowedRoot = process.env.INSTALL_FIXTURE_ROOT +const realPackageRoot = await realpath(installRoot) +const realFixtureRoot = await realpath(allowedRoot) +const packageRelative = relative(realFixtureRoot, realPackageRoot) +assert.ok(packageRelative === '' || packageRelative !== '..' && !packageRelative.startsWith('..' + sep) + && !isAbsolute(packageRelative), 'tarball installation must remain inside the isolated fixture') +assert.equal(pkg.name, 'dsh-github-copilot') +function packageEntry(manifestPath, manifest) { + const rootExport = manifest.exports?.['.'] ?? manifest.exports + const target = typeof rootExport === 'string' ? rootExport + : rootExport?.import ?? rootExport?.node ?? rootExport?.default ?? manifest.main + assert.equal(typeof target, 'string', 'runtime dependency must expose a Node entry') + const path = resolve(dirname(manifestPath), target) + const packageRoot = dirname(manifestPath) + const relativePath = relative(packageRoot, path) + assert.ok(relativePath === '' || relativePath !== '..' && !relativePath.startsWith('..' + sep) + && !isAbsolute(relativePath), 'runtime dependency entry must stay inside its installed package') + return pathToFileURL(path).href +} +assert.equal(typeof main.apply, 'function') +assert.ok(Array.isArray(main.inject) && main.inject.includes('credentials')) +assert.equal(typeof webDelegate.apply, 'function') +assert.deepEqual(webDelegate.inject, ['web']) +assert.equal(typeof RoutedWeb, 'function') +assert.ok(RoutedWeb.inject.includes('githubCopilotOriginalWeb')) +assert.equal(typeof recovery, 'function') +assert.ok(Array.isArray(remote.descriptors) && remote.descriptors.length > 0) +for (const name of Object.keys(pkg.dependencies)) { + const manifestPath = findPackageJSON(name, pathToFileURL(pkgPath).href) + assert.ok(manifestPath, 'production dependency must actually resolve: ' + name) + const physicalDependencyManifest = await realpath(manifestPath) + const dependencyRelative = relative(realFixtureRoot, physicalDependencyManifest) + assert.ok(dependencyRelative === '' || dependencyRelative !== '..' && !dependencyRelative.startsWith('..' + sep) + && !isAbsolute(dependencyRelative), 'production dependency must come from fixture install: ' + name) + const installed = JSON.parse(await readFile(manifestPath, 'utf8')) + assert.equal(installed.version, pkg.dependencies[name], 'production dependency version must match: ' + name) + await import(packageEntry(manifestPath, installed)) +} +for (const [name, expected] of Object.entries(JSON.parse(process.env.EXPECTED_HOST_PEERS))) { + const manifestPath = findPackageJSON(name, pathToFileURL(pkgPath).href) + if (!expected.available) { + assert.equal(manifestPath, undefined, 'missing optional peer must remain host-optional: ' + name) + continue + } + assert.ok(manifestPath, 'required host peer must actually resolve: ' + name) + const actual = JSON.parse(await readFile(manifestPath, 'utf8')) + assert.equal(actual.version, expected.version, 'host peer must resolve to the supplied Core package: ' + name) + assert.equal(await realpath(dirname(manifestPath)), expected.path, + 'host peer must resolve to the explicit Core package path: ' + name) +} +const clientUrl = import.meta.resolve('dsh-github-copilot/client') +let clientRegistration +vm.runInNewContext(await readFile(new URL(clientUrl), 'utf8'), { + window: { __ModuleLoader__: { load(value) { clientRegistration = value } } }, +}) +assert.equal(clientRegistration?.id, pkg.name) +assert.equal(typeof clientRegistration?.factory, 'function') +const client = clientRegistration.factory(specifier => { + if (specifier === 'react') return require('react') + throw new Error('unexpected client external: ' + specifier) +}) +assert.equal(typeof client.apply, 'function') +assert.ok(Array.isArray(client.inject)) + +const context = new Context() +context.provide('credentials', Object.freeze({})) +const mainFiber = context.plugin(main, {}) +await mainFiber +const webContext = new Context() +const isolatedWeb = webContext.isolate('web', Symbol('install-smoke')) +const originalWebFiber = isolatedWeb.plugin(WebRuntime, { searchProvider: undefined, fetchProvider: undefined }) +await originalWebFiber +const delegateFiber = isolatedWeb.plugin(webDelegate) +await delegateFiber +assert.equal(isolatedWeb.get('githubCopilotOriginalWeb'), isolatedWeb.get('web')) +const routedFiber = webContext.plugin(RoutedWeb, { searchProvider: undefined, fetchProvider: undefined }) +await routedFiber +for (const fiber of [routedFiber, delegateFiber, originalWebFiber, mainFiber]) await fiber.dispose() +console.log(JSON.stringify({ ok: true, hostLoading: true, packageVersion: pkg.version, + entrypoints: ['.', './client', './remote', './routed-web', './web-delegate', './manual-compaction-recovery'], + providerRequests: 0, credentialsAccessed: false })) +` + +export function missingDependencyProbe(packageName, entrypoint) { + return `for (const specifier of [${JSON.stringify(entrypoint)}, ${JSON.stringify(packageName)}]) { + let missing = false + try { await import(specifier) } + catch (error) { + missing = error?.code === 'ERR_MODULE_NOT_FOUND' && String(error.message).includes(${JSON.stringify(packageName)}) + if (!missing) { + console.error(error?.code ?? 'ERROR', error?.message ?? String(error)); process.exit(3) + } + } + if (!missing) process.exit(2) +} +console.log('EXPECTED_MISSING_RUNTIME_DEPENDENCY:' + ${JSON.stringify(packageName)}) +` +} + +async function expectLinkedDependencyFailure({ directory, name, dependencies, sourceDirectory, omitted, entrypoint }) { + dependencies['dsh-github-copilot'] = `link:${slash(sourceDirectory)}` + await installFixture(directory, name, dependencies) + const probeFile = join(directory, 'expect-missing.mjs') + await writeFile(probeFile, missingDependencyProbe(omitted, entrypoint), { flag: 'wx' }) + const result = spawnSync(process.execPath, [probeFile], { + cwd: directory, encoding: 'utf8', timeout: 30_000, + }) + if (result.error) throw result.error + if (result.status !== 0 || !result.stdout.includes(`EXPECTED_MISSING_RUNTIME_DEPENDENCY:${omitted}`)) { + throw new Error(`Directory-link regression was not detected for ${omitted}:\n${result.stderr || result.stdout}`) + } + return { case: name, status: 'rejected', missing: omitted } +} + +export async function verifyInstalledPlugin({ archive, hostModules }) { + const archivePath = resolve(archive) + const hostRoot = await realpath(resolve(hostModules)) + const tarball = await verifyTarball(archivePath, root) + const archiveBytes = await readVerifiedArchive(archivePath, tarball.sha256) + const files = readTarball(archiveBytes) + const pkg = JSON.parse(files.get('package/package.json').toString('utf8')) + const coreRelease = pkg.engines?.dsh + if (typeof coreRelease !== 'string' || coreRelease !== '0.2.1-rc.3') { + throw new Error(`Install smoke is pinned to DSH 0.2.1-rc.3; got ${JSON.stringify(coreRelease)}`) + } + const scratch = await mkdtemp(join(tmpdir(), 'dsh-copilot-install-smoke-')) + try { + const validDirectory = join(scratch, 'packed-install') + const valid = await hostDependencies(pkg, hostRoot, undefined, { provideRuntimeDependencies: false }) + valid.dependencies[pkg.name] = `file:${slash(archivePath)}` + await installFixture(validDirectory, 'dsh-copilot-packed-install-smoke', valid.dependencies) + const runner = join(validDirectory, 'load-host.mjs') + await writeFile(runner, runtimeSmoke, { flag: 'wx' }) + const result = spawnSync(process.execPath, [runner], { + cwd: validDirectory, encoding: 'utf8', timeout: 60_000, + env: { ...process.env, INSTALL_FIXTURE_ROOT: validDirectory, + EXPECTED_HOST_PEERS: JSON.stringify(valid.hostPeers) }, + }) + if (result.error) throw result.error + if (result.status !== 0) { + throw new Error(`Packed archive failed the target Core host load smoke:\n${result.stderr || result.stdout}`) + } + + const negative = [] + for (const omitted of ['zod', '@earendil-works/pi-ai']) { + const directory = join(scratch, `directory-link-missing-${omitted.replaceAll('/', '-')}`) + const sourceDirectory = join(directory, 'directory-link-source') + await mkdir(sourceDirectory, { recursive: true }) + await writeTree(files, sourceDirectory) + const fixture = await hostDependencies(pkg, hostRoot, omitted) + negative.push(await expectLinkedDependencyFailure({ + directory, + name: `dsh-copilot-link-missing-${omitted.replaceAll('/', '-')}`, + dependencies: fixture.dependencies, + sourceDirectory, + omitted, + entrypoint: omitted === 'zod' ? 'dsh-github-copilot/remote' : 'dsh-github-copilot', + })) + } + return { schemaVersion: 1, ok: true, kind: 'packed-plugin-install-smoke', + package: { name: pkg.name, version: pkg.version }, coreRelease, archiveSha256: tarball.sha256, + hostPeers: Object.fromEntries(Object.entries(valid.hostPeers).map(([name, peer]) => + [name, peer.available ? { available: true, version: peer.version } : peer])), + installedEntrypoints: ['.', './client', './remote', './routed-web', './web-delegate', './manual-compaction-recovery'], + negativeDirectoryLinks: negative, evidence: 'Installs the verified archive, checks runtime dependency and peer resolution, and loads plugin entries through the pinned Cordis host API. No credentials or provider requests.' } + } finally { + await rm(scratch, { recursive: true, force: true }) + } +} + +function parseArgs(args) { + const options = {} + if (args.length % 2 !== 0) throw new Error('usage: verify-install-smoke --archive --host-modules ') + for (let index = 0; index < args.length; index += 2) { + const key = args[index] + if (!['--archive', '--host-modules'].includes(key) || options[key] !== undefined || !args[index + 1]) { + throw new Error('invalid install smoke arguments') + } + options[key] = args[index + 1] + } + if (!options['--archive'] || !options['--host-modules']) throw new Error('missing install smoke arguments') + return { archive: options['--archive'], hostModules: options['--host-modules'] } +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + try { + console.log(JSON.stringify(await verifyInstalledPlugin(parseArgs(process.argv.slice(2).filter(arg => arg !== '--'))), null, 2)) + } catch (error) { + console.error(error.message) + process.exitCode = 1 + } +} diff --git a/tests/scripts/install-smoke.test.mjs b/tests/scripts/install-smoke.test.mjs new file mode 100644 index 0000000..03e85b6 --- /dev/null +++ b/tests/scripts/install-smoke.test.mjs @@ -0,0 +1,91 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { fileURLToPath } from 'node:url' +import { spawnSync } from 'node:child_process' +import { hostDependencies, missingDependencyProbe, versionSatisfiesPeer } from '../../scripts/verify-install-smoke.mjs' + +test('generated missing-dependency probe executes and rejects unexpected success or another missing package', () => { + const run = (dependency, entry) => spawnSync(process.execPath, ['--input-type=module', '--eval', + missingDependencyProbe(dependency, entry)], { encoding: 'utf8' }) + const missing = 'copilot-install-smoke-nonexistent-dependency' + const failure = run(missing, `${missing}/remote`) + assert.equal(failure.status, 0, failure.stderr) + assert.match(failure.stdout, /EXPECTED_MISSING_RUNTIME_DEPENDENCY/u) + assert.equal(run(missing, 'node:fs').status, 2) + assert.equal(run(missing, 'another-copilot-smoke-missing-package').status, 3) +}) + +async function packageAt(root, name, version) { + const directory = join(root, ...name.split('/')) + await mkdir(directory, { recursive: true }) + await writeFile(join(directory, 'package.json'), JSON.stringify({ name, version })) + return directory +} + +test('host peer graph distinguishes exact host packages from missing optional peers', async () => { + const root = await mkdtemp(join(tmpdir(), 'copilot-install-smoke-')) + try { + const modules = join(root, 'node_modules') + await packageAt(modules, '@deepseek-ai/cordis', '4.0.5') + await packageAt(modules, '@deepseek-ai/dsh-client-ui-slots', '0.2.1-rc.3') + await packageAt(modules, 'zod', '4.4.3') + const result = await hostDependencies({ + dependencies: { zod: '4.4.3' }, + peerDependencies: { + '@deepseek-ai/cordis': '4.0.5', + '@deepseek-ai/dsh-client-ui-slots': '0.2.1-rc.3', + '@deepseek-ai/dsh-web-search-deepseek': '0.2.1-rc.3', + }, + peerDependenciesMeta: { + '@deepseek-ai/dsh-client-ui-slots': { optional: true }, + '@deepseek-ai/dsh-web-search-deepseek': { optional: true }, + }, + }, modules) + + assert.equal(result.hostPeers['@deepseek-ai/cordis'].available, true) + assert.equal(result.hostPeers['@deepseek-ai/cordis'].version, '4.0.5') + assert.equal(result.hostPeers['@deepseek-ai/dsh-client-ui-slots'].available, true) + assert.match(result.dependencies['@deepseek-ai/dsh-client-ui-slots'], /^link:/u) + assert.equal(result.hostPeers['@deepseek-ai/dsh-web-search-deepseek'].available, false) + assert.equal(result.dependencies['@deepseek-ai/dsh-web-search-deepseek'], undefined) + assert.match(result.dependencies.zod, /^link:/u) + assert.equal(versionSatisfiesPeer('4.0.5', '^4.0.0'), true) + assert.equal(versionSatisfiesPeer('5.0.0', '^4.0.0'), false) + const packedArchive = await hostDependencies({ + dependencies: { zod: '4.4.3' }, + peerDependencies: { '@deepseek-ai/cordis': '4.0.5' }, + }, modules, undefined, { provideRuntimeDependencies: false }) + assert.equal(packedArchive.dependencies.zod, undefined, + 'the positive smoke must let the packed archive install its own production dependencies') + } finally { + await rm(root, { recursive: true, force: true }) + } +}) + +test('a required host peer that is absent is not treated as a plugin production dependency', async () => { + const root = await mkdtemp(join(tmpdir(), 'copilot-install-smoke-')) + try { + await assert.rejects(hostDependencies({ + dependencies: {}, + peerDependencies: { '@deepseek-ai/cordis': '4.0.5' }, + }, join(root, 'node_modules')), /MISSING_HOST_PEER: @deepseek-ai\/cordis/u) + } finally { + await rm(root, { recursive: true, force: true }) + } +}) + +test('CI installs the verified archive through the isolated pinned-host smoke', async () => { + const root = join(fileURLToPath(new URL('../..', import.meta.url))) + const workflow = await readFile(join(root, '.github/workflows/ci.yml'), 'utf8') + const smoke = await readFile(join(root, 'scripts/verify-install-smoke.mjs'), 'utf8') + const archiveVerification = workflow.indexOf('pnpm verify:tarball --') + const installSmoke = workflow.indexOf('pnpm verify:install-smoke -- --archive') + assert.ok(archiveVerification >= 0 && installSmoke > archiveVerification) + assert.ok(workflow.includes('--host-modules node_modules')) + assert.ok(workflow.includes('Install verified archive and load it through the pinned Core host')) + assert.doesNotMatch(smoke, /--registry=|registry\.npmjs\.org/u, + 'the smoke must retain the registry configured for its execution environment') +}) From 5f698484a5d5887ab5fa6ae5dbf4937382a438bf Mon Sep 17 00:00:00 2001 From: Cloga Chen Date: Sun, 25 Oct 2026 09:58:13 +0800 Subject: [PATCH 2/6] test: resolve packed dependencies from pnpm physical package root (#448) Use the real package manifest path for Node dependency lookup and distinguish absent optional peers using the actual missing-module result. Assisted-by: GitHub Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- scripts/verify-install-smoke.mjs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/verify-install-smoke.mjs b/scripts/verify-install-smoke.mjs index 7b6abc4..0445222 100644 --- a/scripts/verify-install-smoke.mjs +++ b/scripts/verify-install-smoke.mjs @@ -164,8 +164,9 @@ import RoutedWeb from 'dsh-github-copilot/routed-web' import recovery from 'dsh-github-copilot/manual-compaction-recovery' import remote from 'dsh-github-copilot/remote' const require = createRequire(import.meta.url) -const pkgPath = findPackageJSON('dsh-github-copilot', import.meta.url) -assert.ok(pkgPath, 'installed package manifest must resolve') +const resolvedPackage = findPackageJSON('dsh-github-copilot', import.meta.url) +assert.ok(resolvedPackage, 'installed package manifest must resolve') +const pkgPath = await realpath(resolvedPackage) const pkg = JSON.parse(await readFile(pkgPath, 'utf8')) const installRoot = dirname(pkgPath) const allowedRoot = process.env.INSTALL_FIXTURE_ROOT @@ -207,11 +208,13 @@ for (const name of Object.keys(pkg.dependencies)) { await import(packageEntry(manifestPath, installed)) } for (const [name, expected] of Object.entries(JSON.parse(process.env.EXPECTED_HOST_PEERS))) { - const manifestPath = findPackageJSON(name, pathToFileURL(pkgPath).href) if (!expected.available) { - assert.equal(manifestPath, undefined, 'missing optional peer must remain host-optional: ' + name) + assert.throws(() => findPackageJSON(name, pathToFileURL(pkgPath).href), + error => error?.code === 'ERR_MODULE_NOT_FOUND' && error.message.includes(name), + 'missing optional peer must remain host-optional: ' + name) continue } + const manifestPath = findPackageJSON(name, pathToFileURL(pkgPath).href) assert.ok(manifestPath, 'required host peer must actually resolve: ' + name) const actual = JSON.parse(await readFile(manifestPath, 'utf8')) assert.equal(actual.version, expected.version, 'host peer must resolve to the supplied Core package: ' + name) From 18ba5cd26632e8cb5542f04c4476806b92bede32 Mon Sep 17 00:00:00 2001 From: Cloga Chen Date: Sun, 25 Oct 2026 10:05:29 +0800 Subject: [PATCH 3/6] test: honor native Web config and avoid inspecting Cordis service proxies (#448) Pass the pinned public Volatile config shape and verify service availability without comparing traced service proxy identities. Keep entry-specific causes for host loading failures. Assisted-by: GitHub Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- scripts/verify-install-smoke.mjs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/scripts/verify-install-smoke.mjs b/scripts/verify-install-smoke.mjs index 0445222..2650047 100644 --- a/scripts/verify-install-smoke.mjs +++ b/scripts/verify-install-smoke.mjs @@ -237,17 +237,19 @@ assert.ok(Array.isArray(client.inject)) const context = new Context() context.provide('credentials', Object.freeze({})) -const mainFiber = context.plugin(main, {}) -await mainFiber +async function mount(owner, plugin, config, entry) { + try { return await owner.plugin(plugin, config) } + catch (cause) { throw new Error('Host loading failed for ' + entry, { cause }) } +} +const mainFiber = await mount(context, main, {}, '.') const webContext = new Context() const isolatedWeb = webContext.isolate('web', Symbol('install-smoke')) -const originalWebFiber = isolatedWeb.plugin(WebRuntime, { searchProvider: undefined, fetchProvider: undefined }) -await originalWebFiber -const delegateFiber = isolatedWeb.plugin(webDelegate) -await delegateFiber -assert.equal(isolatedWeb.get('githubCopilotOriginalWeb'), isolatedWeb.get('web')) -const routedFiber = webContext.plugin(RoutedWeb, { searchProvider: undefined, fetchProvider: undefined }) -await routedFiber +const webConfig = () => ({ searchProvider: { get: () => undefined }, fetchProvider: { get: () => undefined } }) +const originalWebFiber = await mount(isolatedWeb, WebRuntime, webConfig(), 'native WebRuntime') +const delegateFiber = await mount(isolatedWeb, webDelegate, undefined, './web-delegate') +assert.ok(isolatedWeb.get('githubCopilotOriginalWeb') !== undefined, 'Web delegate must publish the native service') +const routedFiber = await mount(webContext, RoutedWeb, webConfig(), './routed-web') +assert.ok(webContext.get('web') !== undefined, 'routed Web entry must publish its facade') for (const fiber of [routedFiber, delegateFiber, originalWebFiber, mainFiber]) await fiber.dispose() console.log(JSON.stringify({ ok: true, hostLoading: true, packageVersion: pkg.version, entrypoints: ['.', './client', './remote', './routed-web', './web-delegate', './manual-compaction-recovery'], From 75c497494b8226b5f68d4a78482742a020b6d067 Mon Sep 17 00:00:00 2001 From: Cloga Chen Date: Sun, 25 Oct 2026 10:13:28 +0800 Subject: [PATCH 4/6] test: let Cordis materialize native volatile Web configuration (#448) Supply raw configuration at Context.plugin admission; the native schema constructs the Volatile handles before invoking WebRuntime. Assisted-by: GitHub Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- scripts/verify-install-smoke.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/verify-install-smoke.mjs b/scripts/verify-install-smoke.mjs index 2650047..69292bf 100644 --- a/scripts/verify-install-smoke.mjs +++ b/scripts/verify-install-smoke.mjs @@ -244,7 +244,7 @@ async function mount(owner, plugin, config, entry) { const mainFiber = await mount(context, main, {}, '.') const webContext = new Context() const isolatedWeb = webContext.isolate('web', Symbol('install-smoke')) -const webConfig = () => ({ searchProvider: { get: () => undefined }, fetchProvider: { get: () => undefined } }) +const webConfig = () => ({ searchProvider: undefined, fetchProvider: undefined }) const originalWebFiber = await mount(isolatedWeb, WebRuntime, webConfig(), 'native WebRuntime') const delegateFiber = await mount(isolatedWeb, webDelegate, undefined, './web-delegate') assert.ok(isolatedWeb.get('githubCopilotOriginalWeb') !== undefined, 'Web delegate must publish the native service') From dc58d251c7a481057b7aa131536134826532593d Mon Sep 17 00:00:00 2001 From: Cloga Chen Date: Sun, 25 Oct 2026 10:21:49 +0800 Subject: [PATCH 5/6] ci: keep installation smoke outside published package metadata (#448) Invoke the repository-only verifier through pinned pnpm exec, preserving the original package manifest and prepared alpha.3 release. This internal validation change does not require a product release or a release-policy exception. Assisted-by: GitHub Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- package.json | 1 - scripts/verify-agent-contract.mjs | 2 +- tests/scripts/install-smoke.test.mjs | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e3decf..775ca81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,7 +136,7 @@ jobs: shell: bash run: | version="$(node -p "require('./package.json').version")" - pnpm verify:install-smoke -- --archive "artifacts/dsh-github-copilot-$version.tgz" --host-modules node_modules + pnpm exec node scripts/verify-install-smoke.mjs --archive "artifacts/dsh-github-copilot-$version.tgz" --host-modules node_modules published-adapter: name: Published adapter (${{ matrix.os }}) diff --git a/package.json b/package.json index 4d34313..addc01d 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,6 @@ "preflight:search": "node scripts/check-search-composition.mjs", "verify:agent": "node scripts/verify-agent-contract.mjs", "verify:tarball": "node scripts/verify-tarball.mjs", - "verify:install-smoke": "node scripts/verify-install-smoke.mjs", "verify:baseline": "node scripts/verify-deployment-baseline.mjs", "verify:package": "node scripts/verify-package.mjs", "verify:controlled-core": "node scripts/verify-controlled-core.mjs", diff --git a/scripts/verify-agent-contract.mjs b/scripts/verify-agent-contract.mjs index 823b673..c16e965 100644 --- a/scripts/verify-agent-contract.mjs +++ b/scripts/verify-agent-contract.mjs @@ -131,7 +131,7 @@ export async function verifyAgentContract(root = repositoryRoot) { require(Array.isArray(task.read) && task.read.length > 0 && Array.isArray(task.tests), `${name} needs read/tests lists`) for (const path of [...task.read, ...task.tests]) await file(path) } - for (const script of ['agent:describe', 'agent:doctor', 'agent:plan', 'verify:agent', 'test:scripts', 'typecheck:tests', 'verify:tarball', 'verify:install-smoke']) { + for (const script of ['agent:describe', 'agent:doctor', 'agent:plan', 'verify:agent', 'test:scripts', 'typecheck:tests', 'verify:tarball']) { require(typeof pkg.scripts[script] === 'string', `missing package script ${script}`) } validateIterationReview(JSON.parse(await readFile(resolve(root, review.report), 'utf8'))) diff --git a/tests/scripts/install-smoke.test.mjs b/tests/scripts/install-smoke.test.mjs index 03e85b6..f6d7159 100644 --- a/tests/scripts/install-smoke.test.mjs +++ b/tests/scripts/install-smoke.test.mjs @@ -82,7 +82,7 @@ test('CI installs the verified archive through the isolated pinned-host smoke', const workflow = await readFile(join(root, '.github/workflows/ci.yml'), 'utf8') const smoke = await readFile(join(root, 'scripts/verify-install-smoke.mjs'), 'utf8') const archiveVerification = workflow.indexOf('pnpm verify:tarball --') - const installSmoke = workflow.indexOf('pnpm verify:install-smoke -- --archive') + const installSmoke = workflow.indexOf('pnpm exec node scripts/verify-install-smoke.mjs --archive') assert.ok(archiveVerification >= 0 && installSmoke > archiveVerification) assert.ok(workflow.includes('--host-modules node_modules')) assert.ok(workflow.includes('Install verified archive and load it through the pinned Core host')) From 91fd181cb455202c55cbda286f55db2bacf3acb9 Mon Sep 17 00:00:00 2001 From: Cloga Chen Date: Sun, 25 Oct 2026 10:29:07 +0800 Subject: [PATCH 6/6] ci: support pinned pnpm exec without lifecycle-only environment (#448) Resolve pnpm from the runner PATH when npm_execpath is absent and require its exact repository-declared version before fixture installation. Windows invokes the native command shim with fixed arguments. Assisted-by: GitHub Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- scripts/verify-install-smoke.mjs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/verify-install-smoke.mjs b/scripts/verify-install-smoke.mjs index 69292bf..7677a94 100644 --- a/scripts/verify-install-smoke.mjs +++ b/scripts/verify-install-smoke.mjs @@ -97,7 +97,9 @@ async function writeTree(files, destination) { function pnpmCommand() { const cli = process.env.npm_execpath if (typeof cli !== 'string' || cli.length === 0) { - throw new Error('Run through the repository-pinned pnpm command so npm_execpath is available') + return process.platform === 'win32' + ? { command: process.env.ComSpec ?? 'cmd.exe', args: ['/d', '/s', '/c', 'pnpm'] } + : { command: 'pnpm', args: [] } } if (process.platform === 'win32' && /\.exe$/iu.test(cli)) return { command: cli, args: [] } return { command: process.execPath, args: [cli] } @@ -288,6 +290,15 @@ async function expectLinkedDependencyFailure({ directory, name, dependencies, so } export async function verifyInstalledPlugin({ archive, hostModules }) { + const manager = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')).packageManager + const pnpm = pnpmCommand() + const managerVersion = spawnSync(pnpm.command, [...pnpm.args, '--version'], { + cwd: root, encoding: 'utf8', timeout: 30_000, + }) + if (managerVersion.error) throw managerVersion.error + if (managerVersion.status !== 0 || `pnpm@${managerVersion.stdout.trim()}` !== manager) { + throw new Error(`Install smoke requires the repository-pinned package manager ${manager}`) + } const archivePath = resolve(archive) const hostRoot = await realpath(resolve(hostModules)) const tarball = await verifyTarball(archivePath, root)