mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
fix(scripts): address review findings in the local bundle registry
- The summary lists only the catalog roots as installable specs; the other 29 archives stay in the run manifest, where the installer resolves them itself. - Only a pnpm entrypoint receives pnpm's arguments, a catalog root counts as a required edge for platform admission, and a signal that stops packing reports the interrupted exit instead of an exception. - The packed rehearsal names the Agent SDK's musl artifact on musl hosts again, and its comments match what the fixture does. - The cache test registers its pnpm children for termination, and a new case keeps one profile and its real lockfile: pnpm refuses a new run's URL with ERR_PNPM_TARBALL_URL_MISMATCH until the stale resolution is cleared with pnpm's own `pnpm clean --lockfile` remedy, which the desktop README now documents for same-version iteration.
This commit is contained in:
1 parent
c26b658e61
commit
b99a2c147f
7 files changed
+115
-40
No files matched your search
@@ -59,8 +59,8 @@ it.skipIf(!built)('installs the exported registry\'s local closure and switches
|
||||
timeout: 900_000,
|
||||
retry: 0,
|
||||
}, async () => {
|
||||
// The product under test is a real source-independent installation, and the fixture packs the two providers into it,
|
||||
// so the product ships neither and both stay available to the Official catalog.
|
||||
// The product under test is a real source-independent installation that ships neither provider, so both stay
|
||||
// available to the Official catalog and only the local registry can supply them.
|
||||
const fixture = await createPackedInstallation('dsh-official-packed-', [])
|
||||
const { temporary, installation, home, profile, environment } = fixture
|
||||
|
||||
@@ -108,7 +108,8 @@ it.skipIf(!built)('installs the exported registry\'s local closure and switches
|
||||
packages,
|
||||
localDependency: '@deepseek-ai/dsh-brand',
|
||||
nativePackages: {
|
||||
'@deepseek-ai/dsh-subagent-claude-code': `@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}`,
|
||||
// The Agent SDK publishes a musl variant of its Linux artifact, which is what pnpm installs there.
|
||||
'@deepseek-ai/dsh-subagent-claude-code': `@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}${hostPlatform().libc?.[0] === 'musl' ? '-musl' : ''}`,
|
||||
'@deepseek-ai/dsh-subagent-codex': `@openai/codex-${process.platform}-${process.arch}`,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
en: 57869a5e3885f843
|
||||
zh: 7cfc01b2dcbebd78
|
||||
/deepseek-harness-desktop/package/local-official-bundles-from-a-loopback-registry:
|
||||
en: e6a1ea5aa317af5b
|
||||
zh: 70499589cde60903
|
||||
en: 5f05072ec82cc62d
|
||||
zh: a3d63b58d1ef2db7
|
||||
/deepseek-harness-desktop/package/runtime-file-selection:
|
||||
en: e7c1eb8e78fe46e3
|
||||
zh: b7ea497dd75be524
|
||||
|
||||
@@ -262,7 +262,7 @@ The command validates the catalog and the workspace inventory, runs `pnpm run bu
|
||||
|
||||
In the packaged application, open Plugins → Add plugin, paste one printed spec, choose **Custom address**, paste the printed registry URL, and install. A custom registry is asked alone, so this process must answer every dependency: repository-owned packages come from the run's own archives, and third-party packages keep npm's real metadata and bytes through a fixed redirect to `registry.npmjs.org`, which still requires network access. The installed entry stays Official because its package name is in the catalog. The registry choice is remembered in this browser; choose an offered registry again to switch back.
|
||||
|
||||
A run's artifacts are immutable, so a rebuild serves a new namespace and a new URL. To test changed code at the same version, run the command again and remove, then reinstall the bundle; the Official **Update** control does not appear while the installed and running versions match. `bundle-registry.json` records each archive's version, filename, size, and SHA-512 integrity together with the packing checkout's commit and dirty flag.
|
||||
A run's artifacts are immutable, so a rebuild serves a new namespace and a new URL, and the Official **Update** control does not appear while the installed and running versions match. The profile records the previous run's tarball URL in `pnpm-lock.yaml`, and pnpm verifies recorded entries, so installing or reinstalling that bundle against a new URL is refused with `ERR_PNPM_TARBALL_URL_MISMATCH` and can leave the earlier files in place. To test changed code at the same version, clear the stale resolution first — `pnpm clean --lockfile` in `$DSH_HOME/profiles/<profile>`, or install into a fresh profile — then install again from the new URL. `bundle-registry.json` records each archive's version, filename, size, and SHA-512 integrity together with the packing checkout's commit and dirty flag.
|
||||
|
||||
The served `time` is the real packing time, so pnpm 11's default 24-hour `minimumReleaseAge` records the exemption it granted in the profile's `pnpm-workspace.yaml` (`minimumReleaseAgeExclude`); an explicitly stricter policy there fails the installation with pnpm's own message instead of being overridden. A `@scope:registry` route in `~/.npmrc` or the profile's `.npmrc` overrides `--registry` for that scope, so it must not route the served scope.
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ pnpm run dev:bundle-registry
|
||||
|
||||
在打包应用中打开“插件 → 添加插件”,粘贴一条打印出的规格,选择**自定义地址**,粘贴打印出的 registry URL 后安装。自定义 registry 会被单独询问,因此必须由本进程回答每一个依赖:仓库自有的包取自本次运行的产物,第三方包则通过固定重定向到 `registry.npmjs.org` 保留 npm 的真实元数据与字节,因此仍需要网络访问。安装后的条目仍归类为 Official,因为它的包名在目录中。registry 选择会在此浏览器中记住;再次选择某个提供的 registry 即可切回。
|
||||
|
||||
一次运行的产物是不可变的,所以重新构建会提供新的命名空间和新 URL。要在同一版本上验证改动后的代码,请重新运行命令,然后移除并重新安装该 bundle;当已安装版本与运行版本一致时,Official 的**更新**控件不会出现。`bundle-registry.json` 记录每个归档的版本、文件名、大小和 SHA-512 完整性,以及打包所用 checkout 的 commit 和 dirty 标记。
|
||||
一次运行的产物是不可变的,所以重新构建会提供新的命名空间和新 URL;当已安装版本与运行版本一致时,Official 的**更新**控件不会出现。profile 会在 `pnpm-lock.yaml` 中记录上一次运行的 tarball URL,而 pnpm 会校验已记录的条目,因此用新 URL 再次安装或重装该 bundle 会被 `ERR_PNPM_TARBALL_URL_MISMATCH` 拒绝,并且可能继续保留旧文件。要在同一版本上验证改动后的代码,请先清除过期的解析结果——在 `$DSH_HOME/profiles/<profile>` 中运行 `pnpm clean --lockfile`,或改用全新的 profile——然后再从新 URL 安装。`bundle-registry.json` 记录每个归档的版本、文件名、大小和 SHA-512 完整性,以及打包所用 checkout 的 commit 和 dirty 标记。
|
||||
|
||||
所提供的 `time` 是真实的打包时间,因此 pnpm 11 默认的 24 小时 `minimumReleaseAge` 会在 profile 的 `pnpm-workspace.yaml` 中记录它授予的豁免(`minimumReleaseAgeExclude`);如果那里显式配置了更严格的策略,安装会以 pnpm 自己的报错失败,而不会被覆盖。`~/.npmrc` 或 profile 的 `.npmrc` 中的 `@scope:registry` 路由会覆盖该 scope 的 `--registry`,因此不能让它们指向正在提供服务的 scope。
|
||||
|
||||
|
||||
@@ -67,23 +67,23 @@ async function runStep(root: string, args: readonly string[], signal: AbortSigna
|
||||
|
||||
/**
|
||||
* Print the installation summary the developer copies from.
|
||||
* @param options - run directory, registry URL, packed artifacts, source evidence, and foreign-target members.
|
||||
* Only the catalog roots are installable specs; the rest of the closure is a
|
||||
* dependency the installer resolves itself and stays in the run manifest.
|
||||
* @param options - run directory, registry URL, catalog roots, packed artifacts, source evidence.
|
||||
*/
|
||||
function printSummary(options: {
|
||||
readonly directory: string
|
||||
readonly url: string
|
||||
readonly entries: readonly string[]
|
||||
readonly artifacts: readonly BundleArtifact[]
|
||||
readonly source: ReturnType<typeof sourceEvidence>
|
||||
readonly foreignTargets: readonly string[]
|
||||
}): void {
|
||||
const roots = options.entries.flatMap(name => options.artifacts.filter(artifact => artifact.name === name))
|
||||
process.stdout.write(`Source: ${options.source.commit}${options.source.dirty ? ' (dirty worktree)' : ''}, DSH ${options.source.dshVersion}\n`)
|
||||
if (options.foreignTargets.length > 0) {
|
||||
process.stdout.write(`Other platforms: ${options.foreignTargets.join(', ')} (served; pnpm skips them on this host)\n`)
|
||||
}
|
||||
process.stdout.write(`Artifacts: ${options.directory}\n`)
|
||||
process.stdout.write(`Registry: ${options.url}\n`)
|
||||
process.stdout.write('Packages:\n')
|
||||
for (const artifact of options.artifacts) process.stdout.write(` ${artifact.name}@${artifact.version}\n`)
|
||||
process.stdout.write(`Packages (${String(options.artifacts.length)} archives served):\n`)
|
||||
for (const artifact of roots) process.stdout.write(` ${artifact.name}@${artifact.version}\n`)
|
||||
process.stdout.write('In Plugins > Add plugin, use an exact package above and this custom registry URL.\n')
|
||||
process.stdout.write('Keep this process running during package operations. Ctrl+C stops the registry.\n')
|
||||
}
|
||||
@@ -96,9 +96,6 @@ function runId(): string {
|
||||
/** Build, pack, and serve the local bundle closure until a signal stops the run. */
|
||||
async function main(): Promise<void> {
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
if (!process.features.typescript) {
|
||||
throw new Error('dev:bundle-registry: Node.js TypeScript type stripping is unavailable; remove --no-experimental-strip-types from NODE_OPTIONS')
|
||||
}
|
||||
const abort = new AbortController()
|
||||
// A persistent listener, so a second signal during shutdown cannot fall back to the default kill;
|
||||
// the second signal is an explicit forced exit instead.
|
||||
@@ -146,7 +143,7 @@ async function main(): Promise<void> {
|
||||
registry = await startBundleRegistry({
|
||||
directory, artifacts, workspaceNames: selection.workspaceNames,
|
||||
})
|
||||
printSummary({ directory, url: registry.url, artifacts, source, foreignTargets: selection.foreignTargets })
|
||||
printSummary({ directory, url: registry.url, entries, artifacts, source })
|
||||
await new Promise<void>((resolveStop) => {
|
||||
if (abort.signal.aborted) resolveStop()
|
||||
else abort.signal.addEventListener('abort', () => { resolveStop() }, { once: true })
|
||||
@@ -156,8 +153,10 @@ async function main(): Promise<void> {
|
||||
process.stdout.write('Registry stopped. Artifacts and installed packages stay on disk.\n')
|
||||
} catch (error) {
|
||||
if (registry !== undefined) await registry.close()
|
||||
if (error instanceof Interrupted) {
|
||||
process.stderr.write(`dev:bundle-registry: ${error.message}\n`)
|
||||
// A signal that stopped packing or packing's child is the same stop as a stopped build step.
|
||||
if (error instanceof Interrupted || abort.signal.aborted) {
|
||||
const stage = error instanceof Interrupted ? error.message : 'interrupted during preparation'
|
||||
process.stderr.write(`dev:bundle-registry: ${stage}\n`)
|
||||
process.exitCode = 130
|
||||
return
|
||||
}
|
||||
|
||||
@@ -435,6 +435,50 @@ describe('run record', () => {
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Run one pnpm command in a profile-shaped directory, terminating and awaiting it with the test.
|
||||
* @param options - consumer directory, environment, the store and cache directories an installing
|
||||
* command shares, and the arguments to append.
|
||||
* @returns The settled child result.
|
||||
*/
|
||||
async function pnpmIn(options: {
|
||||
readonly consumer: string
|
||||
readonly environment: NodeJS.ProcessEnv
|
||||
readonly store?: string
|
||||
readonly cache?: string
|
||||
readonly args: readonly string[]
|
||||
}): Promise<{ exitCode?: number | null; stdout: string; stderr: string; timedOut: boolean }> {
|
||||
const invocation = pnpmInvocationFor([
|
||||
...options.args,
|
||||
...options.store === undefined ? [] : ['--store-dir', options.store],
|
||||
...options.cache === undefined ? [] : ['--cache-dir', options.cache],
|
||||
'--config.fetch-retries=0',
|
||||
])
|
||||
const child = execa(invocation.command, invocation.args, {
|
||||
cwd: options.consumer, env: options.environment, extendEnv: false, reject: false, timeout: 240_000,
|
||||
})
|
||||
onTestFinished(async () => {
|
||||
child.kill('SIGKILL')
|
||||
await child.catch(() => undefined)
|
||||
})
|
||||
return child
|
||||
}
|
||||
|
||||
/** Prepare one isolated profile-shaped consumer directory. */
|
||||
function consumerDirectory(parent: string, name: string): string {
|
||||
const consumer = join(parent, name)
|
||||
mkdirSync(consumer, { recursive: true })
|
||||
writeFileSync(join(consumer, 'package.json'), JSON.stringify({ name, private: true, dependencies: {} }))
|
||||
writeFileSync(join(consumer, 'pnpm-workspace.yaml'), 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n')
|
||||
return consumer
|
||||
}
|
||||
|
||||
/** The payload a consumer's installed fixture carries, or a marker when nothing is installed. */
|
||||
function installedPayload(consumer: string): string {
|
||||
try { return readFileSync(join(consumer, 'node_modules', '@dsh-local', 'fixture', 'lib', 'index.js'), 'utf8').trim() }
|
||||
catch { return '(absent)' }
|
||||
}
|
||||
|
||||
describe('package cache freshness', () => {
|
||||
it('installs a rebuilt archive at the same version through a retained pnpm store', { timeout: 300_000 }, async () => {
|
||||
const temporary = temporaryDirectory('dsh-local-registry-cache-')
|
||||
@@ -453,24 +497,15 @@ describe('package cache freshness', () => {
|
||||
directory: destination, artifacts: [artifact], workspaceNames: ['@dsh-local/fixture'],
|
||||
})
|
||||
onTestFinished(() => registry.close())
|
||||
const consumer = join(temporary, run, 'consumer')
|
||||
mkdirSync(consumer, { recursive: true })
|
||||
writeFileSync(join(consumer, 'package.json'), JSON.stringify({ name: `consumer-${run}`, private: true, dependencies: {} }))
|
||||
writeFileSync(join(consumer, 'pnpm-workspace.yaml'), 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n')
|
||||
const invocation = pnpmInvocationFor([
|
||||
'add', '@dsh-local/fixture@1.0.0', `--registry=${registry.url}`,
|
||||
'--store-dir', store, '--cache-dir', cache, '--config.lockfile=false',
|
||||
])
|
||||
const result = await execa(invocation.command, invocation.args, {
|
||||
cwd: consumer, env: environment, extendEnv: false, reject: false, timeout: 240_000,
|
||||
const consumer = consumerDirectory(temporary, `consumer-${run}`)
|
||||
const result = await pnpmIn({
|
||||
consumer, environment, store, cache,
|
||||
args: ['add', '@dsh-local/fixture@1.0.0', `--registry=${registry.url}`],
|
||||
})
|
||||
expect(result.timedOut, `${result.stdout}\n${result.stderr}`).toBe(false)
|
||||
expect(result.exitCode, `${result.stdout}\n${result.stderr}`).toBe(0)
|
||||
await registry.close()
|
||||
return {
|
||||
payload: readFileSync(join(consumer, 'node_modules', '@dsh-local', 'fixture', 'lib', 'index.js'), 'utf8'),
|
||||
settings: readFileSync(join(consumer, 'pnpm-workspace.yaml'), 'utf8'),
|
||||
}
|
||||
return { payload: installedPayload(consumer), settings: readFileSync(join(consumer, 'pnpm-workspace.yaml'), 'utf8') }
|
||||
}
|
||||
|
||||
const first = await install('first', 'export default "first build"\n')
|
||||
@@ -484,3 +519,41 @@ describe('package cache freshness', () => {
|
||||
expect(second.payload).toContain('second build')
|
||||
})
|
||||
})
|
||||
|
||||
describe('same-version iteration', () => {
|
||||
it('needs the earlier run\'s stale resolution cleared before a new run at the same version installs', { timeout: 300_000 }, async () => {
|
||||
const temporary = temporaryDirectory('dsh-local-registry-iteration-')
|
||||
const store = join(temporary, 'store')
|
||||
const userConfig = join(temporary, 'empty.npmrc')
|
||||
writeFileSync(userConfig, '')
|
||||
const environment = { ...process.env, npm_config_userconfig: userConfig }
|
||||
const consumer = consumerDirectory(temporary, 'profile')
|
||||
const serve = async (tag: string, payload: string) => {
|
||||
const destination = join(temporary, tag)
|
||||
mkdirSync(destination, { recursive: true })
|
||||
const { artifact } = fabricate({ destination, name: '@dsh-local/fixture', version: '1.0.0', source: `export default ${payload}\n` })
|
||||
return startBundleRegistry({ directory: destination, artifacts: [artifact], workspaceNames: ['@dsh-local/fixture'] })
|
||||
}
|
||||
const add = (url: string) => pnpmIn({ consumer, environment, store, args: ['add', '@dsh-local/fixture@1.0.0', `--registry=${url}`] })
|
||||
|
||||
const first = await serve('run-a', '"A"')
|
||||
expect((await add(first.url)).exitCode).toBe(0)
|
||||
expect(installedPayload(consumer)).toContain('"A"')
|
||||
await first.close()
|
||||
|
||||
// A new run answers at a new URL, so the recorded tarball no longer verifies against it.
|
||||
const second = await serve('run-b', '"B"')
|
||||
onTestFinished(() => second.close())
|
||||
const refused = await add(second.url)
|
||||
expect(refused.exitCode).not.toBe(0)
|
||||
expect(`${refused.stdout}${refused.stderr}`).toContain('ERR_PNPM_TARBALL_URL_MISMATCH')
|
||||
expect(installedPayload(consumer)).toContain('"A"')
|
||||
|
||||
// pnpm's own remedy for that error: drop the stale resolution, then resolve again from the new run.
|
||||
const cleaned = await pnpmIn({ consumer, environment, args: ['clean', '--lockfile'] })
|
||||
expect(cleaned.exitCode, `${cleaned.stdout}\n${cleaned.stderr}`).toBe(0)
|
||||
const reinstalled = await add(second.url)
|
||||
expect(reinstalled.exitCode, `${reinstalled.stdout}\n${reinstalled.stderr}`).toBe(0)
|
||||
expect(installedPayload(consumer)).toContain('"B"')
|
||||
})
|
||||
})
|
||||
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* A packaged application installs an Official bundle as `name@<running DSH
|
||||
* version>`, and the plugin manager asks a custom registry *alone*
|
||||
* ([registry plan](../../packages/boot/plugin-manager/src/registry.ts)). Every
|
||||
* ([registry plan](../packages/boot/plugin-manager/src/registry.ts)). Every
|
||||
* transitive dependency of the packed bundles must therefore be answerable by
|
||||
* this process: repository-owned packages come from the run's own artifacts,
|
||||
* and genuinely external packages keep npm's real metadata and bytes through a
|
||||
@@ -157,11 +157,12 @@ export interface ReleaseEntry {
|
||||
*/
|
||||
export function pnpmInvocationFor(args: readonly string[]): { command: string; args: string[] } {
|
||||
const entrypoint = process.env.npm_execpath
|
||||
if (entrypoint === undefined || entrypoint === '') {
|
||||
const [command, ...prefix] = pnpmCommand()
|
||||
return { command, args: [...prefix, ...args] }
|
||||
// `npm run` and `yarn run` set the same variable; handing them pnpm's arguments would write a different lockfile.
|
||||
if (entrypoint !== undefined && /[\\/]pnpm[\\/]/u.test(entrypoint) && /\.[cm]?js$/u.test(entrypoint)) {
|
||||
return pnpmInvocation(args)
|
||||
}
|
||||
return pnpmInvocation(args)
|
||||
const [command, ...prefix] = pnpmCommand()
|
||||
return { command, args: [...prefix, ...args] }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -686,6 +687,7 @@ function describePlatform(constraint: PlatformConstraint): string {
|
||||
* @returns True when a required dependency edge reaches the target.
|
||||
*/
|
||||
function reachedByRequiredEdge(target: string, entries: readonly string[], selected: ReadonlyMap<string, WorkspacePackage>): boolean {
|
||||
if (entries.includes(target)) return true
|
||||
const visited = new Set<string>()
|
||||
const pending = [...entries]
|
||||
for (let index = 0; index < pending.length; index += 1) {
|
||||
@@ -728,7 +730,7 @@ function dependencyPath(name: string, parents: ReadonlyMap<string, string>, entr
|
||||
* @returns The package-relative path.
|
||||
*/
|
||||
function payloadPath(file: string): string {
|
||||
return file.replaceAll('\\', '/').replace(/^\.\/+/u, '').replace(/^package\//u, '')
|
||||
return file.replaceAll('\\', '/').replace(/^\.\/+/u, '')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user