diff --git a/apps/cli/tests/profiles/fixtures/official-installer-observer.mjs b/apps/cli/tests/profiles/fixtures/official-installer-observer.mjs new file mode 100644 index 0000000000..4a39f07ff7 --- /dev/null +++ b/apps/cli/tests/profiles/fixtures/official-installer-observer.mjs @@ -0,0 +1,60 @@ +/** Observe ordinary manager operations in an installed Web profile without starting native agents. */ +import { readFile, readdir, stat } from 'node:fs/promises' +import { basename, join } from 'node:path' + +export const name = 'official-installer-observer' +export const inject = ['pluginManager', 'agentPresets', 'subagents', 'sessions', 'appReady'] + +export function apply(ctx, config) { + let active = true + const stop = ctx.appReady.onReady(() => { + void exercise(ctx, config).then( + value => { if (active) process.stdout.write(`PACKED_OFFICIAL_RESULT=${JSON.stringify({ value })}\n`) }, + error => { if (active) process.stdout.write(`PACKED_OFFICIAL_RESULT=${JSON.stringify({ error: String(error) })}\n`) }, + ) + }) + ctx.effect(() => () => { active = false; stop() }, 'packed-official-observer') +} + +function applied(stage, result) { + if (result.application !== 'applied') throw new Error(`${stage}: ${JSON.stringify(result)}`) +} + +async function exercise(ctx, config) { + const initial = await ctx.pluginManager.listBundles() + await fetch(`${config.registry}catalog-observed`) + const steps = [] + for (const name of config.packages) { + const spec = initial.find(bundle => bundle.name === name)?.installTarget?.spec + if (spec === undefined) throw new Error(`No catalog installation target for ${name}`) + const options = { enabled: true, saveExact: true, registry: config.registry } + let install = await ctx.pluginManager.installBundle(spec, options) + const approval = install.pendingBuilds + if (approval?.length) install = await ctx.pluginManager.installBundle(spec, { ...options, approvedBuilds: approval }) + applied(`install ${name}`, install) + const manifest = JSON.parse(await readFile(join(config.profile, 'package.json'), 'utf8')) + const installed = (await ctx.pluginManager.listBundles()).find(bundle => bundle.name === name) + const provider = name.endsWith('-codex') ? 'codex' : 'claude-code' + const providerEnabled = ctx.subagents.list().includes(provider) + const nativeDir = join(config.profile, 'node_modules', config.nativePackages[name]) + const binaryName = `${provider === 'codex' ? 'codex' : 'claude'}${process.platform === 'win32' ? '.exe' : ''}` + const nativeBinary = (await readdir(nativeDir, { recursive: true })).find(path => basename(path) === binaryName) + const nativeArtifact = nativeBinary !== undefined && (await stat(join(nativeDir, nativeBinary))).size > 0 + const presets = await ctx.agentPresets.list() + const beforeUnavailable = await readFile(join(config.profile, 'package.json'), 'utf8') + const unavailable = await ctx.pluginManager.installBundle(`${name}@9999.0.0`, options) + const unchangedAfterUnavailable = beforeUnavailable === await readFile(join(config.profile, 'package.json'), 'utf8') + const off = await ctx.pluginManager.setBundleEnabled(name, false) + applied(`disable ${name}`, off) + const disabled = (await ctx.pluginManager.listBundles()).find(bundle => bundle.name === name) + const on = await ctx.pluginManager.setBundleEnabled(name, true) + applied(`enable ${name}`, on) + const enabled = (await ctx.pluginManager.listBundles()).find(bundle => bundle.name === name) + const removed = await ctx.pluginManager.removeBundle(name) + applied(`remove ${name}`, removed) + const absent = (await ctx.pluginManager.listBundles()).find(bundle => bundle.name === name) + steps.push({ name, install, savedVersion: manifest.dependencies?.[name], installed, providerEnabled, nativeArtifact, presets, + unavailable, unchangedAfterUnavailable, off, disabled, on, enabled, removed, absent }) + } + return { initial, steps, sessionCount: ctx.sessions.list().length } +} diff --git a/apps/cli/tests/profiles/official-installer-packed.e2e.ts b/apps/cli/tests/profiles/official-installer-packed.e2e.ts new file mode 100644 index 0000000000..3bcf0664b7 --- /dev/null +++ b/apps/cli/tests/profiles/official-installer-packed.e2e.ts @@ -0,0 +1,206 @@ +/** Install actual packed Official providers through the ordinary manager in a source-independent Web process. */ +import { createHash, randomUUID } from 'node:crypto' +import { createReadStream, existsSync, globSync } from 'node:fs' +import { readFile, writeFile } from 'node:fs/promises' +import { createServer } from 'node:http' +import { join } from 'node:path' +import { createInterface } from 'node:readline' +import { fileURLToPath } from 'node:url' +import { execa } from 'execa' +import yaml from 'js-yaml' +import { expect, it, onTestFinished } from 'vitest' +import type { BundleInfo, ChangeResult } from '@deepseek-ai/dsh-plugin-manager' +import { packedManifest } from '../../../../scripts/release/tarball.ts' +import { assertInstalledTree, createPackedInstallation } from './packed-installation.ts' + +const root = fileURLToPath(new URL('../../../..', import.meta.url)) +const packages = ['@deepseek-ai/dsh-subagent-claude-code', '@deepseek-ai/dsh-subagent-codex'] +const built = existsSync(join(root, 'apps/cli/lib/bin.js')) + && packages.every(name => existsSync(join(root, 'packages/subagent', name.slice('@deepseek-ai/dsh-'.length), 'lib/index.js'))) + +interface Observation { + initial: BundleInfo[] + sessionCount: number + steps: Array<{ + name: string + install: ChangeResult + savedVersion: string + installed: BundleInfo + providerEnabled: boolean + nativeArtifact: boolean + presets: Array<{ id: string; broken?: string }> + unavailable: ChangeResult + unchangedAfterUnavailable: boolean + off: ChangeResult + disabled: BundleInfo + on: ChangeResult + enabled: BundleInfo + removed: ChangeResult + absent: BundleInfo + }> +} + +it.skipIf(!built)('installs exact Official versions and switches/removes them through the generic package manager', { + // Real tarball packing and isolated dependency installation own this budget; no native model request runs. + timeout: 360_000, + retry: 0, +}, async () => { + const fixture = await createPackedInstallation('dsh-official-packed-', packages) + const { temporary, installation, home, profile, environment, tarballs, members, externalVersions } = fixture + const manifests = new Map(packages.map((name) => { + const archive = tarballs.get(name) + if (archive === undefined) throw new Error(`missing provider archive ${name}`) + return [name, { archive, manifest: packedManifest(archive) }] + })) + const version = manifests.get(packages[0]!)?.manifest.version + if (typeof version !== 'string') throw new Error('packed provider has no version') + const integrity = new Map(await Promise.all([...manifests].map(async ([name, entry]) => + [name, `sha512-${createHash('sha512').update(await readFile(entry.archive)).digest('base64')}`] as const))) + const requests: string[] = [] + let requestsBeforeCatalog: string[] | undefined + let registry = '' + const namespace = `/${randomUUID()}/` + const server = createServer((request, response) => { + const pathname = new URL(request.url ?? '/', 'http://registry.invalid').pathname + if (!pathname.startsWith(namespace)) { response.writeHead(404).end(); return } + const path = decodeURIComponent(pathname.slice(namespace.length)) + if (path === 'catalog-observed') { + requestsBeforeCatalog = [...requests] + response.end('ok') + return + } + requests.push(path) + const archive = [...manifests].find(([name]) => path === `tarballs/${name}`)?.[1].archive + if (archive !== undefined) { + response.setHeader('content-type', 'application/octet-stream') + createReadStream(archive).on('error', error => response.destroy(error)).pipe(response) + return + } + const entry = manifests.get(path) + if (entry === undefined) { + // Only package publication is local: third-party metadata and payloads retain npm's real bytes. + response.writeHead(302, { location: `https://registry.npmjs.org/${request.url?.slice(namespace.length) ?? ''}` }).end() + return + } + response.setHeader('content-type', 'application/json') + response.end(JSON.stringify({ name: path, 'dist-tags': { latest: version }, time: { [version]: '2000-01-01T00:00:00.000Z' }, + versions: { [version]: { ...entry.manifest, dist: { tarball: `${registry}tarballs/${encodeURIComponent(path)}`, integrity: integrity.get(path) } } }, + })) + }) + onTestFinished(async () => { + server.closeAllConnections() + if (!server.listening) return + await new Promise((resolveClose, reject) => { + server.close((error) => { if (error) reject(error); else resolveClose() }) + }) + }) + await new Promise((resolveListen, reject) => { + server.once('error', reject) + server.listen(0, '127.0.0.1', () => { server.off('error', reject); resolveListen() }) + }) + const address = server.address() + if (address === null || typeof address === 'string') throw new Error('test registry has no TCP address') + registry = `http://127.0.0.1:${String(address.port)}${namespace}` + + const dependencies = new Set(packages) + for (const name of dependencies) { + const member = members.get(name) + if (member === undefined) throw new Error(`missing packed dependency ${name}`) + for (const [dependency, range] of Object.entries(member.manifest.dependencies as Record | undefined ?? {})) { + if (range.startsWith('workspace:')) dependencies.add(dependency) + } + } + for (const name of packages) { + dependencies.delete(name) + expect(existsSync(join(installation, 'node_modules', name))).toBe(false) + } + await writeFile(join(profile, 'package.json'), JSON.stringify({ name: 'packed-official-profile', private: true, dependencies: {}, + dsh: { profile: { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'] } }, + })) + const lock = yaml.load(await readFile(join(root, 'pnpm-lock.yaml'), 'utf8')) as { + packages: Record + snapshots: Record }> + } + const report = process.report.getReport() as { header: { glibcVersionRuntime?: string } } + const libc = process.platform === 'linux' ? report.header.glibcVersionRuntime === undefined ? 'musl' : 'glibc' : undefined + // The fixture owns one host installation; foreign native artifacts are never executed or required here. + const foreignPackages = new Set(Object.entries(lock.packages).filter(([, constraints]) => + ([['os', process.platform], ['cpu', process.arch], ['libc', libc]] as const).some(([field, current]) => { + const allowed = constraints[field] + return current !== undefined && allowed !== undefined && !allowed.includes(current) + && !allowed.includes('any') && allowed.every(value => !value.startsWith('!')) + })).map(([name]) => name)) + const ignoredOptionalDependencies = [...new Set(Object.values(lock.snapshots).flatMap(snapshot => + Object.entries(snapshot.optionalDependencies ?? {}) + .filter(([name, version]) => foreignPackages.has(`${name}@${version}`) || foreignPackages.has(version)).map(([name]) => name)))] + const nativePackages = { + '@deepseek-ai/dsh-subagent-claude-code': `@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}${libc === 'musl' ? '-musl' : ''}`, + '@deepseek-ai/dsh-subagent-codex': `@openai/codex-${process.platform}-${process.arch}`, + } + for (const name of Object.values(nativePackages)) expect(ignoredOptionalDependencies).not.toContain(name) + await writeFile(join(profile, 'pnpm-workspace.yaml'), yaml.dump({ packages: ['.'], nodeLinker: 'hoisted', autoInstallPeers: false, + ignoredOptionalDependencies, + overrides: { ...externalVersions, ...Object.fromEntries([...dependencies].map(name => [name, `file:${tarballs.get(name)!}`])) }, + })) + const externalScopes = new Set(Object.keys(externalVersions) + .filter(name => name.startsWith('@') && !name.startsWith('@deepseek-ai/')).map(name => name.slice(0, name.indexOf('/')))) + await writeFile(join(profile, '.npmrc'), [...externalScopes].map(scope => `${scope}:registry=https://registry.npmjs.org/`).join('\n') + '\n') + const userConfig = join(temporary, 'empty.npmrc') + await writeFile(userConfig, '') + environment.npm_config_userconfig = userConfig + const observer = join(temporary, 'official-installer-observer.mjs') + await writeFile(observer, await readFile(new URL('./fixtures/official-installer-observer.mjs', import.meta.url), 'utf8')) + const patch = join(temporary, 'official-installer.patch.yml') + await writeFile(patch, yaml.dump([{ insert: [{ id: 'official-installer-observer', name: observer, + config: { registry, profile, packages, nativePackages }, + }] }])) + const child = execa(process.execPath, [join(installation, 'node_modules/@deepseek-ai/dsh/lib/bin.js'), + 'web', '--patch', patch, '--no-open', '--port', '0'], { + cwd: temporary, env: environment, extendEnv: false, reject: false, timeout: 240_000, + }) + if (child.stdout === null) throw new Error('installed Web process has no stdout') + const lines = createInterface({ input: child.stdout }) + try { + const observed = await new Promise((resolveObservation, reject) => { + lines.on('line', (line) => { + if (!line.startsWith('PACKED_OFFICIAL_RESULT=')) return + const report = JSON.parse(line.slice('PACKED_OFFICIAL_RESULT='.length)) as { value?: Observation; error?: string } + if (report.error !== undefined) reject(new Error(report.error)) + else if (report.value !== undefined) resolveObservation(report.value) + }) + void child.then((result) => { reject(new Error(`Web process exited before observation:\n${result.stdout}\n${result.stderr}`)) }) + }) + expect(requestsBeforeCatalog).toEqual([]) + for (const name of packages) expect(observed.initial.find(entry => entry.name === name)).toMatchObject({ + official: true, availability: 'missing', installed: false, enabled: false, installTarget: { spec: `${name}@${version}`, version }, + }) + for (const step of observed.steps) { + expect(step.install, JSON.stringify(step.install)).toMatchObject({ application: 'applied', enabled: true, version }) + expect(step.savedVersion).toBe(version) + expect(step.installed).toMatchObject({ official: true, availability: 'profile', enabled: true, installed: true, version }) + expect(step.installed.rows.filter(row => row.preset !== undefined).map(row => row.preset).sort()) + .toEqual(['preset-cordis', 'preset-ptc', 'preset-standard']) + expect(step.providerEnabled).toBe(true) + expect(step.nativeArtifact).toBe(true) + expect(step.presets.every(preset => preset.broken === undefined)).toBe(true) + expect(step.unavailable).toMatchObject({ application: 'failed', stage: 'install' }) + expect(step.unchangedAfterUnavailable).toBe(true) + expect(step.off).toMatchObject({ application: 'applied' }) + expect(step.disabled).toMatchObject({ official: true, installed: true, enabled: false }) + expect(step.on).toMatchObject({ application: 'applied' }) + expect(step.enabled).toMatchObject({ official: true, installed: true, enabled: true }) + expect(step.removed, JSON.stringify(step.removed)).toMatchObject({ application: 'applied' }) + expect(step.absent).toMatchObject({ official: true, availability: 'missing', enabled: false, installed: false }) + } + expect(observed.steps).toHaveLength(packages.length) + expect(observed.sessionCount).toBe(0) + expect(globSync('sessions/**/*.jsonl*', { cwd: home })).toEqual([]) + await assertInstalledTree(profile) + } finally { + lines.close() + child.kill('SIGTERM') + const result = await child + expect(result.timedOut, `${result.stdout}\n${result.stderr}`).toBe(false) + expect(result.exitCode === 0 || result.signal === 'SIGTERM', result.stderr).toBe(true) + } +}) diff --git a/apps/cli/tests/profiles/packed-installation.ts b/apps/cli/tests/profiles/packed-installation.ts new file mode 100644 index 0000000000..6cfcd2ebb0 --- /dev/null +++ b/apps/cli/tests/profiles/packed-installation.ts @@ -0,0 +1,190 @@ +/** Pack and install a source-independent product for idle Web-profile integration tests. */ +import { createRequire } from 'node:module' +import { existsSync, globSync } from 'node:fs' +import { lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join, relative, resolve, sep } from 'node:path' +import { fileURLToPath } from 'node:url' +import { execa } from 'execa' +import yaml from 'js-yaml' +import { expect, onTestFinished } from 'vitest' +import { pnpmCommand } from '../../../../scripts/release/process.ts' +import { packedWorkspaceClosure, type WorkspacePackage } from '../../../../packages/sandbox/sandbox-local/tests/packed-workspace-closure.ts' + +const root = fileURLToPath(new URL('../../../..', import.meta.url)) + +/** Capture workspace dependency links without following their targets. */ +async function workspaceLinks(directories: string[]): Promise> { + const links: Record = {} + for (const directory of directories) { + const modules = join(directory, 'node_modules') + links[modules] = existsSync(modules) ? 'directory' : 'absent' + if (!existsSync(modules)) continue + for (const entry of await readdir(modules, { withFileTypes: true })) { + const path = join(modules, entry.name) + const paths = entry.name.startsWith('@') && entry.isDirectory() + ? (await readdir(path)).map(name => join(path, name)) : [path] + for (const dependency of paths) { + const stat = await lstat(dependency) + links[dependency] = stat.isSymbolicLink() ? await readlink(dependency) : stat.isDirectory() ? 'directory' : 'file' + } + } + } + return links +} + +/** Reject workspace links throughout an installed dependency tree. + * @param directory - Installation or profile whose links must remain inside its own tree. + */ +export async function assertInstalledTree(directory: string): Promise { + const installedRoot = await realpath(directory) + const visited = new Set() + const visit = async (path: string): Promise => { + const resolved = await realpath(path) + expect(resolved === installedRoot || resolved.startsWith(installedRoot + sep), `${path} escapes its installation`).toBe(true) + if (visited.has(resolved)) return + visited.add(resolved) + for (const entry of await readdir(path, { withFileTypes: true })) { + const child = join(path, entry.name) + if (entry.isSymbolicLink()) { + // Removed packages can leave dangling .bin shims; those links must also stay inside the profile. + const target = existsSync(child) ? await realpath(child) : resolve(dirname(child), await readlink(child)) + expect(target.startsWith(installedRoot + sep), `${child} escapes its installation`).toBe(true) + } else if (entry.isDirectory()) await visit(child) + } + } + await visit(directory) +} + +/** Actual tarballs and an isolated installed CLI, with an initially empty Web profile. */ +export interface PackedInstallation { + temporary: string + installation: string + home: string + profile: string + archives: string + environment: NodeJS.ProcessEnv + tarballs: Map + members: Map + externalVersions: Record + packageCommand: (args: string[], cwd: string) => Promise<{ stdout: string; stderr: string }> +} + +/** Build the packed product closure and additional packages without changing workspace dependency links. + * Native confinement and Session-writing operations are outside this idle-profile fixture. + * @param temporaryPrefix - Unique temporary-directory prefix identifying the owning test. + * @param extraPackages - Additional workspace packages to pack without installing in the product. + * @returns Isolated installation paths, tarballs, environment, and checked pnpm invocation. + */ +export async function createPackedInstallation(temporaryPrefix: string, extraPackages: readonly string[]): Promise { + const temporary = await realpath(await mkdtemp(join(tmpdir(), temporaryPrefix))) + onTestFinished(() => rm(temporary, { recursive: true, force: true })) + const installation = join(temporary, 'installation') + const home = join(temporary, 'home') + const profile = join(home, 'profiles', 'web') + const archives = join(temporary, 'archives') + await mkdir(profile, { recursive: true }) + await mkdir(archives) + const environment: NodeJS.ProcessEnv = Object.fromEntries(Object.entries(process.env).filter(([key]) => ( + !/KEY|SECRET|TOKEN|PASSWORD/.test(key) && key !== 'NODE_OPTIONS' && key !== 'NODE_PATH' + ))) + Object.assign(environment, { + DSH_HOME: home, + DSH_AGENTS_HOME: join(temporary, 'agents'), + DSH_TELEMETRY_DISABLED: '1', + CI: 'true', + }) + const [pnpm, ...prefix] = pnpmCommand() + const packageCommand = async (args: string[], cwd: string) => { + const result = await execa(pnpm, [...prefix, ...args], { + cwd, env: environment, extendEnv: false, timeout: 180_000, reject: false, + }) + expect(result.timedOut, `${result.stdout}\n${result.stderr}`).toBe(false) + expect(result.signal, result.stderr).toBeUndefined() + expect(result.exitCode, `${result.stdout}\n${result.stderr}`).toBe(0) + return result + } + const packages = new Map() + for (const path of globSync([ + 'apps/*/package.json', 'packages/*/*/package.json', 'vendor/*/package.json', 'native/system/packages/*/package.json', + ], { cwd: root })) { + const manifest = JSON.parse(await readFile(join(root, path), 'utf8')) as Record + if (typeof manifest.name !== 'string') throw new Error(`${path} has no package name`) + packages.set(manifest.name, { name: manifest.name, manifest, directory: dirname(join(root, path)) }) + } + const sourceDirectories = [root, ...[...packages.values()].map(member => member.directory)] + const initialLinks = await workspaceLinks(sourceDirectories) + onTestFinished(async () => { expect(await workspaceLinks(sourceDirectories)).toEqual(initialLinks) }) + const closure = packedWorkspaceClosure('@deepseek-ai/dsh', packages) + // These idle-profile tests create no Session or confined process. The native + // platform packages load lazily for those operations and have separate smokes. + const supportsIdleProfile = (member: WorkspacePackage) => ( + !member.name.startsWith('@deepseek-ai/node-addon-system-') + && (!Array.isArray(member.manifest.os) || member.manifest.os.includes(process.platform)) + && (!Array.isArray(member.manifest.cpu) || member.manifest.cpu.includes(process.arch)) + ) + const runtimeMembers = closure.filter(supportsIdleProfile) + const members = new Map(runtimeMembers.map(member => [member.name, member])) + for (const name of extraPackages) { + for (const member of packedWorkspaceClosure(name, packages).filter(supportsIdleProfile)) members.set(member.name, member) + } + // Packing reads package-owned files; installation never runs in the workspace. + await packageCommand([ + 'pack', '--recursive', '--workspace-concurrency=4', + ...[...members.keys()].map(name => `--filter=${name}`), '--pack-destination', archives, + ], root) + const tarballs = new Map([...members.values()].map((member) => { + if (typeof member.manifest.version !== 'string') throw new Error(`${member.name} has no version`) + const name = member.name.replaceAll('/', '-').replace('@', '') + const archive = join(archives, `${name}-${member.manifest.version}.tgz`) + expect(existsSync(archive), `missing packed payload for ${member.name}`).toBe(true) + return [member.name, archive] + })) + const workspaceSettings = yaml.load(await readFile(join(root, 'pnpm-workspace.yaml'), 'utf8')) as { + allowBuilds: Record + patchedDependencies: Record + minimumReleaseAgeExclude: string[] + overrides: Record + } + const externalVersions: Record = {} + for (const member of members.values()) { + const require = createRequire(join(member.directory, 'package.json')) + for (const section of ['dependencies', 'optionalDependencies', 'peerDependencies']) { + const dependencies = member.manifest[section] as Record | undefined + for (const [name, range] of Object.entries(dependencies ?? {})) { + if (range.startsWith('workspace:')) continue + const manifest = require.resolve.paths(name)?.map(path => join(path, name, 'package.json')).find(path => existsSync(path)) + if (manifest === undefined) continue + const installed = JSON.parse(await readFile(manifest, 'utf8')) as { version: string } + externalVersions[`${name}@${range}`] = installed.version + } + } + } + await mkdir(installation) + for (const path of Object.values(workspaceSettings.patchedDependencies)) { + await mkdir(dirname(join(installation, path)), { recursive: true }) + await writeFile(join(installation, path), await readFile(join(root, path))) + } + await writeFile(join(installation, 'package.json'), JSON.stringify({ + name: 'packed-product-installation', private: true, + dependencies: Object.fromEntries(runtimeMembers.map(member => [member.name, `file:${tarballs.get(member.name)!}`])), + })) + await writeFile(join(installation, 'pnpm-workspace.yaml'), yaml.dump({ + packages: ['.'], nodeLinker: 'hoisted', autoInstallPeers: false, allowUnusedPatches: true, + ignoredOptionalDependencies: closure.filter(member => !members.has(member.name)).map(member => member.name), + allowBuilds: { + ...workspaceSettings.allowBuilds, + [`@deepseek-ai/dsh-subprocess-local@file:${relative(installation, tarballs.get('@deepseek-ai/dsh-subprocess-local')!).split(sep).join('/')}`]: true, + }, + patchedDependencies: workspaceSettings.patchedDependencies, + minimumReleaseAgeExclude: workspaceSettings.minimumReleaseAgeExclude, + overrides: { + ...workspaceSettings.overrides, + ...externalVersions, + ...Object.fromEntries(runtimeMembers.map(member => [member.name, `file:${tarballs.get(member.name)!}`])), + }, + })) + await packageCommand(['install', '--prefer-offline', '--no-frozen-lockfile'], installation) + await assertInstalledTree(installation) + return { temporary, installation, home, profile, archives, environment, tarballs, members, externalVersions, packageCommand } +} diff --git a/apps/cli/tests/profiles/webhook-packed.e2e.ts b/apps/cli/tests/profiles/webhook-packed.e2e.ts index b70138ef72..3844ceb22b 100644 --- a/apps/cli/tests/profiles/webhook-packed.e2e.ts +++ b/apps/cli/tests/profiles/webhook-packed.e2e.ts @@ -1,16 +1,13 @@ /** Exercise the published webhook example from an isolated installed Web profile. */ import { createHmac } from 'node:crypto' -import { createRequire } from 'node:module' import { existsSync, globSync } from 'node:fs' -import { lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, writeFile } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { dirname, join, relative, sep } from 'node:path' +import { readFile, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { execa } from 'execa' import yaml from 'js-yaml' -import { expect, it, onTestFinished } from 'vitest' -import { pnpmCommand } from '../../../../scripts/release/process.ts' -import { packedWorkspaceClosure, type WorkspacePackage } from '../../../../packages/sandbox/sandbox-local/tests/packed-workspace-closure.ts' +import { expect, it } from 'vitest' +import { assertInstalledTree, createPackedInstallation } from './packed-installation.ts' const root = fileURLToPath(new URL('../../../..', import.meta.url)) const adapter = '@deepseek-ai/dsh-webhook-github' @@ -19,163 +16,14 @@ const built = existsSync(join(root, 'apps/cli/lib/bin.js')) && existsSync(join(root, 'packages/experimental/webhook-github/lib/index.js')) const secret = 'packed-webhook-test-secret' -/** Capture workspace dependency links without following their targets. */ -async function workspaceLinks(directories: string[]): Promise> { - const links: Record = {} - for (const directory of directories) { - const modules = join(directory, 'node_modules') - links[modules] = existsSync(modules) ? 'directory' : 'absent' - if (!existsSync(modules)) continue - for (const entry of await readdir(modules, { withFileTypes: true })) { - const path = join(modules, entry.name) - const paths = entry.name.startsWith('@') && entry.isDirectory() - ? (await readdir(path)).map(name => join(path, name)) : [path] - for (const dependency of paths) { - const stat = await lstat(dependency) - links[dependency] = stat.isSymbolicLink() ? await readlink(dependency) : stat.isDirectory() ? 'directory' : 'file' - } - } - } - return links -} - -/** Reject workspace links throughout the installed dependency tree. */ -async function assertInstalledTree(directory: string): Promise { - const installedRoot = await realpath(directory) - const visited = new Set() - const visit = async (path: string): Promise => { - const resolved = await realpath(path) - expect(resolved === installedRoot || resolved.startsWith(installedRoot + sep), `${path} escapes its installation`).toBe(true) - if (visited.has(resolved)) return - visited.add(resolved) - for (const entry of await readdir(path, { withFileTypes: true })) { - const child = join(path, entry.name) - if (entry.isSymbolicLink()) { - const target = await realpath(child) - expect(target.startsWith(installedRoot + sep), `${child} escapes its installation`).toBe(true) - } else if (entry.isDirectory()) await visit(child) - } - } - await visit(directory) -} - it.skipIf(!built)('loads the packed GitHub example with its explicitly installed runtime peer', { // Packing and installing the complete product closure owns this budget; no model request runs. timeout: 360_000, retry: 0, }, async () => { - const temporary = await realpath(await mkdtemp(join(tmpdir(), 'dsh-webhook-packed-'))) - onTestFinished(() => rm(temporary, { recursive: true, force: true })) - const installation = join(temporary, 'installation') - const home = join(temporary, 'home') - const profile = join(home, 'profiles', 'web') - const archives = join(temporary, 'archives') - await mkdir(profile, { recursive: true }) - await mkdir(archives) - const environment: NodeJS.ProcessEnv = Object.fromEntries(Object.entries(process.env).filter(([key]) => ( - !/KEY|SECRET|TOKEN|PASSWORD/.test(key) && key !== 'NODE_OPTIONS' && key !== 'NODE_PATH' - ))) - Object.assign(environment, { - DSH_HOME: home, - DSH_AGENTS_HOME: join(temporary, 'agents'), - DSH_TELEMETRY_DISABLED: '1', - DSH_GITHUB_WEBHOOK_SECRET: secret, - DSH_GITHUB_WEBHOOK_PORT: '0', - CI: 'true', - }) - const [pnpm, ...prefix] = pnpmCommand() - const packageCommand = async (args: string[], cwd: string) => { - const result = await execa(pnpm, [...prefix, ...args], { - cwd, env: environment, extendEnv: false, timeout: 180_000, reject: false, - }) - expect(result.timedOut, `${result.stdout}\n${result.stderr}`).toBe(false) - expect(result.signal, result.stderr).toBeUndefined() - expect(result.exitCode, `${result.stdout}\n${result.stderr}`).toBe(0) - return result - } - const packages = new Map() - for (const path of globSync([ - 'apps/*/package.json', 'packages/*/*/package.json', 'vendor/*/package.json', 'native/system/packages/*/package.json', - ], { cwd: root })) { - const manifest = JSON.parse(await readFile(join(root, path), 'utf8')) as Record - if (typeof manifest.name !== 'string') throw new Error(`${path} has no package name`) - packages.set(manifest.name, { name: manifest.name, manifest, directory: dirname(join(root, path)) }) - } - const sourceDirectories = [root, ...[...packages.values()].map(member => member.directory)] - const initialLinks = await workspaceLinks(sourceDirectories) - onTestFinished(async () => { expect(await workspaceLinks(sourceDirectories)).toEqual(initialLinks) }) - const closure = packedWorkspaceClosure('@deepseek-ai/dsh', packages) - // A signed ping creates no Session and runs no confined process. The native - // platform packages load lazily for those operations and have separate smokes. - const runtimeMembers = closure.filter(member => ( - !member.name.startsWith('@deepseek-ai/node-addon-system-') - && (!Array.isArray(member.manifest.os) || member.manifest.os.includes(process.platform)) - && (!Array.isArray(member.manifest.cpu) || member.manifest.cpu.includes(process.arch)) - )) - const members = new Map(runtimeMembers.map(member => [member.name, member])) - for (const name of [runtime, adapter]) { - const member = packages.get(name) - if (member === undefined) throw new Error(`missing workspace package ${name}`) - members.set(name, member) - } - // Packing reads package-owned files; installation never runs in the workspace. - await packageCommand([ - 'pack', '--recursive', '--workspace-concurrency=4', - ...[...members.keys()].map(name => `--filter=${name}`), '--pack-destination', archives, - ], root) - const tarballs = new Map([...members.values()].map((member) => { - if (typeof member.manifest.version !== 'string') throw new Error(`${member.name} has no version`) - const name = member.name.replaceAll('/', '-').replace('@', '') - const archive = join(archives, `${name}-${member.manifest.version}.tgz`) - expect(existsSync(archive), `missing packed payload for ${member.name}`).toBe(true) - return [member.name, archive] - })) - const workspaceSettings = yaml.load(await readFile(join(root, 'pnpm-workspace.yaml'), 'utf8')) as { - allowBuilds: Record - patchedDependencies: Record - minimumReleaseAgeExclude: string[] - overrides: Record - } - const externalVersions: Record = {} - for (const member of members.values()) { - const require = createRequire(join(member.directory, 'package.json')) - for (const section of ['dependencies', 'optionalDependencies', 'peerDependencies']) { - const dependencies = member.manifest[section] as Record | undefined - for (const [name, range] of Object.entries(dependencies ?? {})) { - if (range.startsWith('workspace:')) continue - const manifest = require.resolve.paths(name)?.map(path => join(path, name, 'package.json')).find(path => existsSync(path)) - if (manifest === undefined) continue - const installed = JSON.parse(await readFile(manifest, 'utf8')) as { version: string } - externalVersions[`${name}@${range}`] = installed.version - } - } - } - await mkdir(installation) - for (const path of Object.values(workspaceSettings.patchedDependencies)) { - await mkdir(dirname(join(installation, path)), { recursive: true }) - await writeFile(join(installation, path), await readFile(join(root, path))) - } - await writeFile(join(installation, 'package.json'), JSON.stringify({ - name: 'packed-webhook-installation', private: true, - dependencies: Object.fromEntries(runtimeMembers.map(member => [member.name, `file:${tarballs.get(member.name)!}`])), - })) - await writeFile(join(installation, 'pnpm-workspace.yaml'), yaml.dump({ - packages: ['.'], nodeLinker: 'hoisted', autoInstallPeers: false, allowUnusedPatches: true, - ignoredOptionalDependencies: closure.filter(member => !members.has(member.name)).map(member => member.name), - allowBuilds: { - ...workspaceSettings.allowBuilds, - [`@deepseek-ai/dsh-subprocess-local@file:${relative(installation, tarballs.get('@deepseek-ai/dsh-subprocess-local')!).split(sep).join('/')}`]: true, - }, - patchedDependencies: workspaceSettings.patchedDependencies, - minimumReleaseAgeExclude: workspaceSettings.minimumReleaseAgeExclude, - overrides: { - ...workspaceSettings.overrides, - ...externalVersions, - ...Object.fromEntries(runtimeMembers.map(member => [member.name, `file:${tarballs.get(member.name)!}`])), - }, - })) - await packageCommand(['install', '--prefer-offline', '--no-frozen-lockfile'], installation) - await assertInstalledTree(installation) + const { temporary, installation, home, profile, environment, tarballs, members, externalVersions, packageCommand } + = await createPackedInstallation('dsh-webhook-packed-', [runtime, adapter]) + Object.assign(environment, { DSH_GITHUB_WEBHOOK_SECRET: secret, DSH_GITHUB_WEBHOOK_PORT: '0' }) expect(existsSync(join(installation, 'node_modules', runtime))).toBe(false) expect(existsSync(join(installation, 'node_modules', adapter))).toBe(false) await writeFile(join(profile, 'package.json'), JSON.stringify({