Merge master into job lifecycle timing and refresh owned fixtures
This commit is contained in:
commit
672b384eea
5646 files changed
+135257
-53826
No files matched your search
@@ -22,7 +22,8 @@ describe('Python runtime executable assets', () => {
|
||||
|
||||
expect(result.status).toBe(0)
|
||||
expect(result.stdout).toContain('node_modules/@deepseek-ai/dsh-web-frontend/dist/**/*')
|
||||
expect(result.stdout).toContain('node_modules/@deepseek-ai/dsh-skill-badge/assets/**/*')
|
||||
expect(result.stdout).toContain('node_modules/@deepseek-ai/dsh-*/**/*.{svg,png,jpg,jpeg,webp}')
|
||||
expect(result.stdout).not.toContain('skill-badge/assets/**/*')
|
||||
expect(result.stdout).toContain('node_modules/@deepseek-ai/dsh-sandbox-windows-acl/assets/**/*')
|
||||
expect(result.stdout).not.toContain('node_modules/**/*.py')
|
||||
})
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, relative } from 'node:path'
|
||||
import { createRequire } from 'node:module'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { deduplicateStagedWorkspacePackages, materializeStagedLinks } from './build-exe-for-python-sdk-staging.ts'
|
||||
|
||||
const temporaryDirectories: string[] = []
|
||||
const directoryLinkType = process.platform === 'win32' ? 'junction' : 'dir'
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(temporaryDirectories.splice(0).map(directory => rm(directory, { recursive: true, force: true })))
|
||||
})
|
||||
|
||||
async function fixture(prefix: string) {
|
||||
const workspace = await mkdtemp(join(tmpdir(), prefix))
|
||||
temporaryDirectories.push(workspace)
|
||||
return workspace
|
||||
}
|
||||
|
||||
/** List every symbolic link below a directory. */
|
||||
async function listSymlinks(directory: string): Promise<string[]> {
|
||||
const entries = await readdir(directory, { withFileTypes: true })
|
||||
const nested = await Promise.all(entries.map(async (entry) => {
|
||||
const path = join(directory, entry.name)
|
||||
if (entry.isSymbolicLink()) return [path]
|
||||
if (entry.isDirectory()) return listSymlinks(path)
|
||||
return []
|
||||
}))
|
||||
return nested.flat()
|
||||
}
|
||||
|
||||
it('drops the deploy root link that points back into the payload', async () => {
|
||||
const workspace = await fixture('dsh-python-staging-root-')
|
||||
const project = join(workspace, 'python', 'sdk-runtime')
|
||||
const staging = join(project, 'src', 'runtime', 'node')
|
||||
await mkdir(join(staging, 'node_modules'), { recursive: true })
|
||||
await writeFile(join(project, 'package.json'), '{"name":"dsh-python-runtime-closure"}\n')
|
||||
await symlink(project, join(staging, 'node_modules', 'dsh-python-runtime-closure'), directoryLinkType)
|
||||
|
||||
await materializeStagedLinks(staging)
|
||||
|
||||
expect(existsSync(join(staging, 'node_modules', 'dsh-python-runtime-closure'))).toBe(false)
|
||||
await expect(readFile(join(project, 'package.json'), 'utf8')).resolves.toContain('dsh-python-runtime-closure')
|
||||
})
|
||||
|
||||
it('copies workspace package links as files and removes command shims', async () => {
|
||||
const workspace = await fixture('dsh-python-staging-packages-')
|
||||
const staging = join(workspace, 'runtime', 'node')
|
||||
const packageDirectory = join(workspace, 'packages', 'dsh-base')
|
||||
await mkdir(join(packageDirectory, 'lib'), { recursive: true })
|
||||
await writeFile(join(packageDirectory, 'package.json'), '{"name":"@deepseek-ai/dsh-base"}\n')
|
||||
await writeFile(join(packageDirectory, 'lib', 'index.js'), 'export {}\n')
|
||||
await mkdir(join(staging, 'node_modules', '@deepseek-ai'), { recursive: true })
|
||||
await mkdir(join(staging, 'node_modules', '.bin'), { recursive: true })
|
||||
await writeFile(join(staging, 'package.json'), '{"name":"dsh-python-runtime-closure"}\n')
|
||||
await symlink(packageDirectory, join(staging, 'node_modules', '@deepseek-ai', 'dsh-base'), directoryLinkType)
|
||||
await symlink(packageDirectory, join(staging, 'node_modules', '.bin', 'dsh'), directoryLinkType)
|
||||
|
||||
await materializeStagedLinks(staging)
|
||||
|
||||
expect(existsSync(join(staging, 'node_modules', '.bin'))).toBe(false)
|
||||
await expect(readFile(join(staging, 'node_modules', '@deepseek-ai', 'dsh-base', 'lib', 'index.js'), 'utf8'))
|
||||
.resolves.toBe('export {}\n')
|
||||
expect(await listSymlinks(staging)).toEqual([])
|
||||
})
|
||||
|
||||
it('shares an identical workspace package while retaining different peers and third-party copies', async () => {
|
||||
const workspace = await fixture('dsh-python-staging-dedupe-')
|
||||
const staging = join(workspace, 'payload')
|
||||
const modules = join(staging, 'node_modules')
|
||||
const root = join(modules, '@fixture', 'tools')
|
||||
const duplicate = join(modules, 'app', 'node_modules', '@fixture', 'tools')
|
||||
const differentPeers = join(modules, 'other', 'node_modules', '@fixture', 'tools')
|
||||
const external = join(modules, 'external')
|
||||
const externalCopy = join(modules, 'app', 'node_modules', 'external')
|
||||
for (const directory of [root, duplicate, differentPeers, external, externalCopy]) {
|
||||
await mkdir(directory, { recursive: true })
|
||||
await writeFile(join(directory, 'index.js'), 'exports.scheduler = Symbol("scheduler")\n')
|
||||
}
|
||||
const locations = (paths: string[]) => paths.map(path => relative(workspace, path))
|
||||
await writeFile(join(modules, '.modules.yaml'), JSON.stringify({ hoistedLocations: {
|
||||
'@fixture/tools@file:packages/tools(peer@1)': locations([root, duplicate]),
|
||||
'@fixture/tools@file:packages/tools(peer@2)': locations([differentPeers]),
|
||||
'[email protected](@fixture/tools@file:packages/tools)': locations([external, externalCopy]),
|
||||
} }))
|
||||
|
||||
await deduplicateStagedWorkspacePackages(staging, workspace)
|
||||
|
||||
expect(existsSync(duplicate)).toBe(false)
|
||||
for (const directory of [root, differentPeers, external, externalCopy]) {
|
||||
expect(existsSync(join(directory, 'index.js'))).toBe(true)
|
||||
}
|
||||
const fromRoot = createRequire(join(staging, 'entry.cjs'))
|
||||
const fromApp = createRequire(join(modules, 'app', 'entry.cjs'))
|
||||
const fromOther = createRequire(join(modules, 'other', 'entry.cjs'))
|
||||
expect(fromApp('@fixture/tools')).toBe(fromRoot('@fixture/tools'))
|
||||
expect(fromOther('@fixture/tools')).not.toBe(fromRoot('@fixture/tools'))
|
||||
})
|
||||
|
||||
it('rejects invalid deployment metadata before removing any package', async () => {
|
||||
const workspace = await fixture('dsh-python-staging-metadata-')
|
||||
const staging = join(workspace, 'payload')
|
||||
const modules = join(staging, 'node_modules')
|
||||
const root = join(modules, 'tools')
|
||||
const duplicate = join(modules, 'app', 'node_modules', 'tools')
|
||||
const outside = join(workspace, 'source')
|
||||
for (const directory of [root, duplicate, outside]) await mkdir(directory, { recursive: true })
|
||||
const valid = { 'tools@file:packages/tools': [root, duplicate].map(path => relative(workspace, path)) }
|
||||
for (const invalid of [
|
||||
{},
|
||||
{ hoistedLocations: { invalid: [42] } },
|
||||
{ hoistedLocations: { ...valid, 'other@file:packages/other': [relative(workspace, outside)] } },
|
||||
]) {
|
||||
await writeFile(join(modules, '.modules.yaml'), JSON.stringify(invalid))
|
||||
await expect(deduplicateStagedWorkspacePackages(staging, workspace)).rejects.toThrow('hoistedLocations')
|
||||
expect(existsSync(duplicate)).toBe(true)
|
||||
expect(existsSync(outside)).toBe(true)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,101 @@
|
||||
/** Prepare a symlink-free Python runtime payload with shared workspace module instances. */
|
||||
import { cp, lstat, readFile, readdir, realpath, rm, unlink } from 'node:fs/promises'
|
||||
import { isAbsolute, join, relative, resolve, sep } from 'node:path'
|
||||
import { load } from 'js-yaml'
|
||||
|
||||
function isHoistedLocations(value: unknown): value is Record<string, string[]> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
&& Object.values(value).every(paths => Array.isArray(paths)
|
||||
&& paths.every((path: unknown) => typeof path === 'string'))
|
||||
}
|
||||
|
||||
/**
|
||||
* Share root workspace packages with nested consumers of the same pnpm dependency identity.
|
||||
* Different peer resolutions and third-party packages keep their installed locations.
|
||||
* All metadata paths are checked before deletion; the caller first materializes links.
|
||||
* @param staging - Symlink-free deployed payload root.
|
||||
* @param workspace - Workspace root relative to which pnpm records hoisted locations.
|
||||
*/
|
||||
export async function deduplicateStagedWorkspacePackages(staging: string, workspace: string): Promise<void> {
|
||||
const modules = resolve(staging, 'node_modules')
|
||||
const file = join(modules, '.modules.yaml')
|
||||
const manifest: unknown = load(await readFile(file, 'utf8'))
|
||||
if (typeof manifest !== 'object' || manifest === null || !('hoistedLocations' in manifest)
|
||||
|| !isHoistedLocations(manifest.hoistedLocations)) {
|
||||
throw new Error(`${file}: hoistedLocations must map dependency identities to path lists`)
|
||||
}
|
||||
const duplicates: string[] = []
|
||||
for (const [identity, locations] of Object.entries(manifest.hoistedLocations)) {
|
||||
const paths = locations.map((location) => {
|
||||
const path = resolve(workspace, location)
|
||||
const local = relative(modules, path)
|
||||
if (local === '' || local === '..' || local.startsWith(`..${sep}`) || isAbsolute(local)) {
|
||||
throw new Error(`${file}: hoistedLocations path is outside deployed node_modules: ${location}`)
|
||||
}
|
||||
return { path, parts: local.split(sep) }
|
||||
})
|
||||
if (!/^(?:@[^/]+\/)?[^@()]+@file:/.test(identity)) continue
|
||||
const root = paths.find(({ parts }) => parts.length === (parts[0]?.startsWith('@') === true ? 2 : 1))
|
||||
if (root === undefined) continue
|
||||
if (!(await lstat(root.path)).isDirectory()) {
|
||||
throw new Error(`${file}: hoistedLocations root package is not a directory: ${root.path}`)
|
||||
}
|
||||
duplicates.push(...paths.filter(({ path }) => path !== root.path).map(({ path }) => path))
|
||||
}
|
||||
for (const path of duplicates) await rm(path, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
/** Return the first symbolic link below a directory, if one exists. */
|
||||
async function findSymlink(directory: string): Promise<string | undefined> {
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
const path = join(directory, entry.name)
|
||||
const metadata = await lstat(path)
|
||||
if (metadata.isSymbolicLink()) return path
|
||||
if (metadata.isDirectory()) {
|
||||
const nested = await findSymlink(path)
|
||||
if (nested !== undefined) return nested
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace every deploy-time package link below the staged payload with real files.
|
||||
*
|
||||
* Legacy deploy links workspace packages into the target's `node_modules`, and
|
||||
* those links must survive packaging as files. One link is not a package: when
|
||||
* the deploy target sits inside the deployed project, pnpm links that project
|
||||
* into its own `node_modules`. Dereferencing it would copy the payload into
|
||||
* itself, and the packaged payload never carries the project source, so the
|
||||
* entry is dropped instead.
|
||||
* @param staging - Deployed payload root whose `node_modules` becomes symlink-free.
|
||||
*/
|
||||
export async function materializeStagedLinks(staging: string): Promise<void> {
|
||||
const payload = await realpath(staging)
|
||||
const nodeModules = join(staging, 'node_modules')
|
||||
let remaining = await findSymlink(nodeModules)
|
||||
while (remaining !== undefined) {
|
||||
const segments = remaining.slice(nodeModules.length + 1).split(sep)
|
||||
const binIndex = segments.lastIndexOf('.bin')
|
||||
if (binIndex >= 0) {
|
||||
await rm(join(nodeModules, ...segments.slice(0, binIndex + 1)), { recursive: true, force: true })
|
||||
remaining = await findSymlink(nodeModules)
|
||||
continue
|
||||
}
|
||||
const destination = remaining
|
||||
const source = await realpath(destination)
|
||||
if (payload === source || payload.startsWith(source + sep)) {
|
||||
await unlink(destination)
|
||||
remaining = await findSymlink(nodeModules)
|
||||
continue
|
||||
}
|
||||
const nestedNodeModules = join(source, 'node_modules')
|
||||
await unlink(destination)
|
||||
await cp(source, destination, {
|
||||
recursive: true,
|
||||
dereference: true,
|
||||
filter: path => path !== nestedNodeModules && !path.startsWith(nestedNodeModules + sep),
|
||||
})
|
||||
remaining = await findSymlink(nodeModules)
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,78 @@ function run(env: NodeJS.ProcessEnv, ...args: string[]) {
|
||||
}
|
||||
|
||||
describe('Python runtime executable builder CLI', () => {
|
||||
it.each([
|
||||
{ flags: [], command: 'run build' },
|
||||
{ flags: ['--artifacts-only'], command: 'run build --artifacts-only' },
|
||||
])('validates the closure before $command and deploys only after building', ({ flags, command }) => {
|
||||
const result = run(
|
||||
{ npm_execpath: 'C:\\tools\\pnpm.cjs' },
|
||||
'--dry-run',
|
||||
'--targets=node24-macos-arm64',
|
||||
...flags,
|
||||
)
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.signal).toBeNull()
|
||||
expect(result.status, result.stderr).toBe(0)
|
||||
const commands = result.stdout.split('\n').filter(line => line.includes('C:\\tools\\pnpm.cjs '))
|
||||
expect(commands[0]).toMatch(/ run verify-runtime-closure$/)
|
||||
expect(commands[1]?.endsWith(' ' + command)).toBe(true)
|
||||
expect(commands[2]).toContain(' --filter dsh-python-runtime-closure deploy ')
|
||||
})
|
||||
|
||||
it('rejects simultaneous build modes before invoking pnpm', () => {
|
||||
const setup = mkdtempSync(join(tmpdir(), 'dsh-python-build-mode-'))
|
||||
temporaryDirectories.push(setup)
|
||||
const entrypoint = join(setup, 'pnpm.cjs')
|
||||
const commands = join(setup, 'commands.jsonl')
|
||||
writeFileSync(entrypoint, [
|
||||
"require('node:fs').appendFileSync(process.env.DSH_PYTHON_BUILD_COMMANDS, JSON.stringify(process.argv.slice(2)) + '\\n')",
|
||||
'process.exit(31)',
|
||||
'',
|
||||
].join('\n'))
|
||||
|
||||
const result = run(
|
||||
{ npm_execpath: entrypoint, DSH_PYTHON_BUILD_COMMANDS: commands },
|
||||
'--skip-build',
|
||||
'--artifacts-only',
|
||||
'--targets=node24-macos-arm64',
|
||||
)
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.signal).toBeNull()
|
||||
expect(result.status).not.toBe(0)
|
||||
expect(result.stderr).toContain('--skip-build and --artifacts-only cannot be combined')
|
||||
expect(result.stdout).toBe('')
|
||||
expect(existsSync(commands)).toBe(false)
|
||||
})
|
||||
|
||||
it('stops before artifact building or deployment when the closure check fails', () => {
|
||||
const setup = mkdtempSync(join(tmpdir(), 'dsh-python-closure-'))
|
||||
temporaryDirectories.push(setup)
|
||||
const entrypoint = join(setup, 'pnpm.cjs')
|
||||
const commands = join(setup, 'commands.jsonl')
|
||||
writeFileSync(entrypoint, [
|
||||
"require('node:fs').appendFileSync(process.env.DSH_PYTHON_BUILD_COMMANDS, JSON.stringify(process.argv.slice(2)) + '\\n')",
|
||||
"if (process.argv.slice(2).join(' ') === 'run verify-runtime-closure') process.exit(31)",
|
||||
'process.exit(32)',
|
||||
'',
|
||||
].join('\n'))
|
||||
|
||||
const result = run(
|
||||
{ npm_execpath: entrypoint, DSH_PYTHON_BUILD_COMMANDS: commands },
|
||||
'--artifacts-only',
|
||||
'--targets=node24-macos-arm64',
|
||||
)
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.signal).toBeNull()
|
||||
expect(result.status).not.toBe(0)
|
||||
expect(result.stderr).toContain('runtime dependency closure failed (exit code 31)')
|
||||
expect(readFileSync(commands, 'utf8').trim().split('\n'))
|
||||
.toEqual([JSON.stringify(['run', 'verify-runtime-closure'])])
|
||||
})
|
||||
|
||||
it('keeps the single-file dispatcher on the Python packaging surface', () => {
|
||||
const bootstrapPath = resolve(root, 'python/sdk-runtime/runtime-bootstrap.mjs')
|
||||
const bootstrap = readFileSync(bootstrapPath, 'utf8')
|
||||
@@ -59,9 +131,11 @@ describe('Python runtime executable builder CLI', () => {
|
||||
|
||||
expect(result.status).toBe(0)
|
||||
expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs run verify-runtime-closure`)
|
||||
expect(result.stdout).not.toMatch(/\[dry-run\] .* run build(?: --artifacts-only)?$/m)
|
||||
expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs --filter dsh-python-runtime-closure deploy`)
|
||||
const deploy = result.stdout.split('\n').find(line => line.includes(' --filter dsh-python-runtime-closure deploy'))
|
||||
expect(deploy).toContain('--prod --config.allow-unused-patches=true')
|
||||
expect(deploy).toContain('--config.hoist-workspace-packages=false')
|
||||
expect(result.stdout.split('--config.allow-unused-patches=true')).toHaveLength(2)
|
||||
expect(result.stdout).not.toContain(resolve(root, 'python/sdk-runtime/runtime-bootstrap.mjs'))
|
||||
expect(result.stdout).toContain('"bin":"runtime-bootstrap.mjs"')
|
||||
|
||||
@@ -8,12 +8,13 @@
|
||||
|
||||
import { spawn } from 'node:child_process'
|
||||
import { existsSync, statSync } from 'node:fs'
|
||||
import { chmod, copyFile, cp, lstat, mkdir, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
|
||||
import { chmod, copyFile, cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { basename, dirname, extname, join, resolve, sep } from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { resolveLinuxNodePtyAddon, resolveWindowsNodePtyAddons } from './build-exe-for-python-sdk-native-pty.ts'
|
||||
import { copyOfficeSidecar, OFFICE_ASSET_IGNORES } from './build-exe-for-python-sdk-office.ts'
|
||||
import { deduplicateStagedWorkspacePackages, materializeStagedLinks } from './build-exe-for-python-sdk-staging.ts'
|
||||
import { preparePrimaryRuntime, smokePrimaryRuntime, type PrimaryRuntimeTarget } from './primary-runtime/prepare.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
@@ -48,7 +49,9 @@ const ASSET_GLOBS = [
|
||||
'node_modules/**/*.mjs',
|
||||
'node_modules/**/package.json',
|
||||
'node_modules/**/*.json',
|
||||
// Package-owned Markdown includes runtime skill instructions and badge content.
|
||||
// Plugin display metadata resolves these package-owned images at runtime.
|
||||
'node_modules/@deepseek-ai/dsh-*/**/*.{svg,png,jpg,jpeg,webp}',
|
||||
// Package-owned Markdown includes runtime skill instructions.
|
||||
'node_modules/**/*.md',
|
||||
'node_modules/**/*.dylib',
|
||||
'node_modules/**/*.dll',
|
||||
@@ -60,8 +63,6 @@ const ASSET_GLOBS = [
|
||||
'node_modules/**/*.yml',
|
||||
// web-app builds this path dynamically, so pkg cannot discover the static frontend.
|
||||
'node_modules/@deepseek-ai/dsh-web-frontend/dist/**/*',
|
||||
// skill-badge resolves both Markdown and image resources through import.meta.url.
|
||||
'node_modules/@deepseek-ai/dsh-skill-badge/assets/**/*',
|
||||
// The diagnosis provider extracts its PowerShell script for an external interpreter.
|
||||
'node_modules/@deepseek-ai/dsh-sandbox-windows-acl/assets/**/*',
|
||||
]
|
||||
@@ -156,8 +157,10 @@ class BuildCli {
|
||||
private constructor(
|
||||
/** Build targets; defaults to the host platform only. */
|
||||
readonly targets: readonly Target[],
|
||||
/** Skip step 1 (`pnpm run build`); lib/ artifacts must already exist. */
|
||||
/** Skip the package build; lib/ artifacts must already exist. */
|
||||
readonly skipBuild: boolean,
|
||||
/** Emit package and Web artifacts without repository test and script typechecks. */
|
||||
readonly artifactsOnly: boolean,
|
||||
/** Print every command and config patch instead of executing. */
|
||||
readonly dryRun: boolean,
|
||||
) {}
|
||||
@@ -181,6 +184,9 @@ class BuildCli {
|
||||
console.log(BuildCli.usage())
|
||||
process.exit(0)
|
||||
}
|
||||
if (values['skip-build'] && values['artifacts-only']) {
|
||||
throw new Error('build-exe-for-python-sdk: --skip-build and --artifacts-only cannot be combined.')
|
||||
}
|
||||
const targets = values.targets === undefined
|
||||
? [Target.host()]
|
||||
: values.targets.split(',').map(part => part.trim()).filter(part => part !== '').map(spec => Target.parse(spec))
|
||||
@@ -193,7 +199,7 @@ class BuildCli {
|
||||
}
|
||||
seen.add(key)
|
||||
}
|
||||
return new BuildCli(targets, values['skip-build'], values['dry-run'])
|
||||
return new BuildCli(targets, values['skip-build'], values['artifacts-only'], values['dry-run'])
|
||||
}
|
||||
|
||||
private static parseRaw(argv: string[]) {
|
||||
@@ -202,6 +208,7 @@ class BuildCli {
|
||||
options: {
|
||||
'targets': { type: 'string' },
|
||||
'skip-build': { type: 'boolean', default: false },
|
||||
'artifacts-only': { type: 'boolean', default: false },
|
||||
'dry-run': { type: 'boolean', default: false },
|
||||
'help': { type: 'boolean', default: false },
|
||||
},
|
||||
@@ -215,6 +222,7 @@ class BuildCli {
|
||||
' --targets=<t1,t2,...> pkg targets, e.g. node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64.',
|
||||
' Default: the host platform only (on node24).',
|
||||
' --skip-build skip `pnpm run build` (lib/ artifacts must already exist).',
|
||||
' --artifacts-only omit repository test and script typechecks from the artifact build.',
|
||||
' --dry-run print every command and config patch without executing.',
|
||||
' --help print this help.',
|
||||
'',
|
||||
@@ -281,7 +289,8 @@ class SingleExeBuild {
|
||||
console.log('build-exe-for-python-sdk: skipping pnpm run build (--skip-build)')
|
||||
return
|
||||
}
|
||||
await this.runPnpm('build', ['run', 'build'])
|
||||
const args = this.cli.artifactsOnly ? ['run', 'build', '--artifacts-only'] : ['run', 'build']
|
||||
await this.runPnpm('build', args)
|
||||
}
|
||||
|
||||
/** Clear and deploy the runtime closure into the node carrier. */
|
||||
@@ -302,6 +311,7 @@ class SingleExeBuild {
|
||||
'--config.node-linker=hoisted',
|
||||
'--config.auto-install-peers=false',
|
||||
'--config.link-workspace-packages=true',
|
||||
'--config.hoist-workspace-packages=false',
|
||||
this.staging,
|
||||
])
|
||||
await this.restoreLegacyHoists()
|
||||
@@ -364,41 +374,8 @@ class SingleExeBuild {
|
||||
console.log('build-exe-for-python-sdk: [dry-run] materialize staged package links')
|
||||
return
|
||||
}
|
||||
const nodeModules = join(this.staging, 'node_modules')
|
||||
let remaining = await this.findSymlink(nodeModules)
|
||||
while (remaining !== undefined) {
|
||||
const segments = remaining.slice(nodeModules.length + 1).split(sep)
|
||||
const binIndex = segments.lastIndexOf('.bin')
|
||||
if (binIndex >= 0) {
|
||||
await rm(join(nodeModules, ...segments.slice(0, binIndex + 1)), { recursive: true, force: true })
|
||||
remaining = await this.findSymlink(nodeModules)
|
||||
continue
|
||||
}
|
||||
const destination = remaining
|
||||
const source = await realpath(destination)
|
||||
const nestedNodeModules = join(source, 'node_modules')
|
||||
await rm(destination, { recursive: true, force: true })
|
||||
await cp(source, destination, {
|
||||
recursive: true,
|
||||
dereference: true,
|
||||
filter: path => path !== nestedNodeModules && !path.startsWith(nestedNodeModules + sep),
|
||||
})
|
||||
remaining = await this.findSymlink(nodeModules)
|
||||
}
|
||||
}
|
||||
|
||||
/** Return the first symbolic link below a directory, if one exists. */
|
||||
private async findSymlink(directory: string): Promise<string | undefined> {
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
const path = join(directory, entry.name)
|
||||
const metadata = await lstat(path)
|
||||
if (metadata.isSymbolicLink()) return path
|
||||
if (metadata.isDirectory()) {
|
||||
const nested = await this.findSymlink(path)
|
||||
if (nested !== undefined) return nested
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
await materializeStagedLinks(this.staging)
|
||||
await deduplicateStagedWorkspacePackages(this.staging, root)
|
||||
}
|
||||
|
||||
/** Add the executable entry and pkg assets to the staged manifest. */
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
describe('repository build CLI', () => {
|
||||
// The sentinel makes parseArgs fail fast if the check ever stops running first.
|
||||
it('rejects a Node process without TypeScript type stripping before any build step', () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
['--no-experimental-strip-types', '--import', 'tsx/esm', resolve(root, 'scripts/build.ts'), '--spec-sentinel'],
|
||||
{ cwd: root, encoding: 'utf8' },
|
||||
)
|
||||
expect(result.status).not.toBe(0)
|
||||
expect(result.stderr).toContain('build: Node.js TypeScript type stripping is unavailable')
|
||||
})
|
||||
|
||||
it('rejects a value for the artifacts-only flag before any build step', () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
['--import', 'tsx/esm', resolve(root, 'scripts/build.ts'), '--artifacts-only=yes'],
|
||||
{ cwd: root, encoding: 'utf8' },
|
||||
)
|
||||
expect(result.status).not.toBe(0)
|
||||
expect(result.stderr).toContain('does not take an argument')
|
||||
})
|
||||
})
|
||||
+24
-8
@@ -1,4 +1,4 @@
|
||||
/** Run the complete repository build and bind its client artifacts to their public environment. */
|
||||
/** Build repository artifacts and bind client outputs to their public environment. */
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { rmSync } from 'node:fs'
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
} from './client-build-environment.ts'
|
||||
import { pnpmInvocation } from './pnpm-invocation.ts'
|
||||
|
||||
/** Run one package script through the package manager that invoked this build. */
|
||||
function runScript(script: string, environment: NodeJS.ProcessEnv): void {
|
||||
const invocation = pnpmInvocation(['run', script], environment)
|
||||
/** Run one package-manager command with the selected public build environment. */
|
||||
function runCommand(args: readonly string[], environment: NodeJS.ProcessEnv, label: string): void {
|
||||
const invocation = pnpmInvocation(args, environment)
|
||||
const result = spawnSync(invocation.command, invocation.args, {
|
||||
cwd: resolve(import.meta.dirname, '..'),
|
||||
env: environment,
|
||||
@@ -24,14 +24,26 @@ function runScript(script: string, environment: NodeJS.ProcessEnv): void {
|
||||
})
|
||||
if (result.error !== undefined) throw result.error
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`build: ${script} exited with ${String(result.status ?? result.signal)}`)
|
||||
throw new Error(`build: ${label} exited with ${String(result.status ?? result.signal)}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** Run the full build selected by `--profile` or `DSH_BUILD_CLIENT_PROFILE`. */
|
||||
/** Run one package script through the package manager that invoked this build. */
|
||||
function runScript(script: string, environment: NodeJS.ProcessEnv): void {
|
||||
runCommand(['run', script], environment, script)
|
||||
}
|
||||
|
||||
/** Build client artifacts and optionally omit the repository test and script typechecks. */
|
||||
function main(): void {
|
||||
// tsdown.config.ts loads only through Node type stripping (`--config-loader native`); this names the cause before tsdown fails.
|
||||
if (!process.features.typescript) {
|
||||
throw new Error('build: Node.js TypeScript type stripping is unavailable in this Node.js process; remove --no-experimental-strip-types from NODE_OPTIONS or use a Node.js build with TypeScript support')
|
||||
}
|
||||
const { values } = parseArgs({
|
||||
options: { profile: { type: 'string' } },
|
||||
options: {
|
||||
profile: { type: 'string' },
|
||||
'artifacts-only': { type: 'boolean' },
|
||||
},
|
||||
allowPositionals: false,
|
||||
})
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
@@ -42,7 +54,11 @@ function main(): void {
|
||||
|
||||
rmSync(resolve(root, CLIENT_BUILD_RECORD_PATH), { force: true })
|
||||
runScript('build:native-system', buildEnvironment)
|
||||
runScript('build:lib', buildEnvironment)
|
||||
if (values['artifacts-only']) {
|
||||
runCommand(['exec', 'tsx', 'scripts/compile-referenced-projects.ts', 'libraries'], buildEnvironment, 'artifact libraries')
|
||||
} else {
|
||||
runScript('build:lib', buildEnvironment)
|
||||
}
|
||||
runScript('build:web', buildEnvironment)
|
||||
const record = writeClientBuildRecord(root, clientEnvironment)
|
||||
console.log(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
||||
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { hasExperimentalPackageReference, isExperimentalPackageName } from './experimental-package-policy.ts'
|
||||
|
||||
/** Package identities are cached only within one build. */
|
||||
export class BundleInputIsolation {
|
||||
@@ -35,7 +36,7 @@ export class BundleInputIsolation {
|
||||
}
|
||||
|
||||
private checkInput(id: string, allowMissing: boolean): void {
|
||||
if (/(?:^|[/:\u0000])@deepseek-ai\/dsh-experimental-[^/?#]+/.test(id)) {
|
||||
if (hasExperimentalPackageReference(id)) {
|
||||
throw new Error(`${this.label}: experimental input ${id}`)
|
||||
}
|
||||
const file = physicalBundleInput(id)
|
||||
@@ -54,7 +55,7 @@ export class BundleInputIsolation {
|
||||
const canonical = resolve(realpathSync(existing), relative(existing, file))
|
||||
if (canonical !== file) this.checkInput(canonical, allowMissing)
|
||||
const name = this.packageName(dirname(canonical))
|
||||
if (name?.startsWith('@deepseek-ai/dsh-experimental-')) {
|
||||
if (name !== undefined && isExperimentalPackageName(name)) {
|
||||
throw new Error(`${this.label}: ${id} belongs to experimental package ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { describe, expect, it, onTestFinished } from 'vitest'
|
||||
import {
|
||||
isPublicExperimentalPackageDirectory,
|
||||
PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES,
|
||||
EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS,
|
||||
} from './experimental-package-policy.ts'
|
||||
import {
|
||||
checkDshFamilyVersion,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
checkWorkspaceProtocol,
|
||||
checkExperimentalDependencyIsolation,
|
||||
checkExperimentalManifest,
|
||||
checkExperimentalNameExceptions,
|
||||
expectedDshPackageFiles,
|
||||
readWorkspaceManifests,
|
||||
type WorkspaceManifest,
|
||||
@@ -120,10 +122,41 @@ describe('experimental workspace constraints', () => {
|
||||
...experimental,
|
||||
manifest: { ...experimental.manifest, name: '@deepseek-ai/dsh-prototype' },
|
||||
})).toEqual([
|
||||
'@deepseek-ai/dsh-prototype: experimental package name must start with "@deepseek-ai/dsh-experimental-"',
|
||||
'@deepseek-ai/dsh-prototype: experimental package name must start with "@deepseek-ai/dsh-experimental-" or match its declared directory exception',
|
||||
])
|
||||
})
|
||||
|
||||
it.each(Object.entries(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS))('retains %s only at its declared directory', (dir, name) => {
|
||||
expect(checkExperimentalManifest({ dir, manifest: { name, publishConfig: { access: 'public' } } })).toEqual([])
|
||||
expect(checkExperimentalManifest({ ...experimental, manifest: { ...experimental.manifest, name } }))
|
||||
.toEqual([expect.stringContaining('declared directory exception')])
|
||||
})
|
||||
|
||||
it('rejects a missing manifest name even when the directory has no exception', () => {
|
||||
expect(checkExperimentalManifest({ ...experimental, manifest: { publishConfig: { access: 'public' } } }))
|
||||
.toEqual([expect.stringContaining('experimental package name must start')])
|
||||
})
|
||||
|
||||
it('rejects stale, moved and duplicated retained-name declarations', () => {
|
||||
const manifests = Object.entries(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS).map(([dir, name]) => ({ dir, manifest: { name } }))
|
||||
expect(checkExperimentalNameExceptions(manifests)).toEqual([])
|
||||
const [first, ...rest] = manifests
|
||||
for (const invalid of [
|
||||
rest,
|
||||
[{ ...first!, manifest: { name: '@deepseek-ai/dsh-experimental-replacement' } }, ...rest],
|
||||
[{ ...first!, dir: 'packages/core/replacement' }, ...rest],
|
||||
[...manifests, { ...first!, dir: 'packages/experimental/duplicate' }],
|
||||
]) expect(checkExperimentalNameExceptions(invalid)).toEqual([expect.stringContaining(first!.manifest.name)])
|
||||
})
|
||||
|
||||
it.each(Object.values(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS))('rejects retained runtime names and aliases: %s', (name) => {
|
||||
for (const dependencies of [{ [name]: 'workspace:*' }, { alias: `npm:${name}@1.0.0` }, { alias: `workspace:${name}@*` }]) {
|
||||
expect(checkExperimentalDependencyIsolation([{
|
||||
dir: 'apps/cli', manifest: { name: '@deepseek-ai/dsh', dependencies },
|
||||
}], [])).toEqual([expect.stringContaining('must not reference an experimental package')])
|
||||
}
|
||||
})
|
||||
|
||||
it('requires public metadata for unlisted experimental packages', () => {
|
||||
expect(checkExperimentalManifest(experimental)).toEqual([])
|
||||
expect(checkExperimentalManifest({
|
||||
@@ -255,9 +288,21 @@ describe('dsh family version coherence', () => {
|
||||
})
|
||||
|
||||
describe('package payload constraints', () => {
|
||||
it.each(['./art/icon.svg', 'art/icon.svg'])('includes declared icon %s in the canonical payload', (icon) => {
|
||||
expect(expectedDshPackageFiles({ icon, exports: { './locale/*.json': './locale/*.json' } })).toEqual([
|
||||
'art/icon.svg', 'locale/*.json', 'lib/index.js', 'lib/types/**/*.d.ts',
|
||||
it.each([
|
||||
['./art/icon.svg', ['art/icon.svg']],
|
||||
[{ import: './art/icon.svg', default: './art/fallback.svg' }, ['art/icon.svg', 'art/fallback.svg']],
|
||||
[['./art/icon.svg', './art/icon.svg'], ['art/icon.svg']],
|
||||
] as const)('includes exported icon targets in the canonical payload: %j', (icon, expected) => {
|
||||
expect(expectedDshPackageFiles({ exports: { './icon': icon } })).toEqual([...expected, 'lib/index.js', 'lib/types/**/*.d.ts'])
|
||||
})
|
||||
|
||||
it.each(['icon.svg', './icon.svg'])('includes and deduplicates manifest icon %s', (icon) => {
|
||||
expect(expectedDshPackageFiles({ icon, exports: { './icon': './icon.svg' } })).toEqual(['icon.svg', 'lib/index.js', 'lib/types/**/*.d.ts'])
|
||||
})
|
||||
|
||||
it('includes manifest, root, and subpath icon targets', () => {
|
||||
expect(expectedDshPackageFiles({ icon: './legacy.svg', exports: { './icon': './fallback.svg', './search/icon': './search.svg' } })).toEqual([
|
||||
'legacy.svg', 'fallback.svg', 'search.svg', 'lib/index.js', 'lib/types/**/*.d.ts',
|
||||
])
|
||||
})
|
||||
|
||||
@@ -376,6 +421,16 @@ it('requires the local speech worker and locked runtime in the published payload
|
||||
}
|
||||
})
|
||||
|
||||
it('requires the Inspector Worker, Client chunks, and mirrored DevTools resources in the published payload', () => {
|
||||
const dir = 'packages/experimental/inspector'
|
||||
const manifest = JSON.parse(readFileSync(new URL(`../${dir}/package.json`, import.meta.url), 'utf8')) as WorkspaceManifest['manifest']
|
||||
expect(checkWorkspaceManifest({ dir, manifest })).toEqual([])
|
||||
for (const omitted of ['lib/client.*.js', 'lib/worker.js', 'lib/devtools/**']) {
|
||||
expect(checkWorkspaceManifest({ dir, manifest: { ...manifest, files: manifest.files!.filter(file => file !== omitted) } }))
|
||||
.toEqual([expect.stringContaining('package.json files must be')])
|
||||
}
|
||||
})
|
||||
|
||||
it('requires the standalone shortcut protocol and rejects unrelated runtime files', () => {
|
||||
const dir = 'packages/client/shortcuts'
|
||||
const manifest = JSON.parse(readFileSync(new URL(`../${dir}/package.json`, import.meta.url), 'utf8')) as WorkspaceManifest['manifest']
|
||||
|
||||
@@ -11,6 +11,10 @@ import { pathToFileURL } from 'node:url'
|
||||
import { load as loadYaml } from 'js-yaml'
|
||||
import {
|
||||
isPublicExperimentalPackageDirectory,
|
||||
isExperimentalPackageName,
|
||||
hasExperimentalPackageReference,
|
||||
EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS,
|
||||
EXPERIMENTAL_PACKAGE_NAME_PREFIX,
|
||||
PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES,
|
||||
} from './experimental-package-policy.ts'
|
||||
import { hasTypertRemoteNavigation, isForbiddenPublicationFile } from './publication-payload.ts'
|
||||
@@ -53,8 +57,6 @@ const publicationSourceAllowlist: Readonly<Record<string, readonly string[]>> =
|
||||
const publishedRepositoryUrl = 'git+https://github.com/deepseek-ai/deepseek-harness.git'
|
||||
/** Packages that participate in the experimental policy. */
|
||||
const experimentalPackageDirectory = /^packages\/experimental\/[^/]+$/
|
||||
/** npm namespace reserved for experimental packages. */
|
||||
const experimentalPackageNamePrefix = '@deepseek-ai/dsh-experimental-'
|
||||
/** Ordinary directories whose packages this repository publishes: one release member each. */
|
||||
const standardReleaseMemberDirectory = /^(?:packages\/(?!experimental\/)[^/]+\/[^/]+|apps\/(?!desktop(?:-host)?$)[^/]+|vendor\/[^/]+)$/
|
||||
/** Installable application assembled by electron-builder rather than published to npm. */
|
||||
@@ -80,16 +82,7 @@ export interface PackageManifest {
|
||||
main?: string
|
||||
types?: string
|
||||
bin?: string | Record<string, string>
|
||||
exports?: Record<
|
||||
string,
|
||||
| string
|
||||
| {
|
||||
types?: string
|
||||
default?: string
|
||||
}
|
||||
| null
|
||||
| undefined
|
||||
>
|
||||
exports?: Record<string, ExportTarget | undefined>
|
||||
files?: string[]
|
||||
icon?: string
|
||||
publishConfig?: { access?: string }
|
||||
@@ -103,6 +96,9 @@ export interface PackageManifest {
|
||||
}
|
||||
}
|
||||
|
||||
/** Node package export target: a path, a fallback list, a conditional map, or an exclusion. */
|
||||
export type ExportTarget = string | readonly ExportTarget[] | { readonly [condition: string]: ExportTarget | undefined } | null
|
||||
|
||||
/** One workspace manifest and its repo-relative path. */
|
||||
export interface WorkspaceManifest {
|
||||
dir: string
|
||||
@@ -174,6 +170,7 @@ export function readWorkspaceManifests(repositoryRoot: string): WorkspaceManifes
|
||||
}
|
||||
|
||||
const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
|
||||
'@deepseek-ai/dsh-webhook-github': ['examples/github-review/cordis.yml', 'examples/github-review/github-ready-review-rule.mjs'],
|
||||
// Owned Worker bundles import this public bootstrap before their business entry.
|
||||
'@deepseek-ai/dsh-app-boot': ['lib/worker/profile-resolution-bootstrap.js'],
|
||||
// Statically linked client libraries keep their stylesheets next to the emitted
|
||||
@@ -194,8 +191,8 @@ const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
|
||||
'@deepseek-ai/dsh-experimental-speech-to-text-sensevoice': ['runtime/assets.json'],
|
||||
// The isolated Node bootstrap is a separately launched bundle.
|
||||
'@deepseek-ai/dsh-ptc-runtime-node': ['lib/process.js'],
|
||||
// The Host entry starts its sibling Worker by URL rather than a package export.
|
||||
'@deepseek-ai/dsh-experimental-inspector': ['lib/worker.js'],
|
||||
// The Inspector owns a Worker and a mirrored frontend outside package export paths.
|
||||
'@deepseek-ai/dsh-experimental-inspector': ['lib/client.*.js', 'lib/worker.js', 'lib/devtools/**'],
|
||||
// Creator's composition guidance travels with the declaration package.
|
||||
'@deepseek-ai/dsh-agent-preset': ['skills'],
|
||||
// The Web Host mounts the default-off settings owner independently of each
|
||||
@@ -229,6 +226,8 @@ const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
|
||||
// through a hashed chunk. The committed bin.js is the link target pnpm can
|
||||
// resolve at install time, before the build produces lib/bin.js.
|
||||
'@deepseek-ai/dsh-experimental-webworker-packer': ['bin.js', 'lib/repository-*.js'],
|
||||
// Startup and runtime share the advertised URL parser.
|
||||
'@deepseek-ai/dsh-web-app': ['lib/public-url-*.js'],
|
||||
// The headless entry and its startup row share the JSON projection code
|
||||
// through a hashed tsdown chunk; both import it by relative path.
|
||||
'@deepseek-ai/dsh-headless': ['lib/json-stream-*.js'],
|
||||
@@ -239,7 +238,7 @@ function sameStringList(actual: readonly string[] | undefined, expected: readonl
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute canonical publication patterns, including the declared icon and exported locale JSON resources.
|
||||
* Compute canonical publication patterns, including declared and exported icon paths and exported locale JSON resources.
|
||||
* @param manifest - workspace package manifest.
|
||||
* @returns the icon and deduplicated locale targets followed by runtime and declaration payloads.
|
||||
*/
|
||||
@@ -256,13 +255,17 @@ export function expectedDshPackageFiles(manifest: PackageManifest): readonly str
|
||||
...bundleFiles,
|
||||
...(manifest.name ? packageFileExtras[manifest.name] ?? [] : []),
|
||||
]
|
||||
return [
|
||||
const targets = (value: ExportTarget | undefined): string[] => typeof value === 'string' ? [value]
|
||||
: typeof value === 'object' && value !== null ? Object.values(value).flatMap(targets) : []
|
||||
const icons = [
|
||||
...typeof manifest.icon === 'string' ? [manifest.icon.replace(/^\.\//u, '')] : [],
|
||||
...Object.entries(manifest.exports ?? {}).filter(([key]) => /^\.\/(?:.+\/)?icon$/u.test(key))
|
||||
.flatMap(([, target]) => targets(target)).filter(icon => icon.startsWith('./')).map(icon => icon.slice(2)),
|
||||
]
|
||||
return [
|
||||
...new Set(icons),
|
||||
...[...localeFiles].sort(),
|
||||
'lib/index.js',
|
||||
// Packages with an invariant export publish its runtime as a separate
|
||||
// bundle; the package-invariant gate validates the source/export pairing.
|
||||
...manifest.exports?.['./invariant'] ? ['lib/invariant.js'] : [],
|
||||
...manifest.bin ? ['lib/bin.js'] : [],
|
||||
// Worker-thread packages ship a CJS worker entry; the browser worker
|
||||
// bundle is an ES module a page loads with `new Worker(type: 'module')`.
|
||||
@@ -300,6 +303,15 @@ export function expectedDshPackageFiles(manifest: PackageManifest): readonly str
|
||||
]
|
||||
}
|
||||
|
||||
/** Fields of a conditional export; scalar and list targets have no named conditions. */
|
||||
function exportFields(value: ExportTarget | undefined): { readonly [condition: string]: ExportTarget | undefined } | undefined {
|
||||
return typeof value !== 'object' || value === null || isExportList(value) ? undefined : value
|
||||
}
|
||||
|
||||
function isExportList(value: ExportTarget): value is readonly ExportTarget[] {
|
||||
return Array.isArray(value)
|
||||
}
|
||||
|
||||
/** Whether one conditional export exactly names the generated runtime and declaration pair. */
|
||||
function hasExportPair(
|
||||
manifest: PackageManifest,
|
||||
@@ -307,19 +319,16 @@ function hasExportPair(
|
||||
types: string,
|
||||
runtime: string,
|
||||
): boolean {
|
||||
const entry = manifest.exports?.[subpath]
|
||||
return typeof entry === 'object'
|
||||
&& entry !== null
|
||||
&& entry.types === types
|
||||
&& entry.default === runtime
|
||||
const entry = exportFields(manifest.exports?.[subpath])
|
||||
return entry?.types === types && entry.default === runtime
|
||||
}
|
||||
|
||||
/** Runtime target of an export entry: conditional `default`, or the bare-string shorthand. */
|
||||
function exportDefault(manifest: PackageManifest, subpath: string): string | undefined {
|
||||
const entry = manifest.exports?.[subpath]
|
||||
if (typeof entry === 'string') return entry
|
||||
if (typeof entry === 'object' && entry !== null) return entry.default
|
||||
return undefined
|
||||
const target = exportFields(entry)?.default
|
||||
return typeof target === 'string' ? target : undefined
|
||||
}
|
||||
|
||||
/** Whether any export's runtime default points into the tsc-emitted lib/types tree. */
|
||||
@@ -336,8 +345,9 @@ export function checkExperimentalManifest(
|
||||
if (!experimentalPackageDirectory.test(dir)) return []
|
||||
const label = manifest.name ?? dir
|
||||
const errors: string[] = []
|
||||
if (manifest.name?.startsWith(experimentalPackageNamePrefix) !== true) {
|
||||
errors.push(`${label}: experimental package name must start with ${JSON.stringify(experimentalPackageNamePrefix)}`)
|
||||
if (manifest.name === undefined || !manifest.name.startsWith(EXPERIMENTAL_PACKAGE_NAME_PREFIX)
|
||||
&& manifest.name !== EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS[dir]) {
|
||||
errors.push(`${label}: experimental package name must start with ${JSON.stringify(EXPERIMENTAL_PACKAGE_NAME_PREFIX)} or match its declared directory exception`)
|
||||
}
|
||||
if (isPublicExperimentalPackageDirectory(dir, privateDirectories)) {
|
||||
if (manifest.private === true) errors.push(`${label}: public experimental package must not set "private": true`)
|
||||
@@ -351,6 +361,22 @@ export function checkExperimentalManifest(
|
||||
return errors
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject missing, renamed, moved, or duplicated retained-name exceptions.
|
||||
* @param manifests - every workspace manifest, with repository-relative directories.
|
||||
* @returns Violations of the exact directory/name ownership declarations.
|
||||
*/
|
||||
export function checkExperimentalNameExceptions(manifests: readonly WorkspaceManifest[]): string[] {
|
||||
const errors: string[] = []
|
||||
for (const [dir, name] of Object.entries(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS)) {
|
||||
const owners = manifests.filter(entry => entry.manifest.name === name)
|
||||
if (owners.length !== 1 || owners[0]?.dir !== dir) {
|
||||
errors.push(`${dir}: retained experimental name ${name} must have exactly one workspace owner at this directory`)
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
function isReleaseMemberDirectory(dir: string): boolean {
|
||||
return standardReleaseMemberDirectory.test(dir) || isPublicExperimentalPackageDirectory(dir)
|
||||
}
|
||||
@@ -479,25 +505,13 @@ export function checkWorkspaceManifest({ dir, manifest }: WorkspaceManifest): st
|
||||
if (manifest.types !== 'lib/types/index.d.ts') {
|
||||
errors.push(`${label}: package.json must set "types": "lib/types/index.d.ts"`)
|
||||
}
|
||||
const rootExport = manifest.exports?.['.']
|
||||
const rootEntry = typeof rootExport === 'object' && rootExport !== null ? rootExport : undefined
|
||||
const rootEntry = exportFields(manifest.exports?.['.'])
|
||||
if (rootEntry?.types !== './lib/types/index.d.ts') {
|
||||
errors.push(`${label}: package.json exports["."].types must be "./lib/types/index.d.ts"`)
|
||||
}
|
||||
if (rootEntry?.default !== './lib/index.js') {
|
||||
errors.push(`${label}: package.json exports["."].default must be "./lib/index.js"`)
|
||||
}
|
||||
const invariantRaw = manifest.exports?.['./invariant']
|
||||
const invariantExport = typeof invariantRaw === 'object' && invariantRaw !== null ? invariantRaw : undefined
|
||||
if (invariantExport?.types !== undefined && invariantExport.types !== './lib/types/invariant.d.ts') {
|
||||
errors.push(`${label}: package.json exports["./invariant"].types must be "./lib/types/invariant.d.ts"`)
|
||||
}
|
||||
if (invariantExport?.default !== undefined && invariantExport.default !== './lib/invariant.js') {
|
||||
errors.push(`${label}: package.json exports["./invariant"].default must be "./lib/invariant.js"`)
|
||||
}
|
||||
if (invariantExport && (invariantExport.types === undefined || invariantExport.default === undefined)) {
|
||||
errors.push(`${label}: package.json exports["./invariant"] must declare both types and default targets`)
|
||||
}
|
||||
const expectedFiles = expectedDshPackageFiles(manifest)
|
||||
if (!sameStringList(manifest.files, expectedFiles)) {
|
||||
errors.push(`${label}: package.json files must be ${JSON.stringify(expectedFiles)}`)
|
||||
@@ -565,8 +579,10 @@ export function checkExperimentalDependencyIsolation(
|
||||
if (!standardReleaseMemberDirectory.test(dir) && dir !== 'python/sdk-runtime') continue
|
||||
const offered = manifest.name === '@deepseek-ai/dsh' ? new Set(optionalBundles) : new Set<string>()
|
||||
for (const section of runtimeDependencySections) {
|
||||
for (const name of Object.keys(manifest[section] ?? {})) {
|
||||
if (!experimentalNames.has(name)) continue
|
||||
for (const [name, range] of Object.entries(manifest[section] ?? {})) {
|
||||
const experimental = experimentalNames.has(name) || isExperimentalPackageName(name)
|
||||
|| /^(?:npm:|workspace:)/.test(range) && hasExperimentalPackageReference(range)
|
||||
if (!experimental) continue
|
||||
if (section === 'dependencies' && offered.has(name)) continue
|
||||
errors.push(`${manifest.name ?? dir}: ${section}.${name} must not reference an experimental package`)
|
||||
}
|
||||
@@ -613,6 +629,7 @@ export function main(): void {
|
||||
...checkRepositoryVersion(),
|
||||
...workspaceManifests().flatMap(checkWorkspaceManifest),
|
||||
...checkWorkspaceProtocol(manifests),
|
||||
...checkExperimentalNameExceptions(manifests),
|
||||
...checkExperimentalDependencyIsolation(manifests),
|
||||
...checkHierarchyShape(),
|
||||
...collectProjectReferenceFaceViolations(root),
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('Node compatibility self-hosted routing', () => {
|
||||
})
|
||||
|
||||
it('isolates version installs and enables hosted package caching only on hosted runners', () => {
|
||||
const setup = job.steps.find(step => step.uses === 'actions/setup-node@v6')!
|
||||
const setup = job.steps.find(step => step.uses === 'actions/setup-node@v7.0.0')!
|
||||
expect(setup.env).toEqual({
|
||||
NODE_OPTIONS: "${{ runner.environment == 'self-hosted' && '--import=./scripts/ci-compatible-toolcache.mjs' || '' }}",
|
||||
})
|
||||
@@ -100,7 +100,7 @@ describe('Node compatibility self-hosted routing', () => {
|
||||
})
|
||||
|
||||
it('overrides runner exports inside setup-node without affecting later Node processes', () => {
|
||||
const setup = job.steps.find(step => step.uses === 'actions/setup-node@v6')!
|
||||
const setup = job.steps.find(step => step.uses === 'actions/setup-node@v7.0.0')!
|
||||
const nodeOptions = evaluate(setup.env!.NODE_OPTIONS!, { runner: { environment: 'self-hosted' } }) as string
|
||||
const root = mkdtempSync(join(tmpdir(), 'ci-compatible preload-'))
|
||||
try {
|
||||
@@ -150,7 +150,7 @@ describe('Node compatibility self-hosted routing', () => {
|
||||
const index = job.steps.findIndex(step => step.name === 'Isolate compatibility caches')
|
||||
const step = job.steps[index]!
|
||||
expect(index).toBeGreaterThan(0)
|
||||
expect(index).toBeLessThan(job.steps.findIndex(candidate => candidate.uses === 'pnpm/action-setup@v4'))
|
||||
expect(index).toBeLessThan(job.steps.findIndex(candidate => candidate.uses === 'pnpm/action-setup@v5.0.0'))
|
||||
expect(step.if).toBe("runner.environment == 'self-hosted'")
|
||||
const root = mkdtempSync(join(tmpdir(), 'ci-compatible-selfhosted-'))
|
||||
try {
|
||||
|
||||
+190
-6
@@ -14,6 +14,131 @@ const runnerPrivatePnpmDestination = /^\$\{\{ runner\.temp \}\}\/setup-pnpm-\$\{
|
||||
const nativeWindowsPnpmDestination = '${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}'
|
||||
|
||||
describe('CI workflow', () => {
|
||||
it('keeps at most ten worker jobs active while runtime targets follow the SDK suite', () => {
|
||||
const ci = loadWorkflow('.github/workflows/ci.yml')
|
||||
const compatibility = workflowJob(ci, 'node-compat')
|
||||
const runtime = workflowJob(ci, 'python-runtime')
|
||||
if (!isRecord(ci.jobs) || !isRecord(compatibility.strategy) || !isRecord(runtime.with)) {
|
||||
throw new TypeError('CI must define jobs, compatibility scheduling, and runtime inputs')
|
||||
}
|
||||
const compatibilityLimit = compatibility.strategy['max-parallel']
|
||||
const targets = runtime.with.targets
|
||||
if (typeof compatibilityLimit !== 'number' || typeof targets !== 'string') {
|
||||
throw new TypeError('CI requires a numeric compatibility limit and explicit runtime targets')
|
||||
}
|
||||
expect(Number.isInteger(compatibilityLimit)).toBe(true)
|
||||
expect(compatibilityLimit).toBeGreaterThan(0)
|
||||
expect(compatibility.strategy).toMatchObject({
|
||||
'fail-fast': false,
|
||||
matrix: { include: [{ node: '22.19' }, { node: '24.9' }, { node: 26 }] },
|
||||
})
|
||||
expect(workflowJob(ci, 'all-checks-passed').needs).toContain('node-compat')
|
||||
const ordinaryJobs = Object.entries(ci.jobs)
|
||||
.filter(([name]) => !['node-compat', 'python-runtime', 'all-checks-passed'].includes(name))
|
||||
for (const [name, job] of ordinaryJobs) {
|
||||
if (!isRecord(job)) throw new TypeError(`${name} must define a job`)
|
||||
expect(job).not.toHaveProperty('strategy')
|
||||
expect(job).not.toHaveProperty('uses')
|
||||
}
|
||||
expect(ordinaryJobs.map(([name]) => name)).toContain('python-sdk')
|
||||
expect(runtime.needs).toBe('python-sdk')
|
||||
expect(runtime.strategy).toBeUndefined()
|
||||
expect(runtime.with).toMatchObject({ targets: 'node24-linux-x64,node24-win-x64' })
|
||||
expect(runtime.uses).toBe('./.github/workflows/build-exe-for-python-sdk.yml')
|
||||
|
||||
const builder = loadWorkflow('.github/workflows/build-exe-for-python-sdk.yml')
|
||||
if (!isRecord(builder.jobs)) throw new TypeError('Python runtime builder must define jobs')
|
||||
expect(Object.keys(builder.jobs).sort()).toEqual(['build', 'plan', 'sdk-wheel'])
|
||||
const plan = workflowJob(builder, 'plan')
|
||||
const wheel = workflowJob(builder, 'sdk-wheel')
|
||||
const build = workflowJob(builder, 'build')
|
||||
expect(plan.needs).toBeUndefined()
|
||||
expect(plan.strategy).toBeUndefined()
|
||||
expect(plan.uses).toBeUndefined()
|
||||
expect(wheel.needs).toBe('plan')
|
||||
expect(wheel.strategy).toBeUndefined()
|
||||
expect(wheel.uses).toBeUndefined()
|
||||
expect(build.needs).toEqual(['plan', 'sdk-wheel'])
|
||||
expect(build.uses).toBeUndefined()
|
||||
if (!isRecord(build.strategy)) throw new TypeError('Python runtime build must define its target matrix')
|
||||
expect(build.strategy.matrix).toEqual({ include: '${{ fromJSON(needs.plan.outputs.matrix) }}' })
|
||||
|
||||
const beforeSdk = ordinaryJobs.length + compatibilityLimit
|
||||
const afterSdk = ordinaryJobs.length - 1 + compatibilityLimit + Math.max(1, targets.split(',').length)
|
||||
expect(beforeSdk).toBeLessThanOrEqual(10)
|
||||
expect(afterSdk).toBeLessThanOrEqual(10)
|
||||
})
|
||||
|
||||
it('retains aggregate diagnostics in required builds and protected publication', () => {
|
||||
const ci = loadWorkflow('.github/workflows/ci.yml')
|
||||
for (const [name, command] of [
|
||||
['node-24-consumers', 'pnpm run check:ci:consumers'],
|
||||
['windows-build', 'pnpm run check:ci:windows-blocking'],
|
||||
] as const) {
|
||||
const job = workflowJob(ci, name)
|
||||
expect(job.steps).toContainEqual(expect.objectContaining({ run: command }))
|
||||
}
|
||||
for (const [file, command] of [
|
||||
['release-publish.yml', 'pnpm run build:official'],
|
||||
['release-vendor-publish.yml', 'pnpm run build:lib:host'],
|
||||
] as const) {
|
||||
expect(workflowJob(loadWorkflow('.github/workflows/' + file), 'pack').steps)
|
||||
.toContainEqual(expect.objectContaining({ name: 'Build', run: command }))
|
||||
}
|
||||
const scripts = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as { scripts: Record<string, string> }
|
||||
expect(scripts.scripts['build:lib:host']).toContain('tsc -b tsconfig.host.json')
|
||||
expect(scripts.scripts['build:lib:client']).toContain('tsc -b tsconfig.client.json')
|
||||
expect(scripts.scripts.typecheck).toBe('npm run build:lib:host && npm run typecheck:contracts-ready')
|
||||
})
|
||||
|
||||
it('selects artifact builds only for PR release and API rehearsals', () => {
|
||||
const official = "${{ github.event_name == 'pull_request' && 'pnpm run build --artifacts-only --profile official' || 'pnpm run build:official' }}"
|
||||
const vendor = "${{ github.event_name == 'pull_request' && 'pnpm exec tsx scripts/compile-referenced-projects.ts host-libraries' || 'pnpm run build:lib:host' }}"
|
||||
for (const [file, jobName, expected] of [
|
||||
['release.yml', 'pack', official],
|
||||
['release-vendor.yml', 'pack', vendor],
|
||||
['e2e.yml', 'e2e', official],
|
||||
] as const) {
|
||||
const job = workflowJob(loadWorkflow('.github/workflows/' + file), jobName)
|
||||
if (!Array.isArray(job.steps)) throw new TypeError(`${jobName} must define steps`)
|
||||
const step = job.steps.filter(isRecord).find(candidate => typeof candidate.run === 'string'
|
||||
&& (candidate.run.includes('--artifacts-only') || candidate.run.includes('host-libraries')))
|
||||
expect(step?.run).toBe(expected)
|
||||
for (const event of ['pull_request', 'push', 'workflow_dispatch']) {
|
||||
const expression = expected.slice('${{ '.length, -' }}'.length)
|
||||
const selected: unknown = runInNewContext(expression, { github: { event_name: event } }, { timeout: 1000 })
|
||||
expect(selected).toBe(event === 'pull_request'
|
||||
? file === 'release-vendor.yml'
|
||||
? 'pnpm exec tsx scripts/compile-referenced-projects.ts host-libraries'
|
||||
: 'pnpm run build --artifacts-only --profile official'
|
||||
: file === 'release-vendor.yml' ? 'pnpm run build:lib:host' : 'pnpm run build:official')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('selects PR artifact compilation inside the Python executable builder', () => {
|
||||
const job = workflowJob(loadWorkflow('.github/workflows/build-exe-for-python-sdk.yml'), 'build')
|
||||
if (!Array.isArray(job.steps)) throw new TypeError('Python runtime builder must define steps')
|
||||
const steps = job.steps.filter(isRecord)
|
||||
const execute = steps.find(step => step.name === 'Build single-exe')
|
||||
expect(steps.some(step => step.run === 'pnpm run build --artifacts-only')).toBe(false)
|
||||
expect(execute).toMatchObject({
|
||||
env: { DSH_BUILD_CLIENT_PROFILE: 'official' },
|
||||
})
|
||||
if (typeof execute?.run !== 'string') throw new TypeError('Python executable builder must define a command')
|
||||
const expression = [...execute.run.matchAll(/\$\{\{ ([^{}]+) \}\}/g)]
|
||||
.map(match => match[1]).find(value => value?.startsWith('inputs.ci'))
|
||||
expect(expression).toBe("inputs.ci && github.event_name == 'pull_request' && '--artifacts-only' || ''")
|
||||
if (expression === undefined) throw new TypeError('Python executable builder must select its build mode')
|
||||
for (const ci of [false, true]) {
|
||||
for (const event of ['pull_request', 'push', 'workflow_dispatch']) {
|
||||
const context = { inputs: { ci }, github: { event_name: event } }
|
||||
const selected: unknown = runInNewContext(expression, context, { timeout: 1000 })
|
||||
expect(selected).toBe(ci && event === 'pull_request' ? '--artifacts-only' : '')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('prepares confinement before Node compatibility smokes', () => {
|
||||
const job = workflowJob(loadWorkflow('.github/workflows/ci.yml'), 'node-compat')
|
||||
if (!Array.isArray(job.steps)) throw new TypeError('Node compatibility job must define steps')
|
||||
@@ -67,15 +192,43 @@ describe('CI workflow', () => {
|
||||
const coverage = workflowJob(workflow, 'windows-coverage')
|
||||
expect(coverage.name).toBe('windows node 26 / coverage')
|
||||
expect(coverage.steps).toContainEqual(expect.objectContaining({
|
||||
uses: 'actions/setup-node@v6', with: { 'node-version': '26.10.0' },
|
||||
uses: 'actions/setup-node@v7.0.0', with: { 'node-version': '26.10.0' },
|
||||
}))
|
||||
for (const name of ['windows-build', 'windows-native-tests']) {
|
||||
expect(workflowJob(workflow, name).steps).toContainEqual(expect.objectContaining({
|
||||
uses: 'actions/setup-node@v6', with: { 'node-version': '${{ env.PRIMARY_NODE_VERSION }}' },
|
||||
uses: 'actions/setup-node@v7.0.0', with: { 'node-version': '${{ env.PRIMARY_NODE_VERSION }}' },
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
it.each([
|
||||
['node-24-coverage', 'Linux', 'LINUX', 'pnpm run check:ci:coverage'],
|
||||
['windows-coverage', 'Windows', 'WINDOWS', 'pnpm run check:ci:coverage'],
|
||||
])('reuses %s coverage timings only within its platform and runner pool', (jobName, platform, variable, command) => {
|
||||
const job = workflowJob(loadWorkflow('.github/workflows/ci.yml'), jobName)
|
||||
if (!Array.isArray(job.steps)) throw new TypeError(`${jobName} must define steps`)
|
||||
const steps = job.steps.filter(isRecord)
|
||||
const checkout = steps.findIndex(step => step.uses === 'actions/[email protected]')
|
||||
const restore = steps.findIndex(step => step.name === 'Restore coverage duration history')
|
||||
const coverage = steps.findIndex(step => step.run === command)
|
||||
const save = steps.findIndex(step => step.name === 'Save coverage duration history')
|
||||
const prefix = `coverage-times-${platform}-\${{ runner.environment }}-\${{ vars.DSH_CI_FAILOVER_${variable} || 'enterprise' }}-`
|
||||
const key = prefix + '${{ github.run_id }}-${{ github.run_attempt }}'
|
||||
expect(checkout).toBeGreaterThanOrEqual(0)
|
||||
expect(restore).toBeGreaterThan(checkout)
|
||||
expect(coverage).toBeGreaterThan(restore)
|
||||
expect(save).toBeGreaterThan(coverage)
|
||||
expect(steps[restore]).toMatchObject({
|
||||
uses: 'actions/cache/[email protected]',
|
||||
with: { path: '.coverage-times.json', key, 'restore-keys': `${prefix}\n` },
|
||||
})
|
||||
expect(steps[save]).toMatchObject({
|
||||
uses: 'actions/cache/[email protected]',
|
||||
if: '${{ !cancelled() }}',
|
||||
with: { path: '.coverage-times.json', key },
|
||||
})
|
||||
})
|
||||
|
||||
it('isolates every pnpm action setup destination per runner', () => {
|
||||
const files = ['.github/workflows/ci.yml', '.github/workflows/ci-master.yml']
|
||||
const setups: Array<{ jobName: string; step: unknown }> = []
|
||||
@@ -122,7 +275,7 @@ describe('CI workflow', () => {
|
||||
if (jobName === 'node-24-consumers') {
|
||||
const browserCache: unknown = job.steps.find(step => isRecord(step) && isRecord(step.with)
|
||||
&& step.with.path === '${{ env.PLAYWRIGHT_BROWSERS_PATH }}')
|
||||
expect(browserCache).toMatchObject({ uses: 'actions/cache/restore@v4' })
|
||||
expect(browserCache).toMatchObject({ uses: 'actions/cache/restore@v6.1.0' })
|
||||
}
|
||||
const store: unknown = job.steps.find(step => isRecord(step) && step.name === 'Configure pnpm store path')
|
||||
expect(store).toMatchObject({
|
||||
@@ -484,7 +637,7 @@ describe('CI workflow', () => {
|
||||
it('always restores the hosted benchmark pnpm cache', () => {
|
||||
const benchmark = workflowJob(loadWorkflow('.github/workflows/ci.yml'), 'node-24-bench')
|
||||
if (!Array.isArray(benchmark.steps)) throw new TypeError('benchmark job must define steps')
|
||||
const caches = benchmark.steps.filter(step => isRecord(step) && step.uses === 'actions/cache/restore@v4')
|
||||
const caches = benchmark.steps.filter(step => isRecord(step) && step.uses === 'actions/cache/restore@v6.1.0')
|
||||
|
||||
expect(caches).toHaveLength(1)
|
||||
expect(caches[0]).not.toHaveProperty('if')
|
||||
@@ -598,7 +751,7 @@ describe('CI workflow', () => {
|
||||
// Removing this injection would send every pnpm call in the lane (setup,
|
||||
// store-path probe, install, and the gate) back to the root partition's
|
||||
// /tmp; rationale in
|
||||
// .agents/notes/implemented/process/2026-08-28-ci-node-compile-cache-data-disk.md.
|
||||
// .github/workflows/ci.yml.
|
||||
expect(redirectStepIndex, `${jobKey} must inject NODE_COMPILE_CACHE into GITHUB_ENV`).toBeGreaterThan(-1)
|
||||
const pnpmSetupIndex = job.steps.findIndex((step): step is Record<string, unknown> & { uses: string } => (
|
||||
isRecord(step) && typeof step.uses === 'string' && step.uses.includes('pnpm/action-setup')
|
||||
@@ -880,7 +1033,7 @@ describe('Python release workflows', () => {
|
||||
expect(manylinuxAddon).toMatchObject({ if: "runner.os == 'Linux'" })
|
||||
expect(JSON.stringify(manylinuxAddon)).toContain('manylinux_2_28_x86_64')
|
||||
expect(JSON.stringify(manylinuxAddon)).toContain('manylinux_2_28_aarch64')
|
||||
expect(JSON.stringify(manylinuxAddon)).toContain('npm_config_build_from_source=true pnpm run install')
|
||||
expect(JSON.stringify(manylinuxAddon)).toContain('npm_config_build_from_source=true pnpm --config.verifyDepsBeforeRun=false run install')
|
||||
expect(JSON.stringify(manylinuxAddon)).toContain('pnpm_setup_root')
|
||||
expect(JSON.stringify(manylinuxAddon)).toContain('$pnpm_setup_root:$pnpm_setup_root:ro')
|
||||
expect(JSON.stringify(manylinuxAddon)).toContain('node-pty-glibc-versions.txt')
|
||||
@@ -1146,6 +1299,37 @@ describe('Issue lifecycle workflow', () => {
|
||||
})
|
||||
|
||||
describe('npm release workflows', () => {
|
||||
it('passes the optional vendor channel as a quoted argument while preserving default publication', () => {
|
||||
const workflow = loadWorkflow('.github/workflows/release-vendor-publish.yml')
|
||||
expect(workflow.on).toMatchObject({
|
||||
workflow_dispatch: {
|
||||
inputs: {
|
||||
'dist-tag': {
|
||||
required: false,
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
const publish = workflowJob(workflow, 'publish')
|
||||
expect(publish.steps).toContainEqual({
|
||||
name: 'Publish tarballs',
|
||||
env: {
|
||||
NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}',
|
||||
RELEASE_DIST_TAG: '${{ inputs.dist-tag }}',
|
||||
},
|
||||
run: [
|
||||
'args=()',
|
||||
'if [[ -n "$RELEASE_DIST_TAG" ]]; then',
|
||||
' args+=(--dist-tag "$RELEASE_DIST_TAG")',
|
||||
'fi',
|
||||
'pnpm run release:publish --family vendor --from dist/npm-vendor "${args[@]}"',
|
||||
'',
|
||||
].join('\n'),
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps publication dispatch-only and pack in the PR workflow', () => {
|
||||
// pack stays in the PR/master release workflows so a PR proves the set packs.
|
||||
for (const file of ['release.yml', 'release-vendor.yml']) {
|
||||
|
||||
@@ -234,7 +234,6 @@ describe('client build environment', () => {
|
||||
|
||||
const configs = clientBundle('@deepseek-ai/dsh-client-ui-sidebar', [
|
||||
'lib/types/index.js',
|
||||
'lib/types/invariant.js',
|
||||
])({ env: { DSH_BUILD_FACE: 'client' } })
|
||||
if (!Array.isArray(configs)) throw new TypeError('client bundle config must be an array')
|
||||
const dynamic = configs.find(config => config.name === '@deepseek-ai/dsh-client-ui-sidebar/client')
|
||||
|
||||
@@ -17,7 +17,7 @@ interface CssPlugin {
|
||||
function cssPlugin(name: 'dsh-css-modules-inline' | 'dsh-css-global-inline' | 'dsh-css-text-inline'): CssPlugin {
|
||||
const configs = clientBundle(
|
||||
'@deepseek-ai/dsh-client-test',
|
||||
['lib/types/index.js', 'lib/types/invariant.js'],
|
||||
['lib/types/index.js'],
|
||||
)({ env: { DSH_BUILD_FACE: 'client' } })
|
||||
const client = configs.find(config => config.platform === 'browser')
|
||||
if (client === undefined) throw new Error('client config missing')
|
||||
|
||||
@@ -39,7 +39,7 @@ interface InputIsolationPlugin {
|
||||
const REQUESTING_PACKAGE = '@deepseek-ai/dsh-client-ui-conversation'
|
||||
|
||||
function clientConfigs(id = REQUESTING_PACKAGE) {
|
||||
return clientBundle(id, ['lib/types/index.js', 'lib/types/invariant.js'])(
|
||||
return clientBundle(id, ['lib/types/index.js'])(
|
||||
{ env: { DSH_BUILD_FACE: 'client' } },
|
||||
).filter(config => config.platform === 'browser')
|
||||
}
|
||||
@@ -89,8 +89,6 @@ function clientSourceMapPath(packagePath: string): string {
|
||||
}
|
||||
|
||||
function purityResolveId(id = REQUESTING_PACKAGE): ResolveId {
|
||||
// libEntry is spelled at every call site (no default) so the
|
||||
// package-invariants text check can see the invariant entry per package.
|
||||
const configs = clientConfigs(id)
|
||||
const plugins = (configs[0] as { plugins: { name: string; resolveId?: unknown }[] }).plugins
|
||||
const gate = plugins.find(p => p.name === 'dsh-client-bundle-purity')
|
||||
@@ -271,6 +269,15 @@ describe('client bundle experimental input isolation', () => {
|
||||
expect(existsSync(join(owner, 'lib/index.js'))).toBe(false)
|
||||
})
|
||||
|
||||
it.each(['static', 'dynamic'] as const)('keeps retained-name experimental %s inputs outside ordinary artifacts', async (kind) => {
|
||||
const { root, owner, entry, prototype } = fixture()
|
||||
const name = '@deepseek-ai/dsh-tool-terminal'
|
||||
writeFileSync(join(root, 'prototype/package.json'), JSON.stringify({ name, type: 'module' }))
|
||||
writeFileSync(entry, `export { marker } from ${JSON.stringify(importPath(entry, prototype))}\n`)
|
||||
await expect(bundle(owner, config(kind))).rejects.toThrow(/client bundle isolation.*experimental/)
|
||||
await expect(bundle(owner, config(kind, name))).resolves.toContain('experimental sentinel')
|
||||
})
|
||||
|
||||
it('proves the static library otherwise hides the experimental input in its emitted JavaScript', async () => {
|
||||
const { owner, entry, prototype } = fixture()
|
||||
writeFileSync(entry, `export { marker } from ${JSON.stringify(importPath(entry, prototype))}\n`)
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { globSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { describe, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { buildLibraryArtifacts, compileReferencedProjects } from './compile-referenced-projects.ts'
|
||||
import { runCiBench } from './run-ci-bench.ts'
|
||||
import { removeFixtureSafely } from './test-fixture-cleanup.ts'
|
||||
|
||||
const compiler = fileURLToPath(import.meta.resolve('typescript/bin/tsc'))
|
||||
|
||||
function fixture(): { root: string; leaf: string } {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-referenced-projects-'))
|
||||
onTestFinished(() => { removeFixtureSafely(root) })
|
||||
const leaf = join(root, 'leaf package')
|
||||
mkdirSync(join(leaf, 'src'), { recursive: true })
|
||||
writeFileSync(join(root, 'package.json'), JSON.stringify({ type: 'module' }))
|
||||
writeFileSync(join(leaf, 'src/index.ts'), 'export const value: number = 42\n')
|
||||
const compilerOptions = {
|
||||
target: 'ES2024',
|
||||
module: 'NodeNext',
|
||||
moduleResolution: 'NodeNext',
|
||||
lib: ['ES2024'],
|
||||
types: [],
|
||||
composite: true,
|
||||
strict: true,
|
||||
}
|
||||
writeFileSync(join(leaf, 'tsconfig.json'), JSON.stringify({
|
||||
compilerOptions: {
|
||||
...compilerOptions,
|
||||
rootDir: 'src',
|
||||
outDir: 'lib/types',
|
||||
declaration: true,
|
||||
declarationMap: true,
|
||||
sourceMap: true,
|
||||
},
|
||||
include: ['src'],
|
||||
}))
|
||||
writeFileSync(join(root, 'aggregate.ts'), 'import { value } from "./leaf package/src/index.js"\nexport const aggregate: number = value\n')
|
||||
for (const face of ['host', 'client']) {
|
||||
writeFileSync(join(root, `tsconfig.${face}.json`), JSON.stringify({
|
||||
compilerOptions: { ...compilerOptions, noEmit: true },
|
||||
include: ['aggregate.ts'],
|
||||
references: [{ path: './leaf package' }],
|
||||
}))
|
||||
}
|
||||
return { root, leaf }
|
||||
}
|
||||
|
||||
function artifacts(leaf: string): Record<string, string> {
|
||||
const output = join(leaf, 'lib')
|
||||
return Object.fromEntries(globSync('**/*', { cwd: output })
|
||||
.filter(path => statSync(join(output, path)).isFile())
|
||||
.sort()
|
||||
.map(path => [path.replaceAll('\\', '/'), readFileSync(join(output, path)).toString('base64')]))
|
||||
}
|
||||
|
||||
function recordedCommands(root: string, failHostBundle = false): () => { args: string[]; title: string }[] {
|
||||
const log = join(root, 'commands.jsonl')
|
||||
const entrypoint = join(root, 'pnpm', 'bin', 'pnpm.cjs')
|
||||
mkdirSync(join(root, 'pnpm', 'bin'), { recursive: true })
|
||||
writeFileSync(entrypoint, [
|
||||
'const { appendFileSync } = require("node:fs")',
|
||||
'const args = process.argv.slice(2)',
|
||||
`appendFileSync(${JSON.stringify(log)}, JSON.stringify({ args, title: process.env.DSH_CLIENT_TITLE }) + "\\n")`,
|
||||
...failHostBundle ? ['if (args.includes("host")) process.exit(7)'] : [],
|
||||
].join('\n') + '\n')
|
||||
vi.stubEnv('npm_execpath', entrypoint)
|
||||
vi.stubEnv('DSH_CLIENT_TITLE', 'Inherited Build Title')
|
||||
onTestFinished(() => { vi.unstubAllEnvs() })
|
||||
return () => readFileSync(log, 'utf8').trim().split('\n')
|
||||
.map(line => JSON.parse(line) as { args: string[]; title: string })
|
||||
}
|
||||
|
||||
describe('internal artifact build order', () => {
|
||||
it('resolves pnpm for a direct CLI without npm_execpath and preserves its environment', () => {
|
||||
const { root } = fixture()
|
||||
const marker = join(root, 'direct-cli-title.txt')
|
||||
const environment: NodeJS.ProcessEnv = { ...process.env, DSH_CLIENT_TITLE: 'Direct CLI Title' }
|
||||
delete environment.npm_execpath
|
||||
const helper = pathToFileURL(resolve(import.meta.dirname, 'compile-referenced-projects.ts')).href
|
||||
const code = `import { runPnpmCommand } from ${JSON.stringify(helper)}; runPnpmCommand(${JSON.stringify(root)}, ${JSON.stringify([
|
||||
'exec', process.execPath, '-e',
|
||||
`require('node:fs').writeFileSync(${JSON.stringify(marker)}, process.env.DSH_CLIENT_TITLE)`,
|
||||
])})`
|
||||
const result = spawnSync(process.execPath, [
|
||||
'--import', import.meta.resolve('tsx/esm'),
|
||||
'--input-type=module', '-e', code,
|
||||
], { cwd: root, env: environment, encoding: 'utf8' })
|
||||
|
||||
expect(result.status, result.stdout + result.stderr).toBe(0)
|
||||
expect(readFileSync(marker, 'utf8')).toBe('Direct CLI Title')
|
||||
})
|
||||
|
||||
it.each([true, false])('preserves bundler order and the public environment with hostOnly=%s', (hostOnly) => {
|
||||
const { root } = fixture()
|
||||
const commands = recordedCommands(root)
|
||||
|
||||
buildLibraryArtifacts(root, hostOnly)
|
||||
|
||||
const expected = [
|
||||
['exec', 'tsdown', '--config-loader', 'native', '--env.DSH_BUILD_FACE', 'host'],
|
||||
['--filter', '@deepseek-ai/dsh-desktop', 'run', 'bundle'],
|
||||
...hostOnly ? [] : [['exec', 'tsdown', '--config-loader', 'native', '--env.DSH_BUILD_FACE', 'client']],
|
||||
]
|
||||
expect(commands()).toEqual(expected.map(args => ({ args, title: 'Inherited Build Title' })))
|
||||
})
|
||||
|
||||
it('stops after a failed Host bundler before Desktop and Client builds', () => {
|
||||
const { root } = fixture()
|
||||
const commands = recordedCommands(root, true)
|
||||
|
||||
expect(() => { buildLibraryArtifacts(root, false) }).toThrow('exited with 7')
|
||||
expect(commands()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('defers benchmark diagnostics while normal artifact builds reject the same source', () => {
|
||||
const { root, leaf } = fixture()
|
||||
const commands = recordedCommands(root)
|
||||
writeFileSync(join(leaf, 'src/index.ts'), 'export const value: number = "invalid"\n')
|
||||
|
||||
runCiBench(root)
|
||||
|
||||
expect(commands()).toEqual([
|
||||
['run', 'build:native-system'],
|
||||
['exec', 'tsdown', '--config-loader', 'native', '--env.DSH_BUILD_FACE', 'host'],
|
||||
['--filter', '@deepseek-ai/dsh-desktop', 'run', 'bundle'],
|
||||
['exec', 'tsdown', '--config-loader', 'native', '--env.DSH_BUILD_FACE', 'client'],
|
||||
['exec', 'tsdown', '--config-loader', 'native', '--config', 'benchmarks/tsdown.config.ts'],
|
||||
['run', 'build:web'],
|
||||
['run', 'test:bench:built'],
|
||||
].map(args => ({ args, title: 'Inherited Build Title' })))
|
||||
|
||||
expect(() => { buildLibraryArtifacts(root, false) }).toThrow('host compiler exited with')
|
||||
expect(commands()).toHaveLength(7)
|
||||
})
|
||||
})
|
||||
|
||||
describe('package project compilation', () => {
|
||||
it('records Windows-style glob keys without changing artifact bytes', () => {
|
||||
const { leaf } = fixture()
|
||||
const output = join(leaf, 'lib')
|
||||
mkdirSync(join(output, 'types'), { recursive: true })
|
||||
const bytes = Buffer.from([0, 255, 13, 10])
|
||||
writeFileSync(join(output, 'types\\index.js'), bytes)
|
||||
|
||||
expect(artifacts(leaf)).toEqual({ 'types/index.js': bytes.toString('base64') })
|
||||
})
|
||||
|
||||
it.each(['host', 'client'] as const)('preserves every %s package artifact byte', (face) => {
|
||||
const { root, leaf } = fixture()
|
||||
const complete = spawnSync(process.execPath, [compiler, '-b', `tsconfig.${face}.json`], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
})
|
||||
expect(complete.status, complete.stdout + complete.stderr).toBe(0)
|
||||
const expected = artifacts(leaf)
|
||||
expect(Object.keys(expected)).toEqual(expect.arrayContaining([
|
||||
'types/index.js', 'types/index.js.map', 'types/index.d.ts', 'types/index.d.ts.map',
|
||||
]))
|
||||
rmSync(join(leaf, 'lib'), { recursive: true })
|
||||
|
||||
compileReferencedProjects(root, face)
|
||||
|
||||
expect(artifacts(leaf)).toEqual(expected)
|
||||
})
|
||||
|
||||
it.each(['host', 'client'] as const)('preserves %s runtime output during benchmark emission', (face) => {
|
||||
const { root, leaf } = fixture()
|
||||
writeFileSync(join(leaf, 'src/support.ts'), [
|
||||
'export interface Payload { label: string }',
|
||||
'export const enum Offset { Baseline = 40 }',
|
||||
'',
|
||||
].join('\n'))
|
||||
writeFileSync(join(leaf, 'src/index.ts'), [
|
||||
'import { Offset, type Payload } from "./support.js"',
|
||||
'export const value: number = Offset.Baseline + 2',
|
||||
'export function describe(payload: Payload): string { return `${payload.label}: ${value}` }',
|
||||
'',
|
||||
].join('\n'))
|
||||
compileReferencedProjects(root, face)
|
||||
const runtimeArtifacts = () => Object.fromEntries(Object.entries(artifacts(leaf))
|
||||
.filter(([path]) => /\.js(?:\.map)?$/.test(path)))
|
||||
const expected = runtimeArtifacts()
|
||||
expect(Object.keys(expected)).toEqual([
|
||||
'types/index.js', 'types/index.js.map', 'types/support.js', 'types/support.js.map',
|
||||
])
|
||||
rmSync(join(leaf, 'lib'), { recursive: true })
|
||||
|
||||
compileReferencedProjects(root, face, 'benchmark-emit')
|
||||
|
||||
expect(runtimeArtifacts()).toEqual(expected)
|
||||
const entrypoint = pathToFileURL(join(leaf, 'lib/types/index.js')).href
|
||||
const result = spawnSync(process.execPath, [
|
||||
'--input-type=module', '-e',
|
||||
`import { describe } from ${JSON.stringify(entrypoint)}; console.log(describe({ label: 'answer' }))`,
|
||||
], { cwd: root, encoding: 'utf8' })
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.signal).toBeNull()
|
||||
expect(result.status, result.stderr).toBe(0)
|
||||
expect(result.stdout.trim()).toBe('answer: 42')
|
||||
})
|
||||
|
||||
it.each(['host', 'client'] as const)('rechecks %s semantic errors after benchmark emission without forcing a rebuild', (face) => {
|
||||
const { root, leaf } = fixture()
|
||||
writeFileSync(join(leaf, 'src/index.ts'), 'export const value: number = "invalid"\n')
|
||||
|
||||
compileReferencedProjects(root, face, 'benchmark-emit')
|
||||
|
||||
expect(Object.keys(artifacts(leaf)).some(path => path.endsWith('.tsbuildinfo'))).toBe(true)
|
||||
const complete = spawnSync(process.execPath, [compiler, '-b', `tsconfig.${face}.json`], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
})
|
||||
expect(complete.error).toBeUndefined()
|
||||
expect(complete.signal).toBeNull()
|
||||
expect(complete.status).not.toBe(0)
|
||||
expect(complete.stdout).toContain('src/index.ts(1,14): error TS2322')
|
||||
})
|
||||
|
||||
it('rejects package syntax errors during benchmark emission', () => {
|
||||
const { root, leaf } = fixture()
|
||||
writeFileSync(join(leaf, 'src/index.ts'), 'export const value = ;\n')
|
||||
|
||||
expect(() => { compileReferencedProjects(root, 'host', 'benchmark-emit') })
|
||||
.toThrow('host compiler exited with')
|
||||
})
|
||||
|
||||
it('keeps aggregate-only errors in the complete compiler check', () => {
|
||||
const { root } = fixture()
|
||||
writeFileSync(join(root, 'aggregate.ts'), 'export const aggregate: number = "invalid"\n')
|
||||
|
||||
expect(() => { compileReferencedProjects(root, 'host') }).not.toThrow()
|
||||
const complete = spawnSync(process.execPath, [compiler, '-b', 'tsconfig.host.json'], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
})
|
||||
expect(complete.status).not.toBe(0)
|
||||
expect(complete.stdout).toContain('aggregate.ts(1,14): error TS2322')
|
||||
})
|
||||
|
||||
it('rejects package source errors', () => {
|
||||
const { root, leaf } = fixture()
|
||||
writeFileSync(join(leaf, 'src/index.ts'), 'export const value: number = "invalid"\n')
|
||||
|
||||
expect(() => { compileReferencedProjects(root, 'host') }).toThrow('host compiler exited with')
|
||||
})
|
||||
|
||||
it.each([undefined, []])('rejects an aggregate with missing or empty references: %j', (references) => {
|
||||
const { root } = fixture()
|
||||
writeFileSync(join(root, 'tsconfig.host.json'), JSON.stringify({ files: [], references }))
|
||||
|
||||
expect(() => { compileReferencedProjects(root, 'host') }).toThrow('requires non-empty project references')
|
||||
})
|
||||
|
||||
it('rejects a missing referenced project', () => {
|
||||
const { root } = fixture()
|
||||
writeFileSync(join(root, 'tsconfig.host.json'), JSON.stringify({
|
||||
compilerOptions: { noEmit: true },
|
||||
files: [],
|
||||
references: [{ path: './absent' }],
|
||||
}))
|
||||
|
||||
expect(() => { compileReferencedProjects(root, 'host') }).toThrow('host compiler exited with')
|
||||
})
|
||||
|
||||
it.each([undefined, false])('rejects an aggregate capable of emitting files: noEmit=%s', (noEmit) => {
|
||||
const { root } = fixture()
|
||||
writeFileSync(join(root, 'tsconfig.host.json'), JSON.stringify({
|
||||
compilerOptions: { noEmit },
|
||||
include: ['aggregate.ts'],
|
||||
references: [{ path: './leaf package' }],
|
||||
}))
|
||||
|
||||
expect(() => { compileReferencedProjects(root, 'host') }).toThrow(
|
||||
`${join(root, 'tsconfig.host.json')} requires noEmit: true`,
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects a missing aggregate config', () => {
|
||||
const { root } = fixture()
|
||||
rmSync(join(root, 'tsconfig.host.json'))
|
||||
|
||||
expect(() => { compileReferencedProjects(root, 'host') }).toThrow('Cannot read file')
|
||||
})
|
||||
|
||||
it('rejects invalid compiler options', () => {
|
||||
const { root } = fixture()
|
||||
writeFileSync(join(root, 'tsconfig.host.json'), JSON.stringify({
|
||||
compilerOptions: { target: 'invalid' },
|
||||
files: [],
|
||||
references: [{ path: './leaf package' }],
|
||||
}))
|
||||
|
||||
expect(() => { compileReferencedProjects(root, 'host') }).toThrow("Argument for '--target' option must be")
|
||||
})
|
||||
|
||||
it('rejects an unsupported build mode before building', () => {
|
||||
const repository = resolve(import.meta.dirname, '..')
|
||||
const result = spawnSync(process.execPath, [
|
||||
'--import', 'tsx/esm',
|
||||
resolve(repository, 'scripts/compile-referenced-projects.ts'),
|
||||
'unsupported',
|
||||
], { cwd: repository, encoding: 'utf8' })
|
||||
|
||||
expect(result.status).not.toBe(0)
|
||||
expect(result.stderr).toContain('expected exactly one mode: host, client, host-libraries, or libraries')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,111 @@
|
||||
/** Compile and bundle package projects without repeating aggregate test and script typechecks. */
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { relative, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { parseArgs } from 'node:util'
|
||||
import ts from 'typescript'
|
||||
import { pnpmCommand } from './release/process.ts'
|
||||
|
||||
type CompilationMode = 'checked' | 'benchmark-emit'
|
||||
|
||||
/**
|
||||
* Run one package-manager command serially with the inherited build environment.
|
||||
* @param root - Repository root supplying the command's working directory.
|
||||
* @param args - Package-manager arguments, without shell quoting.
|
||||
* @throws If the command cannot start or exits unsuccessfully.
|
||||
*/
|
||||
export function runPnpmCommand(root: string, args: readonly string[]): void {
|
||||
const [command, ...prefix] = pnpmCommand()
|
||||
const result = spawnSync(command, [...prefix, ...args], { cwd: root, stdio: 'inherit' })
|
||||
if (result.error !== undefined) throw result.error
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`compile-referenced-projects: pnpm ${args.join(' ')} exited with ${String(result.status ?? result.signal)}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile one aggregate's project references with the serial TypeScript builder.
|
||||
* @param root - Repository root containing the aggregate tsconfig.
|
||||
* @param face - Independent compiler face whose package projects are emitted.
|
||||
* @param mode - Check package types by default; benchmark emission defers diagnostics to required full builds.
|
||||
* @throws If the aggregate emits files, its configuration is invalid, or package compilation fails.
|
||||
*/
|
||||
export function compileReferencedProjects(
|
||||
root: string,
|
||||
face: 'host' | 'client',
|
||||
mode: CompilationMode = 'checked',
|
||||
): void {
|
||||
const configPath = resolve(root, `tsconfig.${face}.json`)
|
||||
const config = ts.getParsedCommandLineOfConfigFile(configPath, {}, {
|
||||
...ts.sys,
|
||||
onUnRecoverableConfigFileDiagnostic(diagnostic) {
|
||||
throw new Error(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'))
|
||||
},
|
||||
})
|
||||
if (config === undefined) throw new Error(`compile-referenced-projects: cannot read ${configPath}`)
|
||||
const references = config.projectReferences
|
||||
if (references === undefined || references.length === 0) {
|
||||
throw new Error(`compile-referenced-projects: ${configPath} requires non-empty project references`)
|
||||
}
|
||||
if (config.errors.length > 0) {
|
||||
throw new Error(config.errors
|
||||
.map(diagnostic => ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'))
|
||||
.join('\n'))
|
||||
}
|
||||
if (config.options.noEmit !== true) {
|
||||
throw new Error(`compile-referenced-projects: ${configPath} requires noEmit: true to exclude its aggregate program`)
|
||||
}
|
||||
const compiler = fileURLToPath(import.meta.resolve('typescript/bin/tsc'))
|
||||
const result = spawnSync(process.execPath, [
|
||||
...face === 'host' ? ['--max-old-space-size=4096'] : [],
|
||||
compiler,
|
||||
'-b',
|
||||
...mode === 'benchmark-emit' ? ['--noCheck'] : [],
|
||||
// Relative arguments keep the complete workspace graph below Windows' command-line limit.
|
||||
...references.map(reference => relative(root, reference.path)),
|
||||
], { cwd: root, stdio: 'inherit' })
|
||||
if (result.error !== undefined) throw result.error
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`compile-referenced-projects: ${face} compiler exited with ${String(result.status ?? result.signal)}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit package libraries in Host, Desktop, then Client dependency order.
|
||||
* @param root - Repository root containing the compiler and bundle configurations.
|
||||
* @param hostOnly - Omit the Client compiler and bundler after the Host artifacts are ready.
|
||||
* @param mode - Check package types by default; benchmark emission defers diagnostics to required full builds.
|
||||
* @throws If any compiler or bundler fails.
|
||||
*/
|
||||
export function buildLibraryArtifacts(root: string, hostOnly: boolean, mode: CompilationMode = 'checked'): void {
|
||||
compileReferencedProjects(root, 'host', mode)
|
||||
runPnpmCommand(root, ['exec', 'tsdown', '--config-loader', 'native', '--env.DSH_BUILD_FACE', 'host'])
|
||||
runPnpmCommand(root, ['--filter', '@deepseek-ai/dsh-desktop', 'run', 'bundle'])
|
||||
if (hostOnly) return
|
||||
compileReferencedProjects(root, 'client', mode)
|
||||
runPnpmCommand(root, ['exec', 'tsdown', '--config-loader', 'native', '--env.DSH_BUILD_FACE', 'client'])
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const { positionals } = parseArgs({ allowPositionals: true })
|
||||
const [mode] = positionals
|
||||
if (positionals.length !== 1) {
|
||||
throw new Error('compile-referenced-projects: expected exactly one mode: host, client, host-libraries, or libraries')
|
||||
}
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
switch (mode) {
|
||||
case 'host':
|
||||
case 'client':
|
||||
compileReferencedProjects(root, mode)
|
||||
break
|
||||
case 'host-libraries':
|
||||
buildLibraryArtifacts(root, true)
|
||||
break
|
||||
case 'libraries':
|
||||
buildLibraryArtifacts(root, false)
|
||||
break
|
||||
default:
|
||||
throw new Error('compile-referenced-projects: expected exactly one mode: host, client, host-libraries, or libraries')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import type { ModuleDiagnostic, ResolvedConfig } from 'vitest/node'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import CoverageFileTimesReporter from './coverage-file-times.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
afterEach(async () => {
|
||||
for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function temporaryRoot(): Promise<string> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-coverage-file-times-'))
|
||||
roots.push(root)
|
||||
return root
|
||||
}
|
||||
|
||||
describe('coverage file timing report', () => {
|
||||
it('includes import and hook costs once without serializing coverage or test objects', async () => {
|
||||
const root = await temporaryRoot()
|
||||
const reporter = new CoverageFileTimesReporter()
|
||||
reporter.onInit({ config: { root, outputFile: { fileTimes: 'times.json' } } })
|
||||
const diagnostic: ModuleDiagnostic = {
|
||||
environmentSetupDuration: 3,
|
||||
prepareDuration: 5,
|
||||
setupDuration: 7,
|
||||
collectDuration: 100,
|
||||
duration: 200,
|
||||
heap: undefined,
|
||||
// Per-dependency import times are already included in collectDuration.
|
||||
importDurations: { dependency: { selfTime: 100, totalTime: 100 } },
|
||||
}
|
||||
const module = {
|
||||
moduleId: join(root, 'packages', 'a', 'tests', 'x.spec.ts'),
|
||||
diagnostic: () => diagnostic,
|
||||
// Vitest modules retain tests and coverage through their project. The
|
||||
// timing report must read only their paths and diagnostics.
|
||||
toJSON: () => { throw new Error('test module was serialized') },
|
||||
}
|
||||
await reporter.onTestRunEnd([module])
|
||||
|
||||
const output = await readFile(join(root, 'times.json'), 'utf8')
|
||||
expect(JSON.parse(output)).toEqual({ 'packages/a/tests/x.spec.ts': 315 })
|
||||
expect(output.endsWith('\n')).toBe(true)
|
||||
expect(output).not.toContain('coverageMap')
|
||||
expect(output).not.toContain('assertionResults')
|
||||
})
|
||||
|
||||
it('writes an empty report when no modules finished', async () => {
|
||||
const root = await temporaryRoot()
|
||||
const reporter = new CoverageFileTimesReporter()
|
||||
reporter.onInit({ config: { root, outputFile: { fileTimes: 'times.json' } } })
|
||||
await reporter.onTestRunEnd([])
|
||||
expect(await readFile(join(root, 'times.json'), 'utf8')).toBe('{}\n')
|
||||
})
|
||||
|
||||
const missingOutputs: ResolvedConfig['outputFile'][] = ['report.json', {}, { fileTimes: '' }]
|
||||
it.each(missingOutputs)(
|
||||
'rejects missing timing output %j',
|
||||
(outputFile) => {
|
||||
const reporter = new CoverageFileTimesReporter()
|
||||
expect(() => { reporter.onInit({ config: { root: '/repo', outputFile } }) })
|
||||
.toThrow('outputFile.fileTimes is required')
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
/** Record coverage partition file costs without retaining test results or coverage maps. */
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
import { relative, resolve, sep } from 'node:path'
|
||||
import type { Reporter, ResolvedConfig, TestModule } from 'vitest/node'
|
||||
|
||||
/** Vitest reporter that writes repository-relative file durations to `outputFile.fileTimes`. */
|
||||
export default class CoverageFileTimesReporter implements Reporter {
|
||||
private root = ''
|
||||
private outputFile = ''
|
||||
|
||||
/**
|
||||
* Resolve the timing output owned by the coverage coordinator.
|
||||
* @param vitest - repository root and configured reporter output files.
|
||||
* @throws when the file-times output is absent.
|
||||
*/
|
||||
public onInit(vitest: { config: Pick<ResolvedConfig, 'root' | 'outputFile'> }): void {
|
||||
const output = vitest.config.outputFile
|
||||
if (typeof output !== 'object' || typeof output.fileTimes !== 'string' || output.fileTimes === '') {
|
||||
throw new Error('coverage-file-times: outputFile.fileTimes is required.')
|
||||
}
|
||||
this.root = vitest.config.root
|
||||
this.outputFile = resolve(this.root, output.fileTimes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write each file's distinct environment, preparation, setup, collection, and execution costs.
|
||||
* @param testModules - finished files, including failed and skipped files.
|
||||
* @returns after the compact timing report has been written.
|
||||
*/
|
||||
public async onTestRunEnd(testModules: readonly Pick<TestModule, 'moduleId' | 'diagnostic'>[]): Promise<void> {
|
||||
const durations: Record<string, number> = {}
|
||||
for (const module of testModules) {
|
||||
const diagnostic = module.diagnostic()
|
||||
const file = relative(this.root, module.moduleId).split(sep).join('/')
|
||||
durations[file] = diagnostic.environmentSetupDuration
|
||||
+ diagnostic.prepareDuration
|
||||
+ diagnostic.setupDuration
|
||||
+ diagnostic.collectDuration
|
||||
+ diagnostic.duration
|
||||
}
|
||||
await writeFile(this.outputFile, `${JSON.stringify(durations)}\n`, 'utf8')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { runGate } from './run-gates.ts'
|
||||
import { PassThrough } from 'node:stream'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ForksPoolWorker } from 'vitest/node'
|
||||
import type { PoolOptions, PoolWorker, TestProject, WorkerRequest, WorkerResponse } from 'vitest/node'
|
||||
import { coverageForkPool } from './coverage-fork-diagnostics.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
const cleanups: Array<() => Promise<void>> = []
|
||||
|
||||
afterEach(async () => {
|
||||
const cleanup = cleanups.splice(0)
|
||||
const directories = roots.splice(0)
|
||||
const results = await Promise.allSettled(cleanup.map(dispose => dispose()))
|
||||
vi.restoreAllMocks()
|
||||
for (const directory of directories) await rm(directory, { recursive: true, force: true })
|
||||
const errors: unknown[] = []
|
||||
for (const result of results) {
|
||||
if (result.status === 'rejected') errors.push(result.reason)
|
||||
}
|
||||
if (errors.length > 0) throw new AggregateError(errors, 'fork fixture cleanup failed')
|
||||
})
|
||||
|
||||
async function fixture(exitCode: number, early = false): Promise<string> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-coverage-fork-'))
|
||||
roots.push(root)
|
||||
await mkdir(join(root, 'workers'))
|
||||
// The public pool's distPath selects a plain Node protocol fixture, not a nested Vitest run.
|
||||
await writeFile(join(root, 'workers/forks.js'), early ? `process.exit(${exitCode})\n` : `
|
||||
process.on('message', (message) => {
|
||||
if (message.type === 'run' || message.type === 'collect') process.send({ kind: 'ready', pid: process.pid })
|
||||
if (message.type === 'cancel') process.exit(${exitCode})
|
||||
if (message.type === 'stop') process.exit(0)
|
||||
})
|
||||
`)
|
||||
return root
|
||||
}
|
||||
|
||||
function poolOptions(root: string) {
|
||||
const outputStream = new PassThrough()
|
||||
const errorStream = new PassThrough()
|
||||
const error = vi.fn<(message: string) => void>()
|
||||
// ForksPoolWorker needs only the logger and project name from the Vitest project.
|
||||
const logger: Pick<TestProject['vitest']['logger'], 'outputStream' | 'errorStream' | 'error'> = {
|
||||
outputStream, errorStream, error,
|
||||
}
|
||||
const project = { name: 'fixture', vitest: { logger } } as TestProject
|
||||
const options: PoolOptions = {
|
||||
distPath: root, project, method: 'run', environment: { name: 'node', options: null },
|
||||
execArgv: [], env: {},
|
||||
}
|
||||
return { options, error, outputStream, errorStream }
|
||||
}
|
||||
|
||||
async function start(root: string, diagnostic = true) {
|
||||
const { options, error, outputStream, errorStream } = poolOptions(root)
|
||||
const pool: PoolWorker = diagnostic ? coverageForkPool.createPoolWorker(options) : new ForksPoolWorker(options)
|
||||
let started = false
|
||||
const closed = Promise.withResolvers<undefined>()
|
||||
cleanups.push(async () => {
|
||||
if (started) {
|
||||
await pool.stop()
|
||||
await closed.promise
|
||||
}
|
||||
outputStream.destroy()
|
||||
errorStream.destroy()
|
||||
})
|
||||
await pool.start()
|
||||
started = true
|
||||
pool.on('close', () => { closed.resolve(undefined) })
|
||||
const exited = new Promise<number | null>((resolve) => { pool.on('exit', resolve) })
|
||||
const ready = new Promise<number>((resolve) => {
|
||||
pool.on('message', (message: unknown) => {
|
||||
const response = message as { kind: string; pid: number }
|
||||
if (response.kind === 'ready') resolve(response.pid)
|
||||
})
|
||||
})
|
||||
return { pool, ready, exited, error }
|
||||
}
|
||||
|
||||
function request(filepath: string, workerId: number): Extract<WorkerRequest, { type: 'run' }> {
|
||||
return {
|
||||
__vitest_worker_request__: true, type: 'run',
|
||||
context: { files: [{ filepath }], workerId, providedContext: {}, environment: { name: 'node', options: null } },
|
||||
}
|
||||
}
|
||||
|
||||
const cancel: WorkerRequest = { __vitest_worker_request__: true, type: 'cancel' }
|
||||
const stop: WorkerRequest = { __vitest_worker_request__: true, type: 'stop' }
|
||||
|
||||
async function configuredPool(pool: 'stock' | 'diagnostic', outcome: 'exit' | 'pass', signal: AbortSignal) {
|
||||
const cacheDir = await mkdtemp(join(tmpdir(), 'dsh-configured-fork-cache-'))
|
||||
roots.push(cacheDir)
|
||||
const controller = new AbortController()
|
||||
const result = runGate({
|
||||
id: `fork-${pool}`, label: 'fork fixture', displayCommand: 'Vitest pool fixture',
|
||||
command: process.execPath,
|
||||
args: [resolve('node_modules/vitest/vitest.mjs'), 'run', '--config', 'scripts/fixtures/coverage-fork.config.ts'],
|
||||
env: { DSH_TEST_FORK_POOL: pool, DSH_TEST_FORK_OUTCOME: outcome, DSH_TEST_FORK_CACHE: cacheDir },
|
||||
}, AbortSignal.any([signal, controller.signal]))
|
||||
let settled = false
|
||||
cleanups.push(async () => {
|
||||
if (!settled) controller.abort()
|
||||
await result
|
||||
})
|
||||
try {
|
||||
return await result
|
||||
} finally {
|
||||
settled = true
|
||||
}
|
||||
}
|
||||
|
||||
describe('coverage fork configuration entry', () => {
|
||||
it('preserves the real Vitest failure and adds metadata before its error report', async ({ signal }) => {
|
||||
const [stock, diagnostic] = await Promise.all([
|
||||
configuredPool('stock', 'exit', signal),
|
||||
configuredPool('diagnostic', 'exit', signal),
|
||||
])
|
||||
for (const result of [stock, diagnostic]) {
|
||||
expect(result.aborted).not.toBe(true)
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.signalCode).toBeNull()
|
||||
expect(result.exitCode).toBe(1)
|
||||
expect(result.output.map(chunk => chunk.text).join('')).toContain('Worker exited unexpectedly')
|
||||
}
|
||||
const output = diagnostic.output.map(chunk => chunk.text).join('')
|
||||
expect(stock.output.map(chunk => chunk.text).join('')).not.toContain('coverage-worker-exit:')
|
||||
const encoded = /coverage-worker-exit: (\{[^\r\n]*\})/u.exec(output)?.[1]
|
||||
expect(encoded).toBeDefined()
|
||||
const record = JSON.parse(encoded!) as {
|
||||
pid: number
|
||||
project: string
|
||||
workerId: number
|
||||
files: string[]
|
||||
exitCode: number
|
||||
signalCode: null
|
||||
}
|
||||
const pid = Number(/coverage-fixture-pid: (\d+)/u.exec(output)?.[1])
|
||||
const anyNumber: unknown = expect.any(Number)
|
||||
const anyString: unknown = expect.any(String)
|
||||
expect(record).toEqual({
|
||||
pid, project: 'configured-fork', workerId: anyNumber,
|
||||
files: [anyString], exitCode: 23, signalCode: null,
|
||||
})
|
||||
expect(record.files.map(file => file.replaceAll('\\', '/')))
|
||||
.toEqual([resolve('scripts/fixtures/coverage-fork.fixture.ts').replaceAll('\\', '/')])
|
||||
expect(output.indexOf('coverage-worker-exit:')).toBeLessThan(output.indexOf('Worker exited unexpectedly'))
|
||||
})
|
||||
|
||||
it('keeps a successful configured run silent and successful', async ({ signal }) => {
|
||||
const result = await configuredPool('diagnostic', 'pass', signal)
|
||||
expect(result.aborted).not.toBe(true)
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.signalCode).toBeNull()
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(result.output.map(chunk => chunk.text).join('')).not.toContain('coverage-worker-exit:')
|
||||
})
|
||||
})
|
||||
|
||||
describe('coverage fork exit callback', () => {
|
||||
it.each([
|
||||
['native receiver', { pid: 41 }, 41],
|
||||
['unavailable receiver', undefined, null],
|
||||
] as const)('retains a signal with %s and silences expected shutdown', async (_name, receiver, pid) => {
|
||||
let exitListener: ((value: unknown) => void) | undefined
|
||||
vi.spyOn(ForksPoolWorker.prototype, 'start').mockResolvedValue(undefined)
|
||||
vi.spyOn(ForksPoolWorker.prototype, 'stop').mockResolvedValue(undefined)
|
||||
vi.spyOn(ForksPoolWorker.prototype, 'send').mockImplementation(() => undefined)
|
||||
vi.spyOn(ForksPoolWorker.prototype, 'off').mockImplementation(() => undefined)
|
||||
vi.spyOn(ForksPoolWorker.prototype, 'on').mockImplementation((event, callback) => {
|
||||
if (event === 'exit') exitListener = callback
|
||||
})
|
||||
const { options, error, outputStream, errorStream } = poolOptions('/unused')
|
||||
const pool = coverageForkPool.createPoolWorker(options)
|
||||
cleanups.push(async () => {
|
||||
await pool.stop()
|
||||
outputStream.destroy()
|
||||
errorStream.destroy()
|
||||
})
|
||||
await pool.start()
|
||||
pool.send(request('/signal.spec.ts', 9))
|
||||
if (exitListener === undefined) throw new Error('exit observer was not registered')
|
||||
Reflect.apply(exitListener, receiver, [null, 'SIGTERM'])
|
||||
expect(error).toHaveBeenCalledWith(`coverage-worker-exit: ${JSON.stringify({
|
||||
pid, project: 'fixture', workerId: 9, files: ['/signal.spec.ts'], exitCode: null, signalCode: 'SIGTERM',
|
||||
})}`)
|
||||
error.mockClear()
|
||||
pool.send(stop)
|
||||
expect(() => { Reflect.apply(exitListener!, undefined, [null, 'SIGTERM']) }).not.toThrow()
|
||||
expect(error).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('coverage fork exit diagnostics', () => {
|
||||
it('attributes a later collect request instead of an earlier run request', async () => {
|
||||
const instance = await start(await fixture(23))
|
||||
instance.pool.send(request('/earlier.spec.ts', 4))
|
||||
const pid = await instance.ready
|
||||
instance.pool.send({ ...request('/collected.spec.ts', 5), type: 'collect' })
|
||||
const unrelated = { type: 'testfileFinished' }
|
||||
expect(instance.pool.deserialize(unrelated)).toBe(unrelated)
|
||||
expect(instance.pool.deserialize(null)).toBeNull()
|
||||
instance.pool.send(cancel)
|
||||
expect(await instance.exited).toBe(23)
|
||||
expect(instance.error).toHaveBeenCalledWith(`coverage-worker-exit: ${JSON.stringify({
|
||||
pid, project: 'fixture', workerId: 5, files: ['/collected.spec.ts'], exitCode: 23, signalCode: null,
|
||||
})}`)
|
||||
})
|
||||
|
||||
it('does not attribute a post-completion exit to a finished file', async () => {
|
||||
const instance = await start(await fixture(23))
|
||||
instance.pool.send(request('/finished.spec.ts', 6))
|
||||
const pid = await instance.ready
|
||||
const finished = {
|
||||
__vitest_worker_response__: true, type: 'testfileFinished',
|
||||
} satisfies Extract<WorkerResponse, { type: 'testfileFinished' }>
|
||||
expect(instance.pool.deserialize(finished)).toBe(finished)
|
||||
instance.pool.send(cancel)
|
||||
expect(await instance.exited).toBe(23)
|
||||
expect(instance.error).toHaveBeenCalledWith(`coverage-worker-exit: ${JSON.stringify({
|
||||
pid, project: 'fixture', workerId: null, files: [], exitCode: 23, signalCode: null,
|
||||
})}`)
|
||||
})
|
||||
|
||||
it('records the exact owner while an independent ready fork remains alive', async () => {
|
||||
const root = await fixture(23)
|
||||
const [first, second] = await Promise.all([start(root), start(root)])
|
||||
first.pool.send(request('/first.spec.ts', 1))
|
||||
second.pool.send(request('/second.spec.ts', 2))
|
||||
const [firstPid, secondPid] = await Promise.all([first.ready, second.ready])
|
||||
expect(firstPid).not.toBe(secondPid)
|
||||
first.pool.send(cancel)
|
||||
expect(await first.exited).toBe(23)
|
||||
expect(first.error.mock.calls).toEqual([[`coverage-worker-exit: ${JSON.stringify({
|
||||
pid: firstPid, project: 'fixture', workerId: 1, files: ['/first.spec.ts'], exitCode: 23, signalCode: null,
|
||||
})}`]])
|
||||
expect(second.error).not.toHaveBeenCalled()
|
||||
expect(() => process.kill(secondPid, 0)).not.toThrow()
|
||||
second.pool.send(stop)
|
||||
expect(await second.exited).toBe(0)
|
||||
expect(second.error).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('retains an unexpected zero exit instead of interpreting it as suite success', async () => {
|
||||
const instance = await start(await fixture(0))
|
||||
instance.pool.send(request('/unfinished.spec.ts', 3))
|
||||
const pid = await instance.ready
|
||||
instance.pool.send(cancel)
|
||||
expect(await instance.exited).toBe(0)
|
||||
expect(instance.error).toHaveBeenCalledWith(`coverage-worker-exit: ${JSON.stringify({
|
||||
pid, project: 'fixture', workerId: 3, files: ['/unfinished.spec.ts'], exitCode: 0, signalCode: null,
|
||||
})}`)
|
||||
})
|
||||
|
||||
it('records an exit before any file request without inventing a file owner', async () => {
|
||||
const instance = await start(await fixture(31, true))
|
||||
expect(await instance.exited).toBe(31)
|
||||
expect(instance.error).toHaveBeenCalledOnce()
|
||||
const record = JSON.parse(instance.error.mock.calls[0]![0].slice('coverage-worker-exit: '.length)) as Record<string, unknown>
|
||||
const anyNumber: unknown = expect.any(Number)
|
||||
expect(record).toEqual({
|
||||
pid: anyNumber, project: 'fixture', workerId: null, files: [], exitCode: 31, signalCode: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('observes the same child failure without diagnostics under the stock pool negative control', async () => {
|
||||
const instance = await start(await fixture(23), false)
|
||||
instance.pool.send(request('/stock.spec.ts', 4))
|
||||
await instance.ready
|
||||
instance.pool.send(cancel)
|
||||
expect(await instance.exited).toBe(23)
|
||||
expect(instance.error).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,69 @@
|
||||
/** Add exit metadata without changing Vitest's fork scheduling or failure handling. */
|
||||
import type { ChildProcess } from 'node:child_process'
|
||||
import { ForksPoolWorker } from 'vitest/node'
|
||||
import type { PoolOptions, PoolRunnerInitializer, WorkerRequest, WorkerResponse } from 'vitest/node'
|
||||
|
||||
const FINISHED_RESPONSE_TYPE: WorkerResponse['type'] = 'testfileFinished'
|
||||
|
||||
/** Attribute only outstanding requests; stop silences exits, while cancel does not. */
|
||||
class DiagnosticForkWorker extends ForksPoolWorker {
|
||||
private expectedExit = false
|
||||
private observing = false
|
||||
private request: Extract<WorkerRequest, { type: 'run' | 'collect' }> | undefined
|
||||
private readonly reportExit: (this: ChildProcess | undefined, code: number | null, signal?: NodeJS.Signals | null) => void
|
||||
|
||||
constructor(options: PoolOptions) {
|
||||
super(options)
|
||||
const report = (child: ChildProcess | undefined, code: number | null, signal: NodeJS.Signals | null | undefined): void => {
|
||||
if (this.expectedExit) return
|
||||
options.project.vitest.logger.error(`coverage-worker-exit: ${JSON.stringify({
|
||||
pid: child?.pid ?? null,
|
||||
project: options.project.name,
|
||||
workerId: this.request?.context.workerId ?? null,
|
||||
files: this.request?.context.files.map(file => file.filepath) ?? [],
|
||||
exitCode: code,
|
||||
signalCode: signal ?? null,
|
||||
})}`)
|
||||
}
|
||||
// ForksPoolWorker.on forwards Node ChildProcess events and their receiver.
|
||||
this.reportExit = function (code, signal) { report(this, code, signal) }
|
||||
}
|
||||
|
||||
override async start(): Promise<void> {
|
||||
await super.start()
|
||||
this.on('exit', this.reportExit)
|
||||
this.observing = true
|
||||
}
|
||||
|
||||
override send(message: WorkerRequest): void {
|
||||
if (message.type === 'run' || message.type === 'collect') this.request = message
|
||||
if (message.type === 'stop') this.expectedExit = true
|
||||
super.send(message)
|
||||
}
|
||||
|
||||
/** Vitest routes worker responses here; completion covers the whole run/collect request. */
|
||||
override deserialize(data: unknown): unknown {
|
||||
const response = super.deserialize(data)
|
||||
if (typeof response === 'object' && response !== null
|
||||
&& '__vitest_worker_response__' in response && response.__vitest_worker_response__ === true
|
||||
&& 'type' in response && response.type === FINISHED_RESPONSE_TYPE) {
|
||||
this.request = undefined
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
override async stop(): Promise<void> {
|
||||
this.expectedExit = true
|
||||
if (this.observing) {
|
||||
this.off('exit', this.reportExit)
|
||||
this.observing = false
|
||||
}
|
||||
await super.stop()
|
||||
}
|
||||
}
|
||||
|
||||
/** Fork pool with additive process-exit diagnostics for coverage partitions. */
|
||||
export const coverageForkPool: PoolRunnerInitializer = {
|
||||
name: 'coverage-forks',
|
||||
createPoolWorker: options => new DiagnosticForkWorker(options),
|
||||
}
|
||||
@@ -269,17 +269,26 @@ describe('coverage file inventory', () => {
|
||||
expect(durations.get('packages/a/tests/y.spec.ts')).toBe(7)
|
||||
})
|
||||
|
||||
it('extracts per-file durations from partition json reports', async () => {
|
||||
it('extracts per-file durations from compact partition timing reports', async () => {
|
||||
const root = await temporaryRoot()
|
||||
const report = join(root, 'partition-1.report.json')
|
||||
const report = join(root, 'partition-1.times.json')
|
||||
await writeFile(report, JSON.stringify({
|
||||
testResults: [
|
||||
{ name: join(root, 'packages/a/tests/x.spec.ts'), startTime: 1000, endTime: 1500 },
|
||||
{ name: 'not-a-spec', startTime: 1, endTime: 2 },
|
||||
],
|
||||
'packages/a/tests/x.spec.ts': 500,
|
||||
'packages/a/tests/y.spec.ts': 0,
|
||||
'packages/a/tests/invalid.spec.ts': -1,
|
||||
'packages/a/tests/malformed.spec.ts': 'unknown',
|
||||
}))
|
||||
const durations = collectPartitionDurations([report], root)
|
||||
const durations = collectPartitionDurations([report])
|
||||
expect(durations.get('packages/a/tests/x.spec.ts')).toBe(500)
|
||||
expect(durations.get('packages/a/tests/y.spec.ts')).toBe(0)
|
||||
expect(durations.size).toBe(2)
|
||||
})
|
||||
|
||||
it.each(['null', '[]', '42', '"invalid"', '{'])('ignores corrupt timing metadata %s', async (contents) => {
|
||||
const root = await temporaryRoot()
|
||||
const report = join(root, 'partition.times.json')
|
||||
await writeFile(report, contents)
|
||||
expect(collectPartitionDurations([report])).toEqual(new Map())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -511,9 +520,10 @@ describe('coverage partition coordinator', () => {
|
||||
'--maxWorkers=1',
|
||||
'--reporter=default',
|
||||
'--reporter=blob',
|
||||
'--reporter=json',
|
||||
'--testTimeout=30000',
|
||||
]))
|
||||
expect(command.args).not.toContain('--reporter=json')
|
||||
expect(command.args).toContain('--reporter=./scripts/coverage-file-times.ts')
|
||||
expect(command.args).not.toContain('--shard=1/3')
|
||||
expect(command.args.some(argument => argument.startsWith('--config='))).toBe(true)
|
||||
expect(command.env).toEqual({
|
||||
@@ -530,6 +540,8 @@ describe('coverage partition coordinator', () => {
|
||||
expect(allConfigs).toContain('c.spec.ts')
|
||||
for (const source of partitionConfigs) {
|
||||
expect(source).toContain("from '../../vitest.config.ts'")
|
||||
expect(source).toContain("from '../../scripts/coverage-fork-diagnostics.ts'")
|
||||
expect(source).toContain('pool: coverageForkPool')
|
||||
}
|
||||
const mergeCommand = commands[3]
|
||||
if (mergeCommand === undefined) throw new Error('coverage merge command was not observed')
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
/** Coordinate single-worker Vitest coverage partitions and one merged report. */
|
||||
/**
|
||||
* Coordinate single-worker Vitest coverage partitions and one merged report.
|
||||
* Separate processes preserve worker isolation; only the complete merged
|
||||
* inventory can enforce per-file coverage thresholds.
|
||||
*/
|
||||
import { spawn } from 'node:child_process'
|
||||
import { globSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { lstat, mkdir, readdir, rm, unlink, writeFile } from 'node:fs/promises'
|
||||
@@ -23,6 +27,7 @@ export const COVERAGE_TEST_TIMEOUT_ENV = 'DSH_COVERAGE_TEST_TIMEOUT_MS'
|
||||
* working directory on every platform.
|
||||
*/
|
||||
const CANONICAL_LOCATIONS_REPORTER = './scripts/coverage-canonical-locations.ts'
|
||||
const FILE_TIMES_REPORTER = './scripts/coverage-file-times.ts'
|
||||
|
||||
/** One child command owned by the coverage coordinator. */
|
||||
export interface CoverageCommand {
|
||||
@@ -295,23 +300,23 @@ export function writeFileDurations(
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract per-file durations from Vitest JSON reporter outputs (one per
|
||||
* partition). Each `testResults` entry names an absolute spec path and carries
|
||||
* `startTime`/`endTime`; the difference is the file's recorded duration.
|
||||
* Extract per-file durations from compact timing reports, one per partition.
|
||||
* @param reportFiles - reports mapping repository-relative file paths to milliseconds.
|
||||
* @returns finite, non-negative file costs from readable reports.
|
||||
*/
|
||||
export function collectPartitionDurations(reportFiles: readonly string[], root: string): Map<string, number> {
|
||||
export function collectPartitionDurations(reportFiles: readonly string[]): Map<string, number> {
|
||||
const durations = new Map<string, number>()
|
||||
for (const file of reportFiles) {
|
||||
let report: { testResults?: Array<{ name?: unknown; startTime?: number; endTime?: number }> }
|
||||
let report: unknown
|
||||
try {
|
||||
report = JSON.parse(readFileSync(file, 'utf8')) as { testResults?: Array<{ name?: unknown; startTime?: number; endTime?: number }> }
|
||||
report = JSON.parse(readFileSync(file, 'utf8'))
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
for (const result of report.testResults ?? []) {
|
||||
if (typeof result.name !== 'string' || typeof result.startTime !== 'number' || typeof result.endTime !== 'number') continue
|
||||
const relativePath = relative(root, result.name).split(sep).join('/')
|
||||
durations.set(relativePath, Math.max(0, result.endTime - result.startTime))
|
||||
if (report === null || typeof report !== 'object' || Array.isArray(report)) continue
|
||||
for (const [path, duration] of Object.entries(report)) {
|
||||
if (typeof duration !== 'number' || !Number.isFinite(duration) || duration < 0) continue
|
||||
durations.set(path, duration)
|
||||
}
|
||||
}
|
||||
return durations
|
||||
@@ -424,6 +429,7 @@ function partitionConfigSource(
|
||||
const processBound = JSON.stringify(files.filter(file => projectOf.get(file) === 'process-bound').map(file => file.split('\\').join('/')))
|
||||
return [
|
||||
"import base from '../../vitest.config.ts'",
|
||||
"import { coverageForkPool } from '../../scripts/coverage-fork-diagnostics.ts'",
|
||||
'export default {',
|
||||
' ...base,',
|
||||
' test: {',
|
||||
@@ -432,6 +438,7 @@ function partitionConfigSource(
|
||||
' ...project,',
|
||||
' test: {',
|
||||
' ...project.test,',
|
||||
' pool: coverageForkPool,',
|
||||
' include: project.test.name === \'process-bound\' ? ' + processBound + ' : ' + threadSafe + ',',
|
||||
' },',
|
||||
' })),',
|
||||
@@ -528,10 +535,10 @@ export class CoveragePartitionCoordinator {
|
||||
private persistDurations(partitionCount: number): void {
|
||||
const reportFiles = Array.from(
|
||||
{ length: partitionCount },
|
||||
(_, index) => join(this.temporaryRoot, `partition-${index + 1}.report.json`),
|
||||
(_, index) => join(this.temporaryRoot, `partition-${index + 1}.times.json`),
|
||||
)
|
||||
const currentFiles = this.projectOf.size > 0 ? [...this.projectOf.keys()] : undefined
|
||||
writeFileDurations(this.root, collectPartitionDurations(reportFiles, this.root), currentFiles)
|
||||
writeFileDurations(this.root, collectPartitionDurations(reportFiles), currentFiles)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -580,7 +587,7 @@ export class CoveragePartitionCoordinator {
|
||||
private partitionCommand(index: number, configPath: string): CoverageCommand {
|
||||
const blobPath = join(this.blobsRoot, `partition-${index}.json`)
|
||||
const reportsDirectory = join(this.temporaryRoot, `coverage-${index}`)
|
||||
const jsonReportPath = join(this.temporaryRoot, `partition-${index}.report.json`)
|
||||
const timingReportPath = join(this.temporaryRoot, `partition-${index}.times.json`)
|
||||
const invocation = pnpmInvocation([
|
||||
'exec',
|
||||
'vitest',
|
||||
@@ -591,10 +598,10 @@ export class CoveragePartitionCoordinator {
|
||||
`--config=${this.relativePath(configPath)}`,
|
||||
'--reporter=default',
|
||||
'--reporter=blob',
|
||||
'--reporter=json',
|
||||
`--reporter=${FILE_TIMES_REPORTER}`,
|
||||
`--reporter=${CANONICAL_LOCATIONS_REPORTER}`,
|
||||
`--outputFile.blob=${this.relativePath(blobPath)}`,
|
||||
`--outputFile.json=${this.relativePath(jsonReportPath)}`,
|
||||
`--outputFile.fileTimes=${this.relativePath(timingReportPath)}`,
|
||||
`--coverage.reportsDirectory=${this.relativePath(reportsDirectory)}`,
|
||||
...this.vitestArgs,
|
||||
], { npm_execpath: this.pnpmEntrypoint })
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Build, pack, and serve the Official on-demand bundles' local dependency
|
||||
* closure as a loopback registry, so a packaged test app can install them
|
||||
* before their release reaches npm.
|
||||
*
|
||||
* The run is a build/test utility, not a Harness application launcher: it
|
||||
* starts no agent and needs no credentials. It shares only the artifact
|
||||
* transport with the release path; packing, manifests, and validation stay with
|
||||
* the owners that already perform them.
|
||||
*/
|
||||
|
||||
import { mkdirSync } from 'node:fs'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { execa } from 'execa'
|
||||
import { getDshRuntimeVersion } from '../packages/boot/app-boot/src/plugin-compatibility.ts'
|
||||
import { generateOfficialBundleCatalog } from './gen-official-bundle-catalog.ts'
|
||||
import {
|
||||
REGISTRY_MANIFEST_FILE,
|
||||
REGISTRY_OUTPUT_ROOT,
|
||||
createRegistryManifest,
|
||||
hostPlatform,
|
||||
packBundleArtifacts,
|
||||
pnpmInvocationFor,
|
||||
readReleaseMembers,
|
||||
readWorkspacePackages,
|
||||
selectBundleDependencies,
|
||||
sourceEvidence,
|
||||
startBundleRegistry,
|
||||
verifyBuiltBundles,
|
||||
verifyReleaseVersions,
|
||||
writeRegistryManifest,
|
||||
type BundleArtifact,
|
||||
} from './local-bundle-registry.ts'
|
||||
|
||||
/** The preparation step a signal stopped, so the failure is not reported as a command's. */
|
||||
class Interrupted extends Error {
|
||||
/**
|
||||
* @param stage - the step that was running when the signal arrived.
|
||||
*/
|
||||
constructor(readonly stage: string) {
|
||||
super(`interrupted during ${stage}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one package script in the repository root, propagating its exit status.
|
||||
* @param root - repository root the command runs in.
|
||||
* @param args - pnpm arguments.
|
||||
* @param signal - abort signal that stops the child on SIGINT/SIGTERM.
|
||||
* @param label - step name used in the failure message.
|
||||
*/
|
||||
async function runStep(root: string, args: readonly string[], signal: AbortSignal, label: string): Promise<void> {
|
||||
const invocation = pnpmInvocationFor(args)
|
||||
let result
|
||||
try {
|
||||
result = await execa(invocation.command, invocation.args, {
|
||||
cwd: root, stdio: 'inherit', reject: false, killDescendants: true, cancelSignal: signal,
|
||||
})
|
||||
} catch (error) {
|
||||
if (signal.aborted) throw new Interrupted(label)
|
||||
throw error
|
||||
}
|
||||
if (result.isCanceled || signal.aborted) throw new Interrupted(label)
|
||||
if (result.exitCode !== 0) throw new Error(`${label} exited with ${String(result.exitCode ?? result.signal)}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the installation summary the developer copies from.
|
||||
* 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>
|
||||
}): 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`)
|
||||
process.stdout.write(`Artifacts: ${options.directory}\n`)
|
||||
process.stdout.write(`Registry: ${options.url}\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')
|
||||
}
|
||||
|
||||
/** Build one run directory name: sortable by time and unique per invocation. */
|
||||
function runId(): string {
|
||||
return `${new Date().toISOString().replace(/[:.]/gu, '-')}-${randomUUID().slice(0, 8)}`
|
||||
}
|
||||
|
||||
/** 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, '..')
|
||||
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.
|
||||
let stopping = false
|
||||
const onSignal = (signal: NodeJS.Signals): void => {
|
||||
if (stopping) {
|
||||
process.exit(130)
|
||||
return
|
||||
}
|
||||
stopping = true
|
||||
abort.abort(signal)
|
||||
}
|
||||
process.on('SIGINT', onSignal)
|
||||
process.on('SIGTERM', onSignal)
|
||||
|
||||
let registry
|
||||
try {
|
||||
// 1. The catalog is the only root list; the inventory is how a local package is distinguished from an external one.
|
||||
const entries = generateOfficialBundleCatalog(root).map(entry => entry.packageName)
|
||||
const packages = readWorkspacePackages(root)
|
||||
const selection = selectBundleDependencies({ entries, packages, platform: hostPlatform() })
|
||||
const release = readReleaseMembers(root)
|
||||
verifyReleaseVersions(root)
|
||||
|
||||
// 2. Current official artifacts, so the run never serves an earlier build.
|
||||
await runStep(root, ['run', 'build:official'], abort.signal, 'build:official')
|
||||
verifyBuiltBundles({ entries, packages })
|
||||
|
||||
// 3. One immutable, run-owned directory; a later run never overwrites these bytes.
|
||||
const buildId = runId()
|
||||
const directory = join(root, REGISTRY_OUTPUT_ROOT, buildId)
|
||||
mkdirSync(directory, { recursive: true })
|
||||
const artifacts = await packBundleArtifacts({
|
||||
root, destination: directory, members: selection.members, release, signal: abort.signal,
|
||||
})
|
||||
|
||||
// 4. The manifest is the completion marker: only a fully packed run has one.
|
||||
const source = sourceEvidence(root, getDshRuntimeVersion())
|
||||
const manifest = createRegistryManifest({
|
||||
buildId, source, entries, workspaceNames: selection.workspaceNames, artifacts,
|
||||
})
|
||||
await writeRegistryManifest(join(directory, REGISTRY_MANIFEST_FILE), manifest)
|
||||
|
||||
// 5. Serve, then announce: the summary is never printed for a registry that did not listen.
|
||||
registry = await startBundleRegistry({
|
||||
directory, artifacts, workspaceNames: selection.workspaceNames,
|
||||
})
|
||||
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 })
|
||||
})
|
||||
await registry.close()
|
||||
registry = undefined
|
||||
process.stdout.write('Registry stopped. Artifacts and installed packages stay on disk.\n')
|
||||
} catch (error) {
|
||||
if (registry !== undefined) await registry.close()
|
||||
// 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
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
process.off('SIGINT', onSignal)
|
||||
process.off('SIGTERM', onSignal)
|
||||
}
|
||||
}
|
||||
|
||||
await main()
|
||||
@@ -195,6 +195,10 @@ import { defineConfig } from 'tsdown'
|
||||
export default defineConfig({
|
||||
entry: { client: 'src.ts' }, outDir: 'lib', format: 'cjs', platform: 'browser', dts: false, clean: false,
|
||||
outputOptions: { entryFileNames: 'client.js' },
|
||||
hooks: { 'build:done': async () => {
|
||||
const { writeFile } = await import('node:fs/promises')
|
||||
await writeFile(new URL('./hook-complete', import.meta.url), 'ready')
|
||||
} },
|
||||
})
|
||||
`)
|
||||
const sourcePath = join(root, 'src.ts')
|
||||
@@ -202,6 +206,7 @@ export default defineConfig({
|
||||
await writeFile(sourcePath, 'export const version = "watch-v1"\n')
|
||||
bundles = await watchClientPlugins(root, ['.'], 50)
|
||||
expect(await readFile(bundlePath, 'utf8')).toContain('watch-v1')
|
||||
expect(await readFile(join(root, 'hook-complete'), 'utf8')).toBe('ready')
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1_000))
|
||||
await writeFile(sourcePath, `export const version = "watch-v2-${'x'.repeat(100)}"\n`)
|
||||
|
||||
+11
-11
@@ -173,7 +173,7 @@ export function discoverLibraryDirs(root = repoRoot): string[] {
|
||||
* @param root - repository or fixture root passed to tsdown.
|
||||
* @param pluginDirs - workspace-relative package directories to watch.
|
||||
* @param pollInterval - optional source-watcher polling interval in milliseconds.
|
||||
* @returns live bundles after every watcher has completed its initial build.
|
||||
* @returns live bundles after every watcher has completed its initial build and package-owned post-build hooks.
|
||||
*/
|
||||
export async function watchClientPlugins(
|
||||
root: string,
|
||||
@@ -187,17 +187,17 @@ export async function watchClientPlugins(
|
||||
const bundles = await build({
|
||||
cwd: root,
|
||||
workspace: [...pluginDirs],
|
||||
configLoader: 'native',
|
||||
watch: true,
|
||||
hooks: {
|
||||
'build:done': ({ options }) => {
|
||||
if (initialized.has(options)) return
|
||||
initialized.add(options)
|
||||
readiness.initializedBuilds += 1
|
||||
if (
|
||||
readiness.expectedBuilds !== undefined
|
||||
&& readiness.initializedBuilds >= readiness.expectedBuilds
|
||||
) resolveInitialBuilds?.()
|
||||
},
|
||||
// Workspace hooks override inline hooks; onSuccess runs after each package's build:done work.
|
||||
onSuccess: (options) => {
|
||||
if (initialized.has(options)) return
|
||||
initialized.add(options)
|
||||
readiness.initializedBuilds += 1
|
||||
if (
|
||||
readiness.expectedBuilds !== undefined
|
||||
&& readiness.initializedBuilds >= readiness.expectedBuilds
|
||||
) resolveInitialBuilds?.()
|
||||
},
|
||||
...pollInterval !== undefined
|
||||
? { inputOptions: { watch: { watcher: { usePolling: true, pollInterval } } } }
|
||||
|
||||
@@ -65,7 +65,7 @@ const PACKAGE_LIBRARIES: Readonly<Record<string, string>> = {
|
||||
'packages/core/scope': 'Scoped-context primitives; exports functions and types without a plugin entry.',
|
||||
'packages/experimental/webworker-packer': 'Build-time VFS image packer and command library.',
|
||||
'packages/experimental/webworker-runtime': 'Browser worker runtime library with explicit host entry points.',
|
||||
'packages/hooks/hook-protocol': 'Shared wire-protocol library between the hook bridges.',
|
||||
'packages/experimental/hook-protocol': 'Shared wire-protocol library between the hook bridges.',
|
||||
'packages/identity/anonymous-user-id': 'Harness-home identity helper with no plugin registration.',
|
||||
'packages/sandbox/sandbox-windows-acl': 'Windows ACL sandbox library consumed by sandbox-local.',
|
||||
'packages/sdk/client': 'Client-process library; the spawned runtime owns plugin behavior.',
|
||||
@@ -77,7 +77,6 @@ const PACKAGE_LIBRARIES: Readonly<Record<string, string>> = {
|
||||
'packages/session/session-format-v3-to-v4': 'Released V3 codec reuse and adjacent migration library.',
|
||||
'packages/session/session-telemetry': 'Telemetry Service Definition and capture library; providers mount the backend.',
|
||||
'packages/session/session-title-llm': 'Shared LLM title-provider registration and request policy.',
|
||||
'packages/subagent/subagent-in-process-driver': 'Shared one-shot child-agent driver used by provider plugins.',
|
||||
'packages/subprocess/win32-process': 'Low-level Win32 process and Job Object primitives.',
|
||||
'packages/test-support/session-snapshot': 'Test infrastructure; mounts nothing into a product composition.',
|
||||
'packages/test-support/agent-loop-testkit': 'Test helper library; mounts nothing into a product composition.',
|
||||
|
||||
@@ -4,14 +4,16 @@ import { builtDeclarationPath } from './doc-typecheck-paths.ts'
|
||||
describe('builtDeclarationPath', () => {
|
||||
it('maps package source directories and exact entry files to built declarations', () => {
|
||||
expect(builtDeclarationPath('./packages/*/*/src')).toBe('./packages/*/*/lib/types')
|
||||
expect(builtDeclarationPath('./packages/runtime-diagnostics/invariants/src/index.ts'))
|
||||
.toBe('./packages/runtime-diagnostics/invariants/lib/types/index.d.ts')
|
||||
expect(builtDeclarationPath('./packages/core/session/src/invariant.ts'))
|
||||
.toBe('./packages/core/session/lib/types/invariant.d.ts')
|
||||
expect(builtDeclarationPath('./packages/core/session/src/index.ts'))
|
||||
.toBe('./packages/core/session/lib/types/index.d.ts')
|
||||
expect(builtDeclarationPath('./packages/core/session/src/types.ts'))
|
||||
.toBe('./packages/core/session/lib/types/types.d.ts')
|
||||
expect(builtDeclarationPath('./packages/experimental/claude-code-mods/examples/*'))
|
||||
.toBe('./packages/experimental/claude-code-mods/examples/*')
|
||||
})
|
||||
|
||||
it('rejects aliases without a supported source target', () => {
|
||||
expect(() => builtDeclarationPath('./packages/runtime-diagnostics/invariants/source/index.ts'))
|
||||
expect(() => builtDeclarationPath('./packages/core/session/source/index.ts'))
|
||||
.toThrow('cannot map workspace source path')
|
||||
})
|
||||
})
|
||||
@@ -8,6 +8,8 @@ export function builtDeclarationPath(candidate: string): string {
|
||||
if (candidate.endsWith('/src/*')) {
|
||||
return `${candidate.slice(0, -'/src/*'.length)}/lib/types/*`
|
||||
}
|
||||
// Example plugin entries ship no declarations; a documentation fence never imports them, so they stay as authored.
|
||||
if (candidate.includes('/examples/')) return candidate
|
||||
const sourceFile = /^(.*)\/src\/(.+)\.ts$/.exec(candidate)
|
||||
if (sourceFile?.[1] && sourceFile[2]) {
|
||||
return `${sourceFile[1]}/lib/types/${sourceFile[2]}.d.ts`
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/** Retained package names keep experimental isolation and persisted record namespaces. */
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS,
|
||||
experimentalPackageRecordNamespace,
|
||||
hasExperimentalPackageReference,
|
||||
isExperimentalPackageName,
|
||||
} from './experimental-package-policy.ts'
|
||||
|
||||
const names = Object.values(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS)
|
||||
|
||||
describe('experimental package identities', () => {
|
||||
it.each([...names, '@deepseek-ai/dsh-experimental-terminal-bundle'])('recognizes %s independently of installation layout', (name) => {
|
||||
expect(isExperimentalPackageName(name)).toBe(true)
|
||||
for (const reference of [name, `${name}/client`, `${name}@1.2.3`, `npm:${name}@1.2.3`, `workspace:${name}@*`,
|
||||
`\0virtual:${name}/client?raw`, `/node_modules/${name}/lib/index.js`, `C:\\node_modules\\${name}\\lib\\index.js`, `C:\\node_modules\\${name.replaceAll('/', '\\')}\\lib\\index.js`]) {
|
||||
expect(hasExperimentalPackageReference(reference), reference).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it.each(names)('does not extend the retained-name exception %s to another package', (name) => {
|
||||
for (const reference of [`${name}-unrelated`, `other-${name}`, name.replace('@deepseek-ai/', '@other/')]) {
|
||||
expect(isExperimentalPackageName(reference)).toBe(false)
|
||||
expect(hasExperimentalPackageReference(reference)).toBe(false)
|
||||
expect(experimentalPackageRecordNamespace(reference)).toBeUndefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps new experimental packages under their prefixed namespaces', () => {
|
||||
expect(experimentalPackageRecordNamespace('@deepseek-ai/dsh-experimental-bridge')).toBe('bridge')
|
||||
expect(hasExperimentalPackageReference('@deepseek-ai/dsh-core')).toBe(false)
|
||||
})
|
||||
|
||||
it.each(Object.entries(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS))('keeps %s record names independent of the retained npm name', (directory, name) => {
|
||||
expect(experimentalPackageRecordNamespace(name)).toBe(directory.split('/').at(-1))
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,53 @@
|
||||
/** Experimental package identities, publication policy, and retained npm names. */
|
||||
|
||||
/** Existing npm names retained by capabilities now housed under experimental/. */
|
||||
export const EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS: Readonly<Record<string, string>> = {
|
||||
'packages/experimental/tool-session-query': '@deepseek-ai/dsh-tool-session-query',
|
||||
'packages/experimental/session-title-all-prompts-llm': '@deepseek-ai/dsh-session-title-all-prompts-llm',
|
||||
'packages/experimental/tool-terminal': '@deepseek-ai/dsh-tool-terminal',
|
||||
'packages/experimental/tool-ralph': '@deepseek-ai/dsh-tool-ralph',
|
||||
'packages/experimental/skill-badge': '@deepseek-ai/dsh-skill-badge',
|
||||
'packages/experimental/hook-protocol': '@deepseek-ai/dsh-hook-protocol',
|
||||
'packages/experimental/hooks-claude-code': '@deepseek-ai/dsh-hooks-claude-code',
|
||||
'packages/experimental/hooks-codex': '@deepseek-ai/dsh-hooks-codex',
|
||||
'packages/experimental/webhook': '@deepseek-ai/dsh-webhook',
|
||||
'packages/experimental/webhook-github': '@deepseek-ai/dsh-webhook-github',
|
||||
} satisfies Record<`packages/experimental/${string}`, `@deepseek-ai/dsh-${string}`>
|
||||
|
||||
/** npm namespace for experimental packages without retained-name exceptions. */
|
||||
export const EXPERIMENTAL_PACKAGE_NAME_PREFIX = '@deepseek-ai/dsh-experimental-'
|
||||
const retainedNames = new Set(Object.values(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS))
|
||||
|
||||
/**
|
||||
* Identify an experimental npm package independently of its installed directory.
|
||||
* @param name - complete npm package name, without a version or subpath.
|
||||
* @returns Whether the name uses the experimental prefix or a retained name.
|
||||
*/
|
||||
export function isExperimentalPackageName(name: string): boolean {
|
||||
return name.startsWith(EXPERIMENTAL_PACKAGE_NAME_PREFIX) || retainedNames.has(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find experimental identities in module ids, paths, subpaths, or npm alias specifications.
|
||||
* @param reference - package reference or bundler/runtime module identifier.
|
||||
* @returns Whether a delimited package name identifies an experimental capability.
|
||||
*/
|
||||
export function hasExperimentalPackageReference(reference: string): boolean {
|
||||
const normalized = reference.replaceAll('\\', '/')
|
||||
return [...normalized.matchAll(/(?:^|[/:\u0000])(@deepseek-ai\/[^/@?#:\s]+)(?=$|[/@?#:])/g)]
|
||||
.some(([, name]) => name !== undefined && isExperimentalPackageName(name))
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the package-owned plugin record namespace without changing persisted names.
|
||||
* @param name - complete experimental npm package name.
|
||||
* @returns Capability suffix, or undefined for a non-experimental name.
|
||||
*/
|
||||
export function experimentalPackageRecordNamespace(name: string): string | undefined {
|
||||
if (!isExperimentalPackageName(name)) return undefined
|
||||
return name.slice(name.startsWith(EXPERIMENTAL_PACKAGE_NAME_PREFIX) ? EXPERIMENTAL_PACKAGE_NAME_PREFIX.length : '@deepseek-ai/dsh-'.length)
|
||||
}
|
||||
|
||||
/** Experimental packages excluded from public releases and npm baselines. */
|
||||
export const PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES: readonly string[] = []
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/** File creation shared by repository scanner fixtures. */
|
||||
import { mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
|
||||
/**
|
||||
* Write a text or JSON fixture, creating its containing directories.
|
||||
* @param root - Owning temporary fixture directory.
|
||||
* @param path - Path relative to that directory.
|
||||
* @param value - Literal text or JSON-serializable fixture value.
|
||||
*/
|
||||
export function writeFixtureFile(root: string, path: string, value: unknown): void {
|
||||
const target = join(root, path)
|
||||
mkdirSync(dirname(target), { recursive: true })
|
||||
writeFileSync(target, typeof value === 'string' ? value : `${JSON.stringify(value)}\n`)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Preserve Node's exit before Vitest installs its test-level process.exit guard.
|
||||
globalThis.__dshCoverageFixtureExit = process.exit.bind(process)
|
||||
@@ -0,0 +1,18 @@
|
||||
import { resolve } from 'node:path'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { coverageForkPool } from '../coverage-fork-diagnostics.ts'
|
||||
|
||||
const cacheDir = process.env.DSH_TEST_FORK_CACHE
|
||||
if (cacheDir === undefined || cacheDir === '') throw new Error('fork fixture requires its private cache directory')
|
||||
|
||||
export default defineConfig({
|
||||
cacheDir,
|
||||
test: {
|
||||
name: 'configured-fork',
|
||||
include: ['scripts/fixtures/coverage-fork.fixture.ts'],
|
||||
pool: process.env.DSH_TEST_FORK_POOL === 'stock' ? 'forks' : coverageForkPool,
|
||||
execArgv: ['--require', resolve(import.meta.dirname, 'coverage-fork-exit.cjs')],
|
||||
maxWorkers: 1,
|
||||
cache: false,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
import { writeSync } from 'node:fs'
|
||||
import { expect, it } from 'vitest'
|
||||
|
||||
declare global {
|
||||
/** Native process exit captured by this fixture's Node preload. */
|
||||
function __dshCoverageFixtureExit(code: number): never
|
||||
}
|
||||
|
||||
it('runs in the configured pool', () => {
|
||||
writeSync(2, `coverage-fixture-pid: ${process.pid}\n`)
|
||||
if (process.env.DSH_TEST_FORK_OUTCOME === 'exit') {
|
||||
globalThis.__dshCoverageFixtureExit(23)
|
||||
}
|
||||
expect(2 + 2).toBe(4)
|
||||
})
|
||||
@@ -1,14 +1,17 @@
|
||||
/** Hold the advanced workflow child's first step until its parent records membership. */
|
||||
/** Order child execution after the advanced parent records its corresponding handoff. */
|
||||
export const name = 'python-snapshot-workflow-order'
|
||||
|
||||
/**
|
||||
* @param {import('@deepseek-ai/cordis').Context} ctx - Scenario-local host context.
|
||||
* @param {{ parentSessionId: string, prompt: string }} config - Exact advanced scenario identities.
|
||||
* @param {{ parentSessionId: string, prompt: string, direct?: { prompt: string, callId: string } }} config - Exact advanced scenario identities.
|
||||
*/
|
||||
export function apply(ctx, config) {
|
||||
const started = new Set()
|
||||
const pending = new Map()
|
||||
let disposed = false
|
||||
let directResultSeq
|
||||
let directStep = false
|
||||
let directDelivered = false
|
||||
|
||||
ctx.effect(() => async () => {
|
||||
disposed = true
|
||||
@@ -18,19 +21,33 @@ export function apply(ctx, config) {
|
||||
started.clear()
|
||||
})
|
||||
ctx.on('session/event', (session, event) => {
|
||||
if (disposed || session.id !== config.parentSessionId || event.type !== 'tool-workflow/agent-start') return
|
||||
started.add(event.data.childId)
|
||||
pending.get(event.data.childId)?.resolve()
|
||||
if (disposed || session.id !== config.parentSessionId) return
|
||||
if (event.type === 'tool-workflow/agent-start') {
|
||||
started.add(event.data.childId)
|
||||
pending.get(event.data.childId)?.resolve()
|
||||
}
|
||||
if (config.direct === undefined) return
|
||||
if (event.type === 'tool/result' && event.data.message.source.callId === config.direct.callId) directResultSeq = event.seq
|
||||
if (directResultSeq === undefined) return
|
||||
if (event.type === 'step/start') directStep = true
|
||||
if (event.type === 'session-log-deepseek/delivery-accepted' && event.data.throughSeq >= directResultSeq) {
|
||||
directDelivered = true
|
||||
}
|
||||
if (directStep && directDelivered) {
|
||||
for (const wait of pending.values()) if (wait.direct) wait.resolve()
|
||||
}
|
||||
})
|
||||
ctx.on('agent/pre-step', async ({ agent, messages, turn, step, signal }, next) => {
|
||||
const hasPrompt = prompt => messages.some(message => message.content.some(block => block.type === 'text' && block.text === prompt))
|
||||
const direct = config.direct !== undefined && hasPrompt(config.direct.prompt)
|
||||
if (agent.session.header.parentSession !== config.parentSessionId || turn !== 1 || step !== 1
|
||||
|| !messages.some(message => message.content.some(block => block.type === 'text' && block.text === config.prompt))) {
|
||||
|| !direct && !hasPrompt(config.prompt)) {
|
||||
return next()
|
||||
}
|
||||
signal.throwIfAborted()
|
||||
if (disposed) throw new Error('workflow snapshot barrier disposed')
|
||||
if (!started.has(agent.id)) {
|
||||
const wait = Promise.withResolvers()
|
||||
if (direct ? !directStep || !directDelivered : !started.has(agent.id)) {
|
||||
const wait = { ...Promise.withResolvers(), direct }
|
||||
const abort = () => { wait.reject(signal.reason) }
|
||||
signal.addEventListener('abort', abort, { once: true })
|
||||
wait.done = wait.promise.finally(() => {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/** Command discovery and complete output preservation for the CLI reference. */
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { collectCliHelp, parseCliHelp } from './gen-cli-help.ts'
|
||||
|
||||
describe('CLI help discovery', () => {
|
||||
it('reads command rows while retaining wrapped descriptions and examples', () => {
|
||||
const help = 'Usage: dsh\r\n\r\nCommands:\r\n web [options] Serve the browser\r\n with a wrapped description\r\n plugin Manage plugins\r\n\r\nExamples:\r\n dsh web\r\n'
|
||||
const result = parseCliHelp(help)
|
||||
expect(result.commands).toEqual(['web', 'plugin'])
|
||||
expect(result.output).toBe(help.replaceAll('\r\n', '\n'))
|
||||
})
|
||||
|
||||
it('includes plugin, both profile forms, and their nested commands when launcher help has no command list', async () => {
|
||||
const visited: string[] = []
|
||||
const pages = await collectCliHelp(async (args) => {
|
||||
visited.push(args.join(' '))
|
||||
if (args.length === 0) return 'Usage: dsh [--profile] <name>\n'
|
||||
if (args[0] === 'plugin') return 'Usage: dsh plugin --profile <name> <args...>\n'
|
||||
const selected = args[0] === '--profile' ? args.slice(1) : args
|
||||
if (selected[1] === 'nested') return 'Usage: nested\n'
|
||||
return `Usage: ${selected[0]} [command]\n\nCommands:\n nested Run the nested command\n`
|
||||
}, ['web'])
|
||||
expect(visited).toEqual(['', 'plugin', 'web', 'web nested', '--profile web'])
|
||||
expect(pages.map(page => page.invocations)).toEqual([
|
||||
['dsh'], ['dsh plugin'], ['dsh web', 'dsh --profile web'],
|
||||
['dsh web nested', 'dsh --profile web nested'],
|
||||
])
|
||||
})
|
||||
|
||||
it('visits each command alias and lists a help command that repeats its parent once', async () => {
|
||||
const visited: string[] = []
|
||||
const parent = 'Usage: web [command]\n\nCommands:\n serve|s [options] Serve the UI\n help [command] display help for command\n'
|
||||
const pages = await collectCliHelp(async (args) => {
|
||||
visited.push(args.join(' '))
|
||||
if (args.length === 0) return 'Usage: dsh\n'
|
||||
if (args[0] === 'plugin') return 'Usage: dsh plugin\n'
|
||||
const selected = args[0] === '--profile' ? args.slice(1) : args
|
||||
return selected[1] === 'serve' || selected[1] === 's' ? 'Usage: web serve\n' : parent
|
||||
}, ['web'])
|
||||
expect(visited).toEqual(['', 'plugin', 'web', 'web serve', 'web s', 'web help', '--profile web'])
|
||||
expect(pages.map(page => page.invocations)).toEqual([
|
||||
['dsh'], ['dsh plugin'], ['dsh web', 'dsh web help', 'dsh --profile web', 'dsh --profile web help'],
|
||||
['dsh web serve', 'dsh web s', 'dsh --profile web serve', 'dsh --profile web s'],
|
||||
])
|
||||
})
|
||||
|
||||
it.each(['', ' \r\n\n'])('rejects blank help %j', (help) => {
|
||||
expect(() => parseCliHelp(help)).toThrow('CLI help returned empty output')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,195 @@
|
||||
/** Generate paired CLI references by invoking the shipped launcher and profile help. */
|
||||
|
||||
import { execFile } from 'node:child_process'
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import { PROFILE_TEMPLATES } from '@deepseek-ai/dsh-app-boot'
|
||||
import {
|
||||
computeTranslationPairingRecord,
|
||||
renderTranslationPairingRecord,
|
||||
translationPairPaths,
|
||||
} from './translation-pairing-record.ts'
|
||||
import { parseTranslationPairingManifest, translationPairSourcePredicate } from './translation-pairing.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const paths = translationPairPaths('docs/cli-help.md')
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
/** Complete help text and the immediate subcommands it advertises. */
|
||||
export interface CliHelp {
|
||||
/** Complete help text with LF line endings. */
|
||||
output: string
|
||||
/** Immediate command names and aliases listed in the `Commands:` section, in their displayed order. */
|
||||
commands: string[]
|
||||
}
|
||||
|
||||
interface HelpPage {
|
||||
help: CliHelp
|
||||
invocations: [string, ...string[]]
|
||||
children: Map<string, HelpPage>
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover commands from actual text help while preserving every displayed line.
|
||||
* @param stdout - Successful CLI help output.
|
||||
* @returns Help text with LF line endings and the command names and aliases (`serve|s`) listed in its `Commands:` section.
|
||||
* @throws When the output is empty.
|
||||
*/
|
||||
export function parseCliHelp(stdout: string): CliHelp {
|
||||
const output = stdout.replaceAll('\r\n', '\n')
|
||||
if (output.trim() === '') throw new Error('CLI help returned empty output')
|
||||
const section = /^Commands:\n((?:[ \t][^\n]*\n|\n)*)/mu.exec(output)?.[1] ?? ''
|
||||
const commands = [...section.matchAll(/^ {2}\S+/gmu)].flatMap(match => match[0].slice(2).split('|'))
|
||||
return { output, commands }
|
||||
}
|
||||
|
||||
// A `help [command]` row prints its parent's help, so a page can list itself as a child.
|
||||
// An invocation that reaches a page already on the current path joins that page without
|
||||
// extending its children, which keeps alias propagation finite.
|
||||
function addAlias(page: HelpPage, invocation: string, path: ReadonlySet<HelpPage>): void {
|
||||
if (page.invocations.includes(invocation)) return
|
||||
page.invocations.push(invocation)
|
||||
if (path.has(page)) return
|
||||
const childPath = new Set([...path, page])
|
||||
for (const [name, child] of page.children) addAlias(child, `${invocation} ${name}`, childPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect launcher, plugin, and profile help even when root help lists no commands.
|
||||
* @param readHelp - run one help invocation and return its complete stdout.
|
||||
* @param profiles - shipped profile names whose explicit and shorthand forms must be documented.
|
||||
* @returns distinct help pages with every discovered invocation and nested command.
|
||||
*/
|
||||
export async function collectCliHelp(
|
||||
readHelp: (args: readonly string[]) => Promise<string>, profiles: readonly string[],
|
||||
): Promise<HelpPage[]> {
|
||||
const pages = new Map<string, HelpPage>()
|
||||
const visit = async (args: string[], path: ReadonlySet<HelpPage>): Promise<HelpPage> => {
|
||||
const invocation = ['dsh', ...args].join(' ')
|
||||
const help = parseCliHelp(await readHelp(args))
|
||||
const existing = pages.get(help.output)
|
||||
if (existing !== undefined) {
|
||||
addAlias(existing, invocation, path)
|
||||
return existing
|
||||
}
|
||||
const page: HelpPage = { help, invocations: [invocation], children: new Map() }
|
||||
pages.set(help.output, page)
|
||||
const childPath = new Set([...path, page])
|
||||
for (const name of help.commands) page.children.set(name, await visit([...args, name], childPath))
|
||||
return page
|
||||
}
|
||||
const top = new Set<HelpPage>()
|
||||
await visit([], top)
|
||||
await visit(['plugin'], top)
|
||||
for (const profile of [...profiles].sort()) {
|
||||
await visit([profile], top)
|
||||
await visit(['--profile', profile], top)
|
||||
}
|
||||
return [...pages.values()]
|
||||
}
|
||||
|
||||
async function collectHelp(): Promise<HelpPage[]> {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'dsh-cli-help-'))
|
||||
const environment = {
|
||||
...process.env,
|
||||
DSH_HOME: join(directory, 'harness'),
|
||||
DSH_AGENTS_HOME: join(directory, 'agents'),
|
||||
DSH_TELEMETRY_DISABLED: '1',
|
||||
NO_COLOR: '1',
|
||||
}
|
||||
for (const name of ['NODE_OPTIONS', 'FORCE_COLOR']) Reflect.deleteProperty(environment, name)
|
||||
try {
|
||||
return await collectCliHelp(async (args) => {
|
||||
const { stdout, stderr } = await execFileAsync(process.execPath, [
|
||||
'--import', 'tsx/esm', 'apps/cli/src/bin.ts', ...args, '--help',
|
||||
], {
|
||||
cwd: root, env: environment, encoding: 'utf8', timeout: 30_000,
|
||||
killSignal: 'SIGKILL', maxBuffer: 2 * 1024 * 1024,
|
||||
})
|
||||
if (stderr !== '') throw new Error(`${['dsh', ...args].join(' ')} --help wrote diagnostics: ${stderr}`)
|
||||
return stdout
|
||||
}, Object.keys(PROFILE_TEMPLATES))
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
function anchor(page: HelpPage): string {
|
||||
return page.invocations[0].replaceAll(/[^a-zA-Z0-9]+/gu, '-').replace(/-$/u, '').toLowerCase()
|
||||
}
|
||||
|
||||
function renderHelp(pages: readonly HelpPage[], chinese: boolean): string {
|
||||
const lines = [
|
||||
'<!-- Generated by scripts/gen-cli-help.ts — do not edit by hand.',
|
||||
' Run `pnpm run gen-cli-help` to regenerate. -->',
|
||||
'',
|
||||
chinese ? '# dsh 命令行帮助' : '# dsh CLI help',
|
||||
'',
|
||||
...(chinese ? ['[English](cli-help.md) | 中文', ''] : []),
|
||||
chinese
|
||||
? '此参考收录启动器、子命令和随附 profile 的完整帮助。同一份帮助只展示一次,并列出所有调用方式。帮助输出保留原文。'
|
||||
: 'This reference contains complete help for the launcher, subcommands, and shipped profiles. Identical help appears once with every invocation listed. Help output retains its original language.',
|
||||
'',
|
||||
chinese
|
||||
? '运行 `pnpm run gen-cli-help` 重新生成;运行 `pnpm run verify-cli-help` 检查是否与当前命令一致。'
|
||||
: 'Run `pnpm run gen-cli-help` to regenerate; run `pnpm run verify-cli-help` to check freshness.',
|
||||
'',
|
||||
chinese ? '## 目录' : '## Contents',
|
||||
'',
|
||||
...pages.map(page => `- [\`${page.invocations[0]}\`](#${anchor(page)})`),
|
||||
'',
|
||||
]
|
||||
for (const page of pages) {
|
||||
lines.push(
|
||||
`<a id="${anchor(page)}"></a>`,
|
||||
`## \`${page.invocations[0]}\``,
|
||||
'',
|
||||
`${chinese ? '调用方式' : 'Invocations'}: ${page.invocations.map(invocation => `\`${invocation} --help\``).join(', ')}`,
|
||||
'',
|
||||
`\`\`\`text\n${page.help.output}${page.help.output.endsWith('\n') ? '' : '\n'}\`\`\``,
|
||||
'',
|
||||
)
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/** Regenerate both CLI reference pages, or check their bytes with --check. @returns Nothing. */
|
||||
export async function main(): Promise<void> {
|
||||
const check = process.argv.includes('--check')
|
||||
const pages = await collectHelp()
|
||||
const source = renderHelp(pages, false)
|
||||
const zh = renderHelp(pages, true)
|
||||
const written = new Set([paths.source, paths.zh])
|
||||
const context = {
|
||||
repoRoot: root,
|
||||
isTranslationPairSource: translationPairSourcePredicate(parseTranslationPairingManifest(
|
||||
readFileSync(resolve(root, 'scripts/translation-pairing.manifest.json'), 'utf8'),
|
||||
)),
|
||||
// Resolve links between the two pages as this run writes them, so a first run
|
||||
// records the same hashes that verify-translation-pairing computes afterward.
|
||||
repositoryFileExists: (path: string) => written.has(path)
|
||||
|| existsSync(resolve(root, path)) && statSync(resolve(root, path)).isFile(),
|
||||
}
|
||||
const outputs = new Map([
|
||||
[paths.source, source],
|
||||
[paths.zh, zh],
|
||||
[paths.meta, renderTranslationPairingRecord(paths, computeTranslationPairingRecord(paths, source, zh, context))],
|
||||
])
|
||||
const changed: string[] = []
|
||||
for (const [path, content] of outputs) {
|
||||
const destination = resolve(root, path)
|
||||
if (existsSync(destination) && readFileSync(destination, 'utf8') === content) continue
|
||||
changed.push(path)
|
||||
if (!check) writeFileSync(destination, content)
|
||||
}
|
||||
if (check && changed.length > 0) {
|
||||
console.error(`gen-cli-help: stale — ${changed.join(', ')}. Run pnpm run gen-cli-help.`)
|
||||
process.exitCode = 1
|
||||
} else {
|
||||
console.log(`gen-cli-help: ${pages.length} help pages; ${check ? 'all outputs are current' : `${changed.length} files written`}.`)
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] !== undefined && import.meta.filename === resolve(process.argv[1])) await main()
|
||||
@@ -50,6 +50,8 @@ export { REGION_BEGIN, REGION_END }
|
||||
* errors, so the partition can never silently drift from the service API.
|
||||
*/
|
||||
export const SERVICE_PAGE: Record<string, string> = {
|
||||
translator: 'translation.md',
|
||||
cotTranslation: 'translation.md',
|
||||
speechToText: 'voice-input.md',
|
||||
speechController: 'voice-input.md',
|
||||
otel: 'otel.md',
|
||||
@@ -93,7 +95,6 @@ export const SERVICE_PAGE: Record<string, string> = {
|
||||
schedule: 'schedule.md',
|
||||
inspector: 'extensions.md',
|
||||
webServer: 'web-server.md',
|
||||
invariants: 'invariants.md',
|
||||
llm: 'llm-streaming.md',
|
||||
lsp: 'lsp.md',
|
||||
messageFeedback: 'feedback.md',
|
||||
@@ -110,6 +111,8 @@ export const SERVICE_PAGE: Record<string, string> = {
|
||||
sessionReferenceResolver: 'session-reference.md',
|
||||
sessionProjectionCache: 'session-projection.md',
|
||||
sessionProjections: 'session-projection.md',
|
||||
workingDirectory: 'working-directory.md',
|
||||
worktrees: 'worktrees.md',
|
||||
sessionController: 'session.md',
|
||||
sessionSkillCatalog: 'skills.md',
|
||||
sessions: 'session.md',
|
||||
@@ -127,6 +130,7 @@ export const SERVICE_PAGE: Record<string, string> = {
|
||||
jobController: 'jobs.md',
|
||||
sessionTelemetry: 'session-telemetry.md',
|
||||
agentTeams: 'agent-team.md',
|
||||
claudeCodeMods: 'claude-code-mods.md',
|
||||
tokenMeter: 'token-meter.md',
|
||||
toolResultPruner: 'compaction.md',
|
||||
tools: 'tools.md',
|
||||
@@ -320,6 +324,8 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
AssistantStreamFrame: 'core.md',
|
||||
ContentBlock: 'llm-streaming.md',
|
||||
CreateAgentOptions: 'core.md',
|
||||
CreateWorktreeRequest: 'worktrees.md',
|
||||
CreatedWorktree: 'worktrees.md',
|
||||
GenerateOptions: 'llm-streaming.md',
|
||||
Inbox: 'core.md',
|
||||
InboxItem: 'core.md',
|
||||
@@ -334,6 +340,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
DeepSeekLlmApiExtensionProvider: 'llm-streaming.md',
|
||||
DeepSeekLlmApiExtensionRequest: 'llm-streaming.md',
|
||||
LlmCallConfig: 'llm-streaming.md',
|
||||
ConfigureCall: 'llm-streaming.md',
|
||||
LlmModelContext: 'llm-streaming.md',
|
||||
LlmModelReasoningInfo: 'llm-streaming.md',
|
||||
LlmResolvedModelInfo: 'llm-streaming.md',
|
||||
@@ -600,8 +607,8 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
SpillRef: 'spill.md',
|
||||
ContinuableCreateRequest: 'subagent.md',
|
||||
ContinuableCreateSpec: 'subagent.md',
|
||||
ContinuableStart: 'subagent.md',
|
||||
ContinuableStartSpec: 'subagent.md',
|
||||
SubagentActivation: 'subagent.md',
|
||||
SubagentActivationSpec: 'subagent.md',
|
||||
AgentMessageSource: 'subagent.md',
|
||||
SubagentCatalogEntry: 'subagent.md',
|
||||
SubagentCatalogState: 'subagent.md',
|
||||
@@ -665,6 +672,13 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
SpeechProviderInfo: 'voice-input.md',
|
||||
SpeechInput: 'voice-input.md',
|
||||
SpeechRequest: 'voice-input.md',
|
||||
TranslationProvider: 'translation.md',
|
||||
TranslationRequest: 'translation.md',
|
||||
TranslationSpec: 'translation.md',
|
||||
CotTranslationPreferences: 'translation.md',
|
||||
CotTranslationSnapshot: 'translation.md',
|
||||
TranslationError: 'translation.md',
|
||||
TranslationErrorCode: 'translation.md',
|
||||
SpeechSpec: 'voice-input.md',
|
||||
Transcript: 'voice-input.md',
|
||||
SpeechCatalog: 'voice-input.md',
|
||||
@@ -683,6 +697,8 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
TeamMemberProjection: 'agent-team.md',
|
||||
TeamWaitResult: 'agent-team.md',
|
||||
UpdateTeamTaskRequest: 'agent-team.md',
|
||||
ModDefinition: 'claude-code-mods.md',
|
||||
SurfaceSnapshot: 'claude-code-mods.md',
|
||||
TokenMeasurement: 'token-meter.md',
|
||||
PtcDispatchLog: 'tools.md',
|
||||
PostToolDecision: 'tools.md',
|
||||
@@ -753,7 +769,6 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
PermissionCatalog: 'permission-presets.md',
|
||||
PresetOption: 'permission-presets.md',
|
||||
PresetSpec: 'permission-presets.md',
|
||||
InvariantInstaller: 'invariants.md',
|
||||
WebRoute: 'web-server.md',
|
||||
IndexInjection: 'web-server.md',
|
||||
StorageBackend: 'storage.md',
|
||||
@@ -796,14 +811,14 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
ProjectionCheckpoint: 'session-projection.md',
|
||||
DirectoryPickerCapability: 'workspace.md',
|
||||
DirectoryListing: 'workspace.md',
|
||||
TypertContribution: 'invariants.md',
|
||||
TypertContribution: 'typert.md',
|
||||
TypertRemoteEventSource: 'typert.md',
|
||||
RemoteEventHostInfo: 'typert.md',
|
||||
TypertFace: 'invariants.md',
|
||||
TypertPackageFilter: 'invariants.md',
|
||||
TypertPackageRecord: 'invariants.md',
|
||||
TypertSchemaFilter: 'invariants.md',
|
||||
TypertSchemaRecord: 'invariants.md',
|
||||
TypertFace: 'typert.md',
|
||||
TypertPackageFilter: 'typert.md',
|
||||
TypertPackageRecord: 'typert.md',
|
||||
TypertSchemaFilter: 'typert.md',
|
||||
TypertSchemaRecord: 'typert.md',
|
||||
}
|
||||
|
||||
/** TypeScript lib and pinned framework types with no repository-owned data page. */
|
||||
@@ -863,6 +878,7 @@ export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
|
||||
AsyncDisposable: 'TypeScript explicit resource management interface',
|
||||
AgentPresetDocument: 'preset composition view is owned by packages/preset/agent-preset-registry/README.md',
|
||||
AgentPresetComposition: 'flattened composition rows are owned by packages/preset/agent-preset-registry/README.md',
|
||||
AgentPresetInspection: 'retained revision modules and isolation diagnostics are owned by packages/preset/agent-preset-registry/README.md',
|
||||
PresetMetadata: 'preset display text is owned by packages/preset/agent-preset-registry/README.md',
|
||||
BashEnvContributor: 'service-local extension type is owned by packages/shell/tool-bash/src/index.ts',
|
||||
BashEnvVariableInfo: 'service-local metadata type is owned by packages/shell/tool-bash/src/index.ts',
|
||||
@@ -915,7 +931,6 @@ export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
|
||||
Translate: 'service-local bound translator is owned by packages/client/i18n/src/index.ts',
|
||||
WebUpgradeRoute:
|
||||
'upgrade route registration contract is owned by packages/host/webserver/src/index.ts',
|
||||
InvariantRegistration: 'service-local lifecycle handle is owned by packages/runtime-diagnostics/invariants/README.md',
|
||||
JsonValue: 'JSON value union is owned by packages/core/session/src/json.ts',
|
||||
KnobState: 'projection unit state fields are owned by packages/interaction/permission-presets/README.md',
|
||||
PromptAssembly: 'assembly result is owned by packages/core/system-prompt/README.md',
|
||||
|
||||
+45
-13
@@ -107,6 +107,21 @@ const GROUP_ORDER = [
|
||||
]
|
||||
|
||||
const SERVICE_ROLES: ServiceRole[] = [
|
||||
{
|
||||
key: 'cotTranslation',
|
||||
pkg: 'experimental-client-ui-cot-translation',
|
||||
title: 'Reasoning translation Remote',
|
||||
mode: 'service',
|
||||
note: 'Serves reasoning translations, reuses saved results, and joins ordinary Session activation for uncached inactive Sessions.',
|
||||
},
|
||||
{
|
||||
key: 'translator',
|
||||
pkg: 'experimental-translator',
|
||||
title: 'Anonymous text translation',
|
||||
mode: 'service',
|
||||
consumers: ['experimental-client-ui-cot-translation'],
|
||||
note: 'Resolves Google or Bing requests and retains shared experimental records through the active Session writer.',
|
||||
},
|
||||
{
|
||||
key: 'hmr',
|
||||
pkg: 'hmr',
|
||||
@@ -147,6 +162,24 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
consumers: ['api-gateway', 'host-frontend-static'],
|
||||
note: 'Owns browser authentication and shared HTTP request dispatch; API adapters register endpoints and streams.',
|
||||
},
|
||||
{
|
||||
key: 'worktrees',
|
||||
pkg: 'experimental-worktree',
|
||||
title: 'Git worktree creation',
|
||||
mode: 'seam',
|
||||
implementations: ['experimental-worktree'],
|
||||
consumers: ['experimental-tool-worktree'],
|
||||
note: 'Explicit experimental creation from a pinned local commit. Existing write permissions govern checkout and shared Git metadata; the working-directory service owns the resulting Session directory.',
|
||||
},
|
||||
{
|
||||
key: 'workingDirectory',
|
||||
pkg: 'working-directory',
|
||||
title: 'Session working directory',
|
||||
mode: 'seam',
|
||||
implementations: ['working-directory'],
|
||||
consumers: ['tool-working-directory', 'tool-fs', 'tool-bash', 'tool-pwsh', 'subagent', 'sdk-jsonrpc-server'],
|
||||
note: 'One Session projection owns the effective execution directory. The filesystem validates changes; user context reports them while original metadata and write grants stay fixed.',
|
||||
},
|
||||
{
|
||||
key: 'mcpResources',
|
||||
pkg: 'mcp-resources',
|
||||
@@ -235,7 +268,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
pkg: 'session',
|
||||
title: 'In-memory session store',
|
||||
mode: 'core',
|
||||
consumers: ['agent-loop', 'agent', 'session-persistence', 'session-query', 'session-query-sqlite', 'subagent-in-process-driver', 'invariants', 'message-feedback'],
|
||||
consumers: ['agent-loop', 'agent', 'session-persistence', 'session-query', 'session-query-sqlite', 'subagent', 'message-feedback'],
|
||||
note: 'Owns append-only Session instances and emits the durable session event feed.',
|
||||
},
|
||||
{
|
||||
@@ -322,14 +355,6 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
mode: 'core',
|
||||
note: 'Carries the picking seam onto the wire: capability gating, cancellation, and the seam-coded failures a browser directory flow discriminates on.',
|
||||
},
|
||||
{
|
||||
key: 'invariants',
|
||||
pkg: 'invariants',
|
||||
title: 'Package-owned invariant registry',
|
||||
mode: 'core',
|
||||
consumers: ['session', 'agent', 'scope', 'agent-loop'],
|
||||
note: 'Companion subpaths register owner-local checks; the service owns selection, uniqueness, child fibers, and package-attributed failures.',
|
||||
},
|
||||
{
|
||||
key: 'typert',
|
||||
pkg: 'typert-registry',
|
||||
@@ -584,7 +609,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
pkg: 'agent',
|
||||
title: 'Agent service',
|
||||
mode: 'core',
|
||||
consumers: ['agent-loop', 'acp', 'subagent-in-process-driver'],
|
||||
consumers: ['agent-loop', 'acp', 'subagent'],
|
||||
note: 'Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation.',
|
||||
},
|
||||
{
|
||||
@@ -730,7 +755,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
mode: 'seam',
|
||||
implementations: ['subagent-spawn-in-process', 'subagent-fork-in-process', 'subagent-acp', 'subagent-codex', 'subagent-claude-code', 'subagent-dsh-sdk'],
|
||||
consumers: ['tool-subagent', 'tool-subagent-control', 'tool-ralph'],
|
||||
note: 'Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route.',
|
||||
note: 'Providers prepare local children or execute external tasks; one activation manager owns both lifecycles. The delegation tool selects a backend, control tools deliver local follow-ups, and workflows await activation results.',
|
||||
},
|
||||
{
|
||||
key: 'speechToText',
|
||||
@@ -749,6 +774,13 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
consumers: ['experimental-tool-agent-team'],
|
||||
note: 'Owns the implicit-root roster, durable peer mailbox, shared task DAG, and continuable-child lifecycle; tool-agent-team contributes model controls.',
|
||||
},
|
||||
{
|
||||
key: 'claudeCodeMods',
|
||||
pkg: 'experimental-claude-code-mods',
|
||||
title: 'Claude Code mods bridge',
|
||||
mode: 'core',
|
||||
note: 'Loads mods that defineMod plugins add, raises their hook chains from harness extension points, and draws the band above the prompt over its Remote.',
|
||||
},
|
||||
{
|
||||
key: 'inspector',
|
||||
pkg: 'inspector',
|
||||
@@ -762,8 +794,8 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
title: 'Background job registry',
|
||||
mode: 'seam',
|
||||
implementations: ['jobs-local'],
|
||||
consumers: ['tool-bash', 'tool-pwsh', 'tool-terminal', 'tool-subagent', 'tool-jobs', 'api-job-controller'],
|
||||
note: 'Producers (background bash/pwsh, PTY sends, and subagent delegations) register running work; record-declaring jobs additionally stream raw output for non-consuming observers; tool-jobs is the model-facing controller that reads, lists, and kills it; jobs-local is the process-local registry.',
|
||||
consumers: ['tool-bash', 'tool-pwsh', 'tool-terminal', 'tool-jobs', 'api-job-controller'],
|
||||
note: 'Producers (background bash/pwsh and PTY sends) register running work; record-declaring jobs additionally stream raw output for non-consuming observers; tool-jobs is the model-facing controller that reads, lists, and kills it; jobs-local is the process-local registry.',
|
||||
},
|
||||
{
|
||||
key: 'web',
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/** Offline Official catalog ownership, completeness, and freshness checks. */
|
||||
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, utimesSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { ON_DEMAND_BUNDLES } from '../packages/boot/app-boot/src/official-bundles.ts'
|
||||
import { generateOfficialBundleCatalog, renderOfficialBundleCatalog, syncOfficialBundleCatalog } from './gen-official-bundle-catalog.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
const output = 'packages/boot/app-boot/src/official-bundles.generated.ts'
|
||||
|
||||
function write(root: string, path: string, content: unknown): void {
|
||||
mkdirSync(dirname(join(root, path)), { recursive: true })
|
||||
writeFileSync(join(root, path), typeof content === 'string' ? content : `${JSON.stringify(content)}\n`)
|
||||
}
|
||||
|
||||
function directory(name: string): string { return `packages/subagent/${name.slice('@deepseek-ai/dsh-'.length)}` }
|
||||
|
||||
function fixture(): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-official-catalog-'))
|
||||
roots.push(root)
|
||||
write(root, 'apps/cli/package.json', { name: '@deepseek-ai/dsh' })
|
||||
for (const name of ON_DEMAND_BUNDLES) {
|
||||
const dir = directory(name)
|
||||
write(root, `${dir}/package.json`, {
|
||||
name, type: 'module', version: '1.2.3', icon: './icon.svg', dsh: { bundle: { patch: './cordis.patch.yml' } },
|
||||
exports: { '.': './must-not-run.js', './package.json': './package.json', './locale/*.json': './locale/*.json' },
|
||||
})
|
||||
write(root, `${dir}/must-not-run.js`, "throw new Error('metadata must not evaluate a provider')")
|
||||
write(root, `${dir}/icon.svg`, '<svg xmlns="http://www.w3.org/2000/svg"/>')
|
||||
write(root, `${dir}/locale/en.json`, { meta: { title: 'Native agent', description: 'Delegate a task.' } })
|
||||
write(root, `${dir}/locale/zh.json`, { meta: { title: '原生智能体', description: '委派任务。' } })
|
||||
}
|
||||
write(root, output, '')
|
||||
return root
|
||||
}
|
||||
|
||||
function updateManifest(root: string, name: string, changes: Record<string, unknown>): void {
|
||||
const path = `${directory(name)}/package.json`
|
||||
const manifest = JSON.parse(readFileSync(join(root, path), 'utf8')) as Record<string, unknown>
|
||||
write(root, path, { ...manifest, ...changes })
|
||||
}
|
||||
|
||||
afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) })
|
||||
|
||||
describe('Official bundle catalog generation', () => {
|
||||
it('reads package resources without evaluating native provider entries or pinning a version', () => {
|
||||
const entries = generateOfficialBundleCatalog(fixture())
|
||||
expect(entries.map(entry => entry.packageName)).toEqual(ON_DEMAND_BUNDLES)
|
||||
expect(entries[0]?.meta).toEqual({
|
||||
title: { en: 'Native agent', zh: '原生智能体' }, description: { en: 'Delegate a task.', zh: '委派任务。' },
|
||||
icon: `data:image/svg+xml;base64,${Buffer.from('<svg xmlns="http://www.w3.org/2000/svg"/>').toString('base64')}`,
|
||||
})
|
||||
expect(renderOfficialBundleCatalog(entries)).not.toContain('1.2.3')
|
||||
})
|
||||
|
||||
it.each(['private', 'not-bundle', 'missing', 'duplicate'])('rejects %s catalog membership', (kind) => {
|
||||
const root = fixture()
|
||||
const name = ON_DEMAND_BUNDLES[0]!
|
||||
if (kind === 'private') updateManifest(root, name, { private: true })
|
||||
if (kind === 'not-bundle') updateManifest(root, name, { dsh: {} })
|
||||
if (kind === 'missing') rmSync(join(root, directory(name)), { recursive: true })
|
||||
if (kind === 'duplicate') write(root, 'packages/subagent/duplicate/package.json', { name })
|
||||
expect(() => generateOfficialBundleCatalog(root)).toThrow(/public DSH bundle|duplicate workspace/)
|
||||
})
|
||||
|
||||
it.each([false, true])('rejects an on-demand provider in the installed dependency closure (indirect=%s)', (indirect) => {
|
||||
const root = fixture()
|
||||
const name = ON_DEMAND_BUNDLES[0]!
|
||||
write(root, 'apps/cli/package.json', { name: '@deepseek-ai/dsh', dependencies: { [indirect ? '@deepseek-ai/dsh-layer' : name]: '*' } })
|
||||
if (indirect) write(root, 'packages/bundle/layer/package.json', { name: '@deepseek-ai/dsh-layer', dependencies: { [name]: '*' } })
|
||||
expect(() => generateOfficialBundleCatalog(root)).toThrow('must not be installed by the DSH distribution')
|
||||
})
|
||||
|
||||
it('requires distinct nonempty catalog admission', () => {
|
||||
const root = fixture()
|
||||
expect(() => generateOfficialBundleCatalog(root, [])).toThrow('distinct package names')
|
||||
expect(() => generateOfficialBundleCatalog(root, [ON_DEMAND_BUNDLES[0]!, ON_DEMAND_BUNDLES[0]!])).toThrow('distinct package names')
|
||||
})
|
||||
|
||||
it.each(['icon', 'en', 'zh'])('rejects missing %s metadata', (resource) => {
|
||||
const root = fixture()
|
||||
const dir = directory(ON_DEMAND_BUNDLES[0]!)
|
||||
rmSync(join(root, dir, resource === 'icon' ? 'icon.svg' : `locale/${resource}.json`))
|
||||
expect(() => generateOfficialBundleCatalog(root)).toThrow()
|
||||
})
|
||||
|
||||
it.each(['en', 'zh'])('requires explicit %s fields instead of metadata fallback text', (language) => {
|
||||
const root = fixture()
|
||||
write(root, `${directory(ON_DEMAND_BUNDLES[0]!)}/locale/${language}.json`, { meta: { description: 'Only a description.' } })
|
||||
expect(() => generateOfficialBundleCatalog(root)).toThrow(`explicit ${language} title and description`)
|
||||
})
|
||||
|
||||
it('rejects stale output when package-owned display text changes', () => {
|
||||
const root = fixture()
|
||||
rmSync(join(root, output))
|
||||
syncOfficialBundleCatalog(root, false)
|
||||
expect(() => { syncOfficialBundleCatalog(root, true) }).not.toThrow()
|
||||
write(root, `${directory(ON_DEMAND_BUNDLES[0]!)}/locale/en.json`, { meta: { title: 'Changed', description: 'Delegate a task.' } })
|
||||
const before = readFileSync(join(root, output), 'utf8')
|
||||
expect(() => { syncOfficialBundleCatalog(root, true) }).toThrow('is stale')
|
||||
expect(readFileSync(join(root, output), 'utf8')).toBe(before)
|
||||
syncOfficialBundleCatalog(root, false)
|
||||
expect(readFileSync(join(root, output), 'utf8')).toContain('Changed')
|
||||
})
|
||||
|
||||
it('does not rewrite an already current catalog during explicit generation', () => {
|
||||
const root = fixture()
|
||||
syncOfficialBundleCatalog(root, false)
|
||||
const path = join(root, output)
|
||||
const timestamp = new Date('2000-01-01T00:00:00Z')
|
||||
utimesSync(path, timestamp, timestamp)
|
||||
const before = statSync(path).mtimeMs
|
||||
syncOfficialBundleCatalog(root, false)
|
||||
expect(statSync(path).mtimeMs).toBe(before)
|
||||
expect(() => { syncOfficialBundleCatalog(root, true) }).not.toThrow()
|
||||
})
|
||||
|
||||
it('rejects a missing generated catalog without recreating it during freshness checking', () => {
|
||||
const root = fixture()
|
||||
rmSync(join(root, output))
|
||||
expect(() => { syncOfficialBundleCatalog(root, true) }).toThrow('is stale')
|
||||
expect(existsSync(join(root, output))).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,127 @@
|
||||
/** Embed package-owned display metadata for Official bundles whose providers install on demand. */
|
||||
|
||||
import { globSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import ts from 'typescript'
|
||||
import type { LocalizedText } from '../packages/util/package-manifest/src/types.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../packages/boot/app-boot/src/official-bundle-packages.ts'
|
||||
import type { OfficialBundleCatalogEntry } from '../packages/boot/app-boot/src/official-bundles.ts'
|
||||
import { readPluginMeta, resolvePluginResource } from '../packages/boot/app-boot/src/package-meta.ts'
|
||||
|
||||
const OUTPUT = 'packages/boot/app-boot/src/official-bundles.generated.ts'
|
||||
|
||||
/**
|
||||
* Read complete metadata without importing or activating any provider code.
|
||||
* @param root - Repository root containing the catalog's public packages.
|
||||
* @param names - Catalog admission list; defaults to the installation-owned list.
|
||||
* @returns Offline entries in admission order, with metadata from the owning packages.
|
||||
*/
|
||||
export function generateOfficialBundleCatalog(root: string, names: readonly string[] = ON_DEMAND_BUNDLES): OfficialBundleCatalogEntry[] {
|
||||
if (names.length === 0 || new Set(names).size !== names.length) throw new Error('Official on-demand catalog must contain distinct package names')
|
||||
const manifests = new Map<string, { path: string; private: boolean; bundle: boolean; runtime: string[] }>()
|
||||
for (const path of globSync(['packages/*/*/package.json', 'apps/*/package.json', 'vendor/*/package.json'], { cwd: root }).map(path => path.replaceAll('\\', '/')).sort()) {
|
||||
const manifest = JSON.parse(readFileSync(resolve(root, path), 'utf8')) as {
|
||||
name: string
|
||||
private?: boolean
|
||||
dependencies?: Record<string, string>
|
||||
optionalDependencies?: Record<string, string>
|
||||
peerDependencies?: Record<string, string>
|
||||
dsh?: { bundle?: { patch?: unknown } }
|
||||
}
|
||||
if (manifests.has(manifest.name)) throw new Error(`duplicate workspace package ${manifest.name}`)
|
||||
const runtime = ['dependencies', 'optionalDependencies', 'peerDependencies'] as const
|
||||
manifests.set(manifest.name, { path, private: manifest.private === true, bundle: manifest.dsh?.bundle?.patch !== undefined,
|
||||
runtime: runtime.flatMap(section => Object.keys(manifest[section] ?? {})) })
|
||||
}
|
||||
const visited = new Set<string>()
|
||||
const checkInstallation = (name: string): void => {
|
||||
if (visited.has(name)) return
|
||||
visited.add(name)
|
||||
if (names.includes(name)) throw new Error(`${name}: on-demand providers must not be installed by the DSH distribution`)
|
||||
for (const dependency of manifests.get(name)?.runtime ?? []) checkInstallation(dependency)
|
||||
}
|
||||
if (!manifests.has('@deepseek-ai/dsh')) throw new Error('Official catalog requires the DSH installation manifest')
|
||||
checkInstallation('@deepseek-ai/dsh')
|
||||
return names.map((packageName) => {
|
||||
const entry = manifests.get(packageName)
|
||||
if (entry === undefined || !entry.path.startsWith('packages/') || !packageName.startsWith('@deepseek-ai/dsh-') || entry.private || !entry.bundle) {
|
||||
throw new Error(`${packageName}: Official catalog entries must be public DSH bundle packages`)
|
||||
}
|
||||
const parentURL = pathToFileURL(resolve(root, entry.path)).href
|
||||
for (const language of ['en', 'zh']) {
|
||||
const file = resolvePluginResource(`${packageName}/locale/${language}.json`, parentURL)
|
||||
const dictionary = JSON.parse(readFileSync(file, 'utf8')) as { meta?: { title?: unknown; description?: unknown } }
|
||||
if (typeof dictionary.meta?.title !== 'string' || dictionary.meta.title.trim() === ''
|
||||
|| typeof dictionary.meta.description !== 'string' || dictionary.meta.description.trim() === '') {
|
||||
throw new Error(`${packageName}: Official metadata requires explicit ${language} title and description`)
|
||||
}
|
||||
}
|
||||
const meta = readPluginMeta(packageName, parentURL)
|
||||
if (meta?.error !== undefined) throw new Error(`${packageName}: ${meta.error}`)
|
||||
if (meta?.icon === undefined || typeof meta.title !== 'object' || typeof meta.description !== 'object') {
|
||||
throw new Error(`${packageName}: Official catalog entries require an icon and localized title and description`)
|
||||
}
|
||||
for (const value of [meta.title, meta.description]) {
|
||||
for (const language of ['en', 'zh']) {
|
||||
if (typeof value[language] !== 'string' || value[language].trim() === '') {
|
||||
throw new Error(`${packageName}: Official metadata requires ${language} text`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return { packageName, meta: { title: meta.title, description: meta.description, icon: meta.icon } }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a source-plane catalog with no package imports or pinned runtime version.
|
||||
* @param entries - Validated metadata in catalog order.
|
||||
* @returns Deterministic TypeScript source for app-boot's generated module.
|
||||
*/
|
||||
export function renderOfficialBundleCatalog(entries: readonly OfficialBundleCatalogEntry[]): string {
|
||||
return '/** Generated offline Official bundle metadata. Run pnpm gen-official-bundle-catalog; do not edit. */\n\n'
|
||||
+ "import type { OfficialBundleCatalogEntry } from './official-bundles.ts'\n\n"
|
||||
+ '/** Official on-demand entries, independent of installed packages and registry availability. */\n'
|
||||
+ 'export const OFFICIAL_ON_DEMAND_CATALOG: readonly OfficialBundleCatalogEntry[] = [\n'
|
||||
+ entries.map(entry => ' {\n'
|
||||
+ ` packageName: ${quote(entry.packageName)},\n`
|
||||
+ ' meta: {\n'
|
||||
+ ` title: ${localized(entry.meta.title)},\n`
|
||||
+ ` description: ${localized(entry.meta.description)},\n`
|
||||
+ ` icon: ${quote(entry.meta.icon)},\n`
|
||||
+ ' },\n },\n').join('')
|
||||
+ ']\n'
|
||||
}
|
||||
|
||||
const printer = ts.createPrinter()
|
||||
const sourceFile = ts.createSourceFile('catalog.ts', '', ts.ScriptTarget.Latest)
|
||||
|
||||
function quote(value: string): string {
|
||||
return printer.printNode(ts.EmitHint.Expression, ts.factory.createStringLiteral(value, true), sourceFile)
|
||||
}
|
||||
|
||||
function localized(value: LocalizedText): string {
|
||||
if (typeof value === 'string') return quote(value)
|
||||
return `{ ${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([language, text]) => `${quote(language)}: ${quote(text)}`).join(', ')} }`
|
||||
}
|
||||
|
||||
/**
|
||||
* Write changed metadata or freshness-check it against the owning package resources.
|
||||
* @param root - Repository root containing source and metadata resources.
|
||||
* @param check - Reject drift without changing the generated file.
|
||||
*/
|
||||
export function syncOfficialBundleCatalog(root: string, check: boolean): void {
|
||||
const source = renderOfficialBundleCatalog(generateOfficialBundleCatalog(root))
|
||||
const path = resolve(root, OUTPUT)
|
||||
let current: string | undefined
|
||||
try { current = readFileSync(path, 'utf8') }
|
||||
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error }
|
||||
if (current === source) return
|
||||
if (check) throw new Error(`${OUTPUT} is stale; run pnpm gen-official-bundle-catalog`)
|
||||
writeFileSync(path, source)
|
||||
}
|
||||
|
||||
if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
syncOfficialBundleCatalog(resolve(import.meta.dirname, '..'), process.argv.includes('--check'))
|
||||
console.log('Official on-demand catalog matches package-owned metadata.')
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
* the owning event-envelope types. This is the durable-record vocabulary, not
|
||||
* the live Cordis bus. Event declarations must be unique, explicitly typed,
|
||||
* documented, inheritance-free, and free of Cordis-only `@mode` tags; every
|
||||
* surface-union member must resolve to one. `--check` verifies the artifact.
|
||||
* surface-union member must resolve to one. Experimental plugin records have
|
||||
* a separate current-source catalog. `--check` verifies every generated artifact.
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
import { extractPersistenceSchema } from './persistence-schema.ts'
|
||||
import { persistenceCatalogText, type PersistenceCatalogLocale } from './persistence-catalog-text.ts'
|
||||
import { renderPersistencePair, type PersistenceArtifact } from './persistence-artifacts.ts'
|
||||
import { collectPluginRecords, type PluginRecordEntry } from './plugin-record-catalog.ts'
|
||||
|
||||
export { annotateSurface, collectEventEnvelopeTypes, collectLogEvents, collectSurfaceEventTypes } from './persistence-catalog-source.ts'
|
||||
export type { AnnotatedLogEventEntry, EventEnvelopeTypeEntry, LogEventEntry } from './persistence-catalog-source.ts'
|
||||
@@ -24,6 +26,7 @@ import { renderPersistenceSchemaDefinitions, renderPersistenceSchemaIndex } from
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const OUT = 'docs/persistence-catalog.md'
|
||||
const OUT_PLUGIN_RECORDS = 'docs/experimental-persistence-catalog.md'
|
||||
const OUT_RUNTIME_TYPES = 'packages/core/session/src/known-event-types.ts'
|
||||
const OUT_SCHEMA = 'docs/persistence-schema.json'
|
||||
|
||||
@@ -81,6 +84,46 @@ function renderEvent(e: AnnotatedLogEventEntry, locale: PersistenceCatalogLocale
|
||||
return out
|
||||
}
|
||||
|
||||
/** Escape a source annotation as inline table code, including template literals and unions. */
|
||||
function recordCode(value: string): string {
|
||||
return `<code>${value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')
|
||||
.replaceAll('|', '|').replaceAll('`', '`').replace(/\r?\n/gu, '<br>')}</code>`
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the standalone current-source plugin record catalog without expanding payload types.
|
||||
* @param records - validated experimental package record declarations.
|
||||
* @param locale - language of the page's headings and explanatory prose.
|
||||
* @returns the complete Markdown page, including its language switcher and main catalog link.
|
||||
*/
|
||||
export function renderPluginRecordCatalog(records: readonly PluginRecordEntry[], locale: PersistenceCatalogLocale = 'en'): string {
|
||||
const text = persistenceCatalogText[locale]
|
||||
const lines = [
|
||||
'<!-- Generated by scripts/gen-persistence-catalog.ts — do not edit by hand.',
|
||||
' Run `pnpm run gen-persistence-catalog` to regenerate. -->',
|
||||
'',
|
||||
`# ${text.pluginRecords}`,
|
||||
'',
|
||||
locale === 'en'
|
||||
? 'English | [中文](experimental-persistence-catalog.zh.md)'
|
||||
: '[English](experimental-persistence-catalog.md) | 中文',
|
||||
'',
|
||||
text.pluginRecordsIntro,
|
||||
'',
|
||||
text.pluginRecordsGeneration,
|
||||
'',
|
||||
text.pluginRecordsMainLink,
|
||||
'',
|
||||
]
|
||||
if (records.length === 0) return [...lines, text.pluginRecordsEmpty, ''].join('\n')
|
||||
lines.push(text.pluginRecordColumns, '|---|---|---|---|')
|
||||
for (const record of [...records].sort((left, right) => left.owner.localeCompare(right.owner) || left.name.localeCompare(right.name))) {
|
||||
const doc = record.doc.replaceAll('|', '\\|').replace(/\s*\n\s*/gu, ' ')
|
||||
lines.push(`| \`${record.name}\` | \`${record.owner}\` | ${doc} | ${recordCode(record.payload)} · [\`${record.source}\`](../${record.source}) |`)
|
||||
}
|
||||
return [...lines, ''].join('\n')
|
||||
}
|
||||
|
||||
/** Render the full catalog (pure, deterministic given the collected inputs). */
|
||||
export function render(
|
||||
events: AnnotatedLogEventEntry[],
|
||||
@@ -98,6 +141,8 @@ export function render(
|
||||
...(locale === 'zh' ? ['[English](persistence-catalog.md) | 中文', ''] : []),
|
||||
text.intro,
|
||||
'',
|
||||
text.pluginRecordsLink,
|
||||
'',
|
||||
text.generation,
|
||||
'',
|
||||
text.envelopeIntro,
|
||||
@@ -170,13 +215,15 @@ export function renderKnownEventTypes(events: AnnotatedLogEventEntry[]): string
|
||||
* Render the complete generated persistence reference from one extracted inventory.
|
||||
* @param scanRoot - checkout whose source event declarations supply JSDoc.
|
||||
* @param schema - already extracted source schemas; shared with record validation.
|
||||
* @returns both catalog languages, pairing metadata, runtime names and machine schemas.
|
||||
* @returns both catalog pairs and their metadata, runtime names and machine schemas.
|
||||
*/
|
||||
export function persistenceCatalogArtifacts(scanRoot: string, schema: PersistenceSchemaInventory): PersistenceArtifact[] {
|
||||
const events = annotateSurface(collectLogEvents(scanRoot), collectSurfaceEventTypes(scanRoot))
|
||||
const envelope = collectEventEnvelopeTypes(scanRoot)
|
||||
const pluginRecords = collectPluginRecords(scanRoot)
|
||||
return [
|
||||
...renderPersistencePair(scanRoot, OUT, render(events, envelope, schema), render(events, envelope, schema, 'zh')),
|
||||
...renderPersistencePair(scanRoot, OUT_PLUGIN_RECORDS, renderPluginRecordCatalog(pluginRecords), renderPluginRecordCatalog(pluginRecords, 'zh')),
|
||||
{ path: OUT_RUNTIME_TYPES, content: renderKnownEventTypes(events) },
|
||||
{ path: OUT_SCHEMA, content: `${JSON.stringify(persistenceSchemaSnapshot(schema), null, 2)}\n` },
|
||||
]
|
||||
|
||||
@@ -1,399 +0,0 @@
|
||||
/**
|
||||
* Generate dsh-scope's invariant resolver map from the repository TypeScript
|
||||
* Program.
|
||||
*
|
||||
* A scoped event declares `this: Scoped<Base>`. Real `scopeTarget(base, key)`
|
||||
* calls establish the routing-key type for that base. The generator searches
|
||||
* every event payload parameter and one property level for exactly one type
|
||||
* equivalent to that key. Each generated resolver compiles against the merged
|
||||
* `Events` parameter tuple. Zero matches require `@dshScopeScan unsupported`;
|
||||
* multiple matches are ambiguous and always fail loud.
|
||||
*
|
||||
* `pnpm run gen-scoped-events` -> write the generated source
|
||||
* `pnpm run verify-scoped-events` -> exit 1 when it is stale
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import ts from 'typescript'
|
||||
import { pointer, rawJsDoc } from './jsdoc.ts'
|
||||
import { TypeScriptProject } from './ts-project.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const OUT = 'packages/core/scope/src/scoped-events.generated.ts'
|
||||
const SCOPE_DOC_MARKER = 'Scope-filtered dispatch'
|
||||
|
||||
interface ScopeTargetContract {
|
||||
baseType: ts.Type
|
||||
keyType: ts.Type
|
||||
source: string
|
||||
}
|
||||
|
||||
interface SubjectCandidate {
|
||||
path: string
|
||||
parameter: number
|
||||
property?: string
|
||||
type: ts.Type
|
||||
}
|
||||
|
||||
interface ScopedEventResolver {
|
||||
event: string
|
||||
candidate: SubjectCandidate | null
|
||||
}
|
||||
|
||||
interface ScopeTag {
|
||||
present: boolean
|
||||
unsupported: boolean
|
||||
}
|
||||
|
||||
/** Program-backed analyzer and renderer for the generated scoped-event resolvers. */
|
||||
class ScopedEventGenerator {
|
||||
private readonly checker: ts.TypeChecker
|
||||
private readonly packageSources: ts.SourceFile[]
|
||||
private readonly scopeTargetDeclaration: ts.FunctionDeclaration
|
||||
private readonly scopedSymbol: ts.Symbol
|
||||
private readonly violations: string[] = []
|
||||
|
||||
constructor(private readonly project: TypeScriptProject) {
|
||||
this.checker = project.checker
|
||||
this.packageSources = project.sourceFiles().filter((sourceFile) => {
|
||||
return /^packages\/[^/]+\/[^/]+\/src\/.+\.ts$/.test(project.relativePath(sourceFile))
|
||||
})
|
||||
this.scopeTargetDeclaration = this.functionDeclaration(
|
||||
'packages/core/scope/src/index.ts',
|
||||
'scopeTarget',
|
||||
)
|
||||
this.scopedSymbol = this.typeAliasSymbol(
|
||||
'packages/core/scope/src/index.ts',
|
||||
'Scoped',
|
||||
)
|
||||
}
|
||||
|
||||
/** Render the complete generated TypeScript module or throw every contract violation. */
|
||||
render(): string {
|
||||
const contracts = this.collectScopeTargetContracts()
|
||||
const resolvers = this.collectScopedEventResolvers(contracts)
|
||||
if (this.violations.length > 0) {
|
||||
throw new Error(
|
||||
`gen-scoped-events: ${this.violations.length} scoped-event contract violation(s):\n`
|
||||
+ this.violations.map(violation => ` - ${violation}`).join('\n'),
|
||||
)
|
||||
}
|
||||
return [
|
||||
'/**',
|
||||
' * Generated scoped-event routing-subject resolvers for dsh-scope invariants.',
|
||||
' * Do not edit by hand; run `pnpm run gen-scoped-events`.',
|
||||
' *',
|
||||
' * @module @deepseek-ai/dsh-scope/scoped-events.generated',
|
||||
' */',
|
||||
'',
|
||||
'type ScopedSubjectResolver = (args: readonly unknown[]) => unknown',
|
||||
'',
|
||||
'const scopedSubjectResolvers: Readonly<Record<string, ScopedSubjectResolver | null>> = Object.freeze({',
|
||||
...resolvers.map(({ event, candidate }) => {
|
||||
if (candidate === null) return ` '${event}': null,`
|
||||
const subject = candidate.property === undefined
|
||||
? `args[${candidate.parameter}]`
|
||||
: `(args[${candidate.parameter}] as Record<string, unknown>)[${quote(candidate.property)}]`
|
||||
return ` '${event}': args => ${subject},`
|
||||
}),
|
||||
'})',
|
||||
'',
|
||||
'/**',
|
||||
' * Resolve the routing key named by one scoped event payload. A null',
|
||||
' * resolver means the payload cannot expose its external routing key, so the',
|
||||
' * invariant checks carrier presence only.',
|
||||
' * @param event - runtime Cordis event name.',
|
||||
' * @returns the generated subject resolver, null for presence-only,',
|
||||
' * or undefined when the event is not scope-filtered.',
|
||||
' */',
|
||||
'export function scopedSubjectResolverFor(event: string): ScopedSubjectResolver | null | undefined {',
|
||||
' return scopedSubjectResolvers[event]',
|
||||
'}',
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/** Resolve one named function declaration from a known source file. */
|
||||
private functionDeclaration(relativePath: string, name: string): ts.FunctionDeclaration {
|
||||
const sourceFile = this.project.sourceFile(relativePath)
|
||||
const declaration = sourceFile.statements.find((statement): statement is ts.FunctionDeclaration => {
|
||||
return ts.isFunctionDeclaration(statement) && statement.name?.text === name
|
||||
})
|
||||
if (!declaration) throw new Error(`gen-scoped-events: cannot resolve function ${name} from ${relativePath}`)
|
||||
return declaration
|
||||
}
|
||||
|
||||
/** Resolve one named type-alias symbol from a known source file. */
|
||||
private typeAliasSymbol(relativePath: string, name: string): ts.Symbol {
|
||||
const sourceFile = this.project.sourceFile(relativePath)
|
||||
const declaration = sourceFile.statements.find((statement): statement is ts.TypeAliasDeclaration => {
|
||||
return ts.isTypeAliasDeclaration(statement) && statement.name.text === name
|
||||
})
|
||||
const symbol = declaration && this.checker.getSymbolAtLocation(declaration.name)
|
||||
if (!symbol) throw new Error(`gen-scoped-events: cannot resolve type ${name} from ${relativePath}`)
|
||||
return symbol
|
||||
}
|
||||
|
||||
/** Collect every real scopeTarget(base, key) base/key type contract. */
|
||||
private collectScopeTargetContracts(): ScopeTargetContract[] {
|
||||
const contracts: ScopeTargetContract[] = []
|
||||
const visit = (sourceFile: ts.SourceFile, node: ts.Node): void => {
|
||||
if (ts.isCallExpression(node)
|
||||
&& this.checker.getResolvedSignature(node)?.declaration === this.scopeTargetDeclaration) {
|
||||
const base = node.arguments[0]
|
||||
const key = node.arguments[1]
|
||||
if (!base || !key) {
|
||||
const source = pointer(this.project.relativePath(sourceFile), sourceFile, node)
|
||||
this.violations.push(`${source} calls scopeTarget without base and key arguments`)
|
||||
} else {
|
||||
contracts.push({
|
||||
baseType: this.checker.getTypeAtLocation(base),
|
||||
keyType: this.checker.getTypeAtLocation(key),
|
||||
source: pointer(this.project.relativePath(sourceFile), sourceFile, node),
|
||||
})
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, (child) => { visit(sourceFile, child) })
|
||||
}
|
||||
for (const sourceFile of this.packageSources) visit(sourceFile, sourceFile)
|
||||
return contracts
|
||||
}
|
||||
|
||||
/** Collect every Events member and derive its generated resolver. */
|
||||
private collectScopedEventResolvers(contracts: readonly ScopeTargetContract[]): ScopedEventResolver[] {
|
||||
const resolvers: ScopedEventResolver[] = []
|
||||
for (const sourceFile of this.packageSources) {
|
||||
const rel = this.project.relativePath(sourceFile)
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isInterfaceDeclaration(node) && node.name.text === 'Events' && isCordisModuleInterface(node)) {
|
||||
for (const member of node.members) {
|
||||
if (!ts.isMethodSignature(member) || !ts.isStringLiteral(member.name)) continue
|
||||
const event = member.name.text
|
||||
const raw = rawJsDoc(sourceFile.text, member)
|
||||
const where = `event '${event}' (${pointer(rel, sourceFile, member)})`
|
||||
const tag = parseScopeTag(raw, where, this.violations)
|
||||
const thisParameter = member.parameters.find(isThisParameter)
|
||||
const scopedBase = thisParameter && this.scopedBaseType(thisParameter)
|
||||
if (!scopedBase) {
|
||||
if (raw.includes(SCOPE_DOC_MARKER)) {
|
||||
this.violations.push(
|
||||
`${where} documents scope-filtered dispatch but its signature has no this: Scoped<...> receiver`,
|
||||
)
|
||||
}
|
||||
if (tag.present) {
|
||||
this.violations.push(`${where} has @dshScopeScan metadata but is not a Scoped event`)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (!raw.includes(SCOPE_DOC_MARKER)) {
|
||||
this.violations.push(
|
||||
`${where} has this: Scoped<...> but its JSDoc does not explain "${SCOPE_DOC_MARKER}"`,
|
||||
)
|
||||
}
|
||||
const keyType = this.routingKeyType(where, scopedBase, contracts)
|
||||
if (!keyType) continue
|
||||
const candidates = this.subjectCandidates(member)
|
||||
.filter(candidate => this.typesEquivalent(candidate.type, keyType))
|
||||
if (candidates.length > 1) {
|
||||
this.violations.push(
|
||||
`${where} has multiple routing-key candidates for ${this.typeText(keyType)}: `
|
||||
+ candidates.map(candidate => `${candidate.path}: ${this.typeText(candidate.type)}`).join(', '),
|
||||
)
|
||||
continue
|
||||
}
|
||||
if (candidates.length === 0) {
|
||||
if (!tag.unsupported) {
|
||||
const keyLabel = this.typeText(keyType)
|
||||
this.violations.push(
|
||||
`${where} exposes no parameter or one-level property equivalent to routing key type ${keyLabel}; `
|
||||
+ 'add @dshScopeScan unsupported only when the key is intentionally absent from the payload',
|
||||
)
|
||||
}
|
||||
resolvers.push({ event, candidate: null })
|
||||
continue
|
||||
}
|
||||
if (tag.unsupported) {
|
||||
this.violations.push(
|
||||
`${where} has unnecessary @dshScopeScan unsupported; ${candidates[0]?.path} exposes the routing key`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
resolvers.push({ event, candidate: candidates[0] ?? null })
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
visit(sourceFile)
|
||||
}
|
||||
return resolvers.sort((left, right) => left.event.localeCompare(right.event))
|
||||
}
|
||||
|
||||
/** Extract the Base type from one exact this: Scoped<Base> parameter. */
|
||||
private scopedBaseType(parameter: ts.ParameterDeclaration): ts.Type | undefined {
|
||||
const type = this.checker.getTypeAtLocation(parameter)
|
||||
if (type.aliasSymbol !== this.scopedSymbol) return undefined
|
||||
return type.aliasTypeArguments?.[0]
|
||||
}
|
||||
|
||||
/** Resolve one unambiguous key type for a scoped carrier base. */
|
||||
private routingKeyType(
|
||||
where: string,
|
||||
scopedBase: ts.Type,
|
||||
contracts: readonly ScopeTargetContract[],
|
||||
): ts.Type | undefined {
|
||||
const matches = contracts.filter((contract) => {
|
||||
return this.checker.isTypeAssignableTo(this.normalizedType(contract.baseType), this.normalizedType(scopedBase))
|
||||
})
|
||||
if (matches.length === 0) {
|
||||
this.violations.push(
|
||||
`${where} has no matching scopeTarget(base, key) call for carrier base ${this.typeText(scopedBase)}`,
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
const keyTypes: ts.Type[] = []
|
||||
for (const match of matches) {
|
||||
if (!keyTypes.some(type => this.typesEquivalent(type, match.keyType))) keyTypes.push(match.keyType)
|
||||
}
|
||||
if (keyTypes.length > 1) {
|
||||
this.violations.push(
|
||||
`${where} carrier base ${this.typeText(scopedBase)} has inconsistent routing-key types: `
|
||||
+ matches.map(match => `${this.typeText(match.keyType)} at ${match.source}`).join(', '),
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
return keyTypes[0]
|
||||
}
|
||||
|
||||
/** Enumerate every payload parameter and every accessible one-level property. */
|
||||
private subjectCandidates(member: ts.MethodSignature): SubjectCandidate[] {
|
||||
const candidates: SubjectCandidate[] = []
|
||||
let runtimeIndex = 0
|
||||
for (const parameter of member.parameters) {
|
||||
if (isThisParameter(parameter)) continue
|
||||
const directPath = `args[${runtimeIndex}]`
|
||||
const parameterType = this.checker.getTypeAtLocation(parameter)
|
||||
candidates.push({ path: directPath, parameter: runtimeIndex, type: parameterType })
|
||||
for (const property of this.checker.getPropertiesOfType(this.normalizedType(parameterType))) {
|
||||
const name = property.getName()
|
||||
if (name.startsWith('__@') || hasNonPublicDeclaration(property)) continue
|
||||
candidates.push({
|
||||
path: `${directPath}.${name}`,
|
||||
parameter: runtimeIndex,
|
||||
property: name,
|
||||
type: this.checker.getTypeOfSymbolAtLocation(property, parameter),
|
||||
})
|
||||
}
|
||||
runtimeIndex += 1
|
||||
}
|
||||
return dedupeCandidates(candidates)
|
||||
}
|
||||
|
||||
/** Compare exact Program type identities after removing null and undefined. */
|
||||
private typesEquivalent(left: ts.Type, right: ts.Type): boolean {
|
||||
const normalizedLeft = this.normalizedType(left)
|
||||
const normalizedRight = this.normalizedType(right)
|
||||
if (normalizedLeft.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return false
|
||||
if (normalizedRight.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return false
|
||||
return normalizedLeft === normalizedRight
|
||||
}
|
||||
|
||||
/** Remove null and undefined from a routing or candidate type. */
|
||||
private normalizedType(type: ts.Type): ts.Type {
|
||||
return this.checker.getNonNullableType(type)
|
||||
}
|
||||
|
||||
/** Render a stable diagnostic type label. */
|
||||
private typeText(type: ts.Type): string {
|
||||
return this.checker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation)
|
||||
}
|
||||
}
|
||||
|
||||
/** Return whether an Events interface is inside declare module '@deepseek-ai/cordis'. */
|
||||
function isCordisModuleInterface(node: ts.InterfaceDeclaration): boolean {
|
||||
const block = node.parent
|
||||
const declaration = block.parent
|
||||
return ts.isModuleBlock(block)
|
||||
&& ts.isModuleDeclaration(declaration)
|
||||
&& ts.isStringLiteral(declaration.name)
|
||||
&& declaration.name.text === '@deepseek-ai/cordis'
|
||||
}
|
||||
|
||||
/** Return whether a parameter is the explicit TypeScript this receiver. */
|
||||
function isThisParameter(parameter: ts.ParameterDeclaration): boolean {
|
||||
return ts.isIdentifier(parameter.name) && parameter.name.text === 'this'
|
||||
}
|
||||
|
||||
/** Parse and validate the optional @dshScopeScan unsupported tag. */
|
||||
function parseScopeTag(raw: string, where: string, violations: string[]): ScopeTag {
|
||||
const tags = raw
|
||||
.replace(/^\/\*\*/, '')
|
||||
.replace(/\*\/$/, '')
|
||||
.split('\n')
|
||||
.map(line => line.replace(/^\s*\*?\s?/, '').trim())
|
||||
.filter(line => line.startsWith('@dshScopeScan'))
|
||||
if (tags.length > 1) violations.push(`${where} has multiple @dshScopeScan tags`)
|
||||
if (tags.length === 0) return { present: false, unsupported: false }
|
||||
const unsupported = tags[0] === '@dshScopeScan unsupported'
|
||||
if (!unsupported) {
|
||||
violations.push(
|
||||
`${where} has invalid scoped-event scan metadata '${tags[0]}'; expected '@dshScopeScan unsupported'`,
|
||||
)
|
||||
}
|
||||
return { present: true, unsupported }
|
||||
}
|
||||
|
||||
/** Return whether a property has a private or protected declaration. */
|
||||
function hasNonPublicDeclaration(symbol: ts.Symbol): boolean {
|
||||
return (symbol.declarations ?? []).some((declaration) => {
|
||||
if (!ts.canHaveModifiers(declaration)) return false
|
||||
return ts.getModifiers(declaration)?.some((modifier) => {
|
||||
return modifier.kind === ts.SyntaxKind.PrivateKeyword || modifier.kind === ts.SyntaxKind.ProtectedKeyword
|
||||
}) ?? false
|
||||
})
|
||||
}
|
||||
|
||||
/** Deduplicate candidate paths contributed by merged/intersection types. */
|
||||
function dedupeCandidates(candidates: readonly SubjectCandidate[]): SubjectCandidate[] {
|
||||
const seen = new Set<string>()
|
||||
return candidates.filter((candidate) => {
|
||||
if (seen.has(candidate.path)) return false
|
||||
seen.add(candidate.path)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
/** Quote a generated property key as a single-quoted TypeScript string. */
|
||||
function quote(value: string): string {
|
||||
return `'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the generated scoped-event resolver module for one repository root.
|
||||
* @param projectRoot - repository root carrying tsconfig.host.json.
|
||||
* @returns complete generated TypeScript source.
|
||||
*/
|
||||
export function renderScopedEvents(projectRoot: string = root): string {
|
||||
return new ScopedEventGenerator(new TypeScriptProject(projectRoot)).render()
|
||||
}
|
||||
|
||||
/** Generate or freshness-check the fixed dsh-scope source file. */
|
||||
function main(): void {
|
||||
const content = renderScopedEvents()
|
||||
const output = resolve(root, OUT)
|
||||
if (process.argv.includes('--check')) {
|
||||
const committed = existsSync(output) ? readFileSync(output, 'utf8') : null
|
||||
if (committed === content) {
|
||||
console.log(`gen-scoped-events: ${OUT} is up to date.`)
|
||||
return
|
||||
}
|
||||
console.error(`gen-scoped-events: ${OUT} is stale. Run \`pnpm run gen-scoped-events\` and commit it.`)
|
||||
process.exit(1)
|
||||
}
|
||||
writeFileSync(output, content)
|
||||
console.log(`gen-scoped-events: wrote ${OUT}.`)
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) {
|
||||
main()
|
||||
}
|
||||
@@ -36,6 +36,8 @@ describe('THIRD_PARTY_NOTICES.md', () => {
|
||||
expect(generated).toContain(`| [\`numpy\`](https://github.com/numpy/numpy) | ${primaryRuntimeLock.pythonPackages.numpy} | BSD-3-Clause |`)
|
||||
expect(generated).toContain('## LibreOffice conversion kit')
|
||||
expect(generated).toContain('Recipients must have access to those corresponding sources and notices.')
|
||||
expect(generated.split('## Development-only npm dependencies')[0]).toContain('| [`chrome-devtools-frontend`]')
|
||||
expect(generated).toContain('third-party license and notice files supplied by the npm source')
|
||||
expect(readFileSync(resolve(root, 'THIRD_PARTY_NOTICES.md'), 'utf8'), 'stale notices — run `pnpm run gen-third-party-notices`').toBe(generated)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@ import { parse as parseToml, type TomlTableWithoutBigInt, type TomlValueWithoutB
|
||||
import parseSpdx from 'spdx-expression-parse'
|
||||
import primaryRuntimeLock from './primary-runtime/lock.json' with { type: 'json' }
|
||||
import { browserBundledExternals } from './browser-bundled-externals.ts'
|
||||
import { DEVTOOLS_NPM_VERSION, DEVTOOLS_SOURCE_REVISION } from '../packages/experimental/inspector/scripts/devtools/source.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const OUT = 'THIRD_PARTY_NOTICES.md'
|
||||
@@ -743,6 +744,8 @@ ${rows.join('\n')}
|
||||
*/
|
||||
export async function render(): Promise<string> {
|
||||
const browser = await browserBundledExternals(root)
|
||||
// The Inspector's Host build hook compiles this standalone frontend outside the Client bundle graph.
|
||||
browser.add('chrome-devtools-frontend')
|
||||
// The linked-manifest cache is keyed by name only, so it must not outlive
|
||||
// the manifests map it was resolved from; render() owns that single load.
|
||||
workspaceLinkedManifestCache.clear()
|
||||
@@ -793,13 +796,18 @@ ${renderNpmTable(runtimeDeps)}
|
||||
pnpm applies local patches to the following packages at install time, so shipped artifacts carry modified copies; each patch file is the complete record of the modification:
|
||||
|
||||
${patchedLines.join('\n')}
|
||||
|
||||
## Chrome DevTools frontend
|
||||
|
||||
The optional experimental Inspector distributes a locally compiled copy of [chrome-devtools-frontend ${DEVTOOLS_NPM_VERSION}](https://www.npmjs.com/package/chrome-devtools-frontend/v/${DEVTOOLS_NPM_VERSION}), from upstream revision [${DEVTOOLS_SOURCE_REVISION}](https://chromium.googlesource.com/devtools/devtools-frontend/+/${DEVTOOLS_SOURCE_REVISION}). The build includes the Chromium [BSD-3-Clause license](packages/experimental/inspector/assets/devtools/LICENSE) and the third-party license and notice files supplied by the npm source. The Chromium root license does not replace those dependencies' licenses.
|
||||
|
||||
${renderClaudeDistribution(claudeDistribution)}
|
||||
${kitRuntime ? `
|
||||
## LibreOffice conversion kit
|
||||
|
||||
${[...LIBREOFFICE_PACKAGES].map(name => `\`${name}\``).join(', ')} declare MPL-2.0, which remains outside the permissive-license allowlist; the notices check accepts only these package identities at those terms. The [distribution decision](.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md) records the source obligations.
|
||||
|
||||
The [kit repository](https://github.com/deepseek-harness/libreoffice-kit) supplies the corresponding LibreOffice source pin, modifications, build instructions, Node API, and artifact validation. Its engine packages retain their license and third-party notices; the Node API retains its MPL-2.0 declaration and NOTICE. Recipients must have access to those corresponding sources and notices.
|
||||
The [kit repository](https://github.com/deepseek-ai/dsh-libreoffice-kit) supplies the corresponding LibreOffice source pin, modifications, build instructions, Node API, and artifact validation. Its engine packages retain their license and third-party notices; the Node API retains its MPL-2.0 declaration and NOTICE. Recipients must have access to those corresponding sources and notices.
|
||||
` : ''}
|
||||
|
||||
## Development-only npm dependencies
|
||||
|
||||
+50
-24
@@ -25,6 +25,9 @@ import * as BashEnvPlugin from '@deepseek-ai/dsh-shell-env'
|
||||
import { PwshLocalExecutor } from '@deepseek-ai/dsh-pwsh-local'
|
||||
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
|
||||
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
|
||||
import WorkingDirectory from '@deepseek-ai/dsh-working-directory'
|
||||
import * as ToolWorkingDirectory from '@deepseek-ai/dsh-tool-working-directory'
|
||||
import * as ToolWorktree from '@deepseek-ai/dsh-experimental-tool-worktree'
|
||||
import { AttachmentStore } from '@deepseek-ai/dsh-attachment'
|
||||
import type { ImageAttachmentLimits, ImageAttachmentRef, SaveImageAttachment, StoredImageAttachment } from '@deepseek-ai/dsh-attachment'
|
||||
import UserQuestionService from '@deepseek-ai/dsh-user-questions'
|
||||
@@ -136,7 +139,7 @@ const catalogChildScopes = new WeakMap<Context, Agent>()
|
||||
|
||||
/**
|
||||
* Tool package plus its hand-maintained boot recipe. The caller mounts the
|
||||
* prompt and registry; each recipe supplies only package-specific seams and
|
||||
* prompt, directory services, and registry; each recipe supplies only package-specific seams and
|
||||
* config, while `dir` participates in the completeness check.
|
||||
*/
|
||||
export interface ToolPackage {
|
||||
@@ -241,7 +244,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
pkg: '@deepseek-ai/dsh-tools',
|
||||
dir: 'tools',
|
||||
source: 'packages/core/tools/src/ptc.ts',
|
||||
requires: ['ctx.tools', 'ctx.ptcRuntime (execution time)', 'ctx.systemPrompt'],
|
||||
requires: ['ctx.workingDirectory (Agent executions)', 'ctx.tools', 'ctx.ptcRuntime (execution time)', 'ctx.systemPrompt'],
|
||||
writes: ['tool/call', 'one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call', 'tool/result'],
|
||||
// The registry's OWN tool: run_code exists only under a non-native mode
|
||||
// (the registry registers it in its constructor; the PTC runtime is read
|
||||
@@ -249,7 +252,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
toolsConfig: { mode: 'ptc' },
|
||||
async mount() {},
|
||||
note:
|
||||
'Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: ptc` / `mode: both` (see the PTC mode Agent Note). Under `ptc` it is the registry\'s only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime\'s language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.',
|
||||
'Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: ptc` (see the PTC mode Agent Note). Under `ptc` it is the registry\'s only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime\'s language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-plan-mode',
|
||||
@@ -267,7 +270,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
pkg: '@deepseek-ai/dsh-tool-bash',
|
||||
dir: 'tool-bash',
|
||||
source: 'packages/shell/tool-bash/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.shell', 'ctx.systemPrompt', 'ctx.shellEnv', 'ctx.jobs for run_in_background and the job-backed foreground path'],
|
||||
requires: ['ctx.workingDirectory', 'ctx.tools', 'ctx.shell', 'ctx.systemPrompt', 'ctx.shellEnv', 'ctx.jobs for run_in_background and the job-backed foreground path'],
|
||||
writes: ['tool/call', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(LocalSubprocessRuntime)
|
||||
@@ -285,10 +288,9 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
pkg: '@deepseek-ai/dsh-tool-present',
|
||||
dir: 'tool-present',
|
||||
source: 'packages/deliverables/tool-present/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.fs', 'ctx.sessionProjections'],
|
||||
requires: ['ctx.workingDirectory', 'ctx.tools', 'ctx.fs', 'ctx.sessionProjections'],
|
||||
writes: ['tool/call', 'deliverables/presented after a successful final result', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(LocalFileSystem)
|
||||
await ctx.plugin(ToolPresent)
|
||||
},
|
||||
note: 'Deliveries belong to the calling Session; Web ui-deliverables supplies source-file opening and cards.',
|
||||
@@ -297,7 +299,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
pkg: '@deepseek-ai/dsh-tool-pwsh',
|
||||
dir: 'tool-pwsh',
|
||||
source: 'packages/shell/tool-pwsh/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.shell', 'ctx.systemPrompt', 'ctx.shellEnv', 'ctx.jobs for run_in_background and the job-backed foreground path'],
|
||||
requires: ['ctx.workingDirectory', 'ctx.tools', 'ctx.shell', 'ctx.systemPrompt', 'ctx.shellEnv', 'ctx.jobs for run_in_background and the job-backed foreground path'],
|
||||
writes: ['tool/call', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
// The pwsh tool consumes the bash executor seam; the schema harvest
|
||||
@@ -330,7 +332,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
pkg: '@deepseek-ai/dsh-tool-bash-persistent',
|
||||
dir: 'tool-bash-persistent',
|
||||
source: 'packages/shell/tool-bash-persistent/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.terminals', 'an owning Agent at execution time'],
|
||||
requires: ['ctx.workingDirectory', 'ctx.tools', 'ctx.terminals', 'an owning Agent at execution time'],
|
||||
writes: ['tool/call', 'PTY shell state', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(TerminalSessionService)
|
||||
@@ -343,7 +345,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
pkg: '@deepseek-ai/dsh-tool-pwsh-persistent',
|
||||
dir: 'tool-pwsh-persistent',
|
||||
source: 'packages/shell/tool-pwsh-persistent/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.terminals', 'an owning Agent at execution time'],
|
||||
requires: ['ctx.workingDirectory', 'ctx.tools', 'ctx.terminals', 'an owning Agent at execution time'],
|
||||
writes: ['tool/call', 'PTY shell state', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(TerminalSessionService)
|
||||
@@ -359,7 +361,6 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
requires: ['ctx.tools', 'ctx.fs'],
|
||||
writes: ['tool/call', 'fs/observed after view presence/absence, edit absence, or successful mutation', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(LocalFileSystem)
|
||||
await ctx.plugin(ToolStrReplaceEditor)
|
||||
},
|
||||
note:
|
||||
@@ -369,13 +370,11 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
pkg: '@deepseek-ai/dsh-tool-fs',
|
||||
dir: 'tool-fs',
|
||||
source: 'packages/fs/tool-fs/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.fs', 'ctx.systemPrompt', 'ctx.attachments (image-tool registration)', 'ctx.llm + an image-capable route (image-tool execution)'],
|
||||
requires: ['ctx.workingDirectory', 'ctx.tools', 'ctx.fs', 'ctx.systemPrompt', 'ctx.attachments (image-tool registration)', 'ctx.llm + an image-capable route (image-tool execution)'],
|
||||
writes: ['tool/call', 'fs/write-intent or fs/edit-intent for mutations', 'fs/observed after read presence/absence or successful file operation', 'durable attachment (read_image)', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
// The tool needs `fs`; the bare provider is sufficient because policy
|
||||
// changes behavior, not schema shape. The catalog seam marker opts into
|
||||
// The catalog seam marker opts into
|
||||
// the attachments-conditional image schema without attachment I/O.
|
||||
await ctx.plugin(LocalFileSystem)
|
||||
await ctx.plugin(CatalogAttachmentStore)
|
||||
await ctx.plugin(ToolFs)
|
||||
},
|
||||
@@ -386,7 +385,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
pkg: '@deepseek-ai/dsh-tool-fs-search',
|
||||
dir: 'tool-fs-search',
|
||||
source: 'packages/fs/tool-fs-search/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.subprocess', 'ctx.systemPrompt'],
|
||||
requires: ['ctx.workingDirectory', 'ctx.tools', 'ctx.subprocess', 'ctx.systemPrompt'],
|
||||
writes: ['tool/call', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
// The tools inject `subprocess` (search spawns the packaged ripgrep
|
||||
@@ -403,8 +402,8 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-terminal',
|
||||
dir: 'tool-terminal',
|
||||
source: 'packages/terminal/tool-terminal/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.terminals', 'ctx.systemPrompt', 'ctx.jobs at call time for run_in_background'],
|
||||
source: 'packages/experimental/tool-terminal/src/index.ts',
|
||||
requires: ['ctx.workingDirectory', 'ctx.tools', 'ctx.terminals', 'ctx.systemPrompt', 'ctx.jobs at call time for run_in_background'],
|
||||
writes: ['tool/call', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(TerminalSessionService)
|
||||
@@ -450,7 +449,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
pkg: '@deepseek-ai/dsh-tool-lsp',
|
||||
dir: 'tool-lsp',
|
||||
source: 'packages/lsp/tool-lsp/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.lsp', 'ctx.systemPrompt'],
|
||||
requires: ['ctx.workingDirectory', 'ctx.tools', 'ctx.lsp', 'ctx.systemPrompt'],
|
||||
writes: ['tool/call', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
// The tool registers from the seam alone; the schema does not depend on any provider.
|
||||
@@ -463,7 +462,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-ralph',
|
||||
dir: 'tool-ralph',
|
||||
source: 'packages/workflow/tool-ralph/src/index.ts',
|
||||
source: 'packages/experimental/tool-ralph/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.workflowEngine', 'ctx.subagents', 'ctx.systemPrompt', 'a calling Agent (exec.agent parents every fresh round)'],
|
||||
writes: ['tool/call', 'tool/result', 'workflow and child session events during execution'],
|
||||
async mount(ctx) {
|
||||
@@ -479,7 +478,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
pkg: '@deepseek-ai/dsh-tool-skill',
|
||||
dir: 'tool-skill',
|
||||
source: 'packages/skill/tool-skill/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.agents', 'ctx.skills'],
|
||||
requires: ['ctx.workingDirectory', 'ctx.tools', 'ctx.agents', 'ctx.skills'],
|
||||
writes: ['tool/call', 'tool/result', 'user/message replacement catalogs via agent.inject()'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(AgentRegistry)
|
||||
@@ -494,7 +493,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-session-query',
|
||||
dir: 'tool-session-query',
|
||||
source: 'packages/session-query/tool-session-query/src/index.ts',
|
||||
source: 'packages/experimental/tool-session-query/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.systemPrompt', 'ctx.sessionQuery', 'a calling Agent for workspace authority'],
|
||||
writes: ['tool/call', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
@@ -523,7 +522,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
registerListSubagentModels(ctx, { routes: [{ provider: 'mock', model: 'mock' }] })
|
||||
},
|
||||
note:
|
||||
'The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion available in an enabled Session. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Each instance independently controls whether it reads model-selection settings and its background behavior through `modelSelectionSettings`, `backgroundMode`, and `enableRunInBackground`.',
|
||||
'The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion available in an enabled Session. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-subagent-control',
|
||||
@@ -544,7 +543,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
await ctx.plugin(ToolSubagentListAgents)
|
||||
},
|
||||
note:
|
||||
'The globally named control tools over continuable background subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries).',
|
||||
'The globally named discovery, interruption, and local-message tools over managed subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries).',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-jobs',
|
||||
@@ -557,7 +556,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
await ctx.plugin(ToolJobs)
|
||||
},
|
||||
note:
|
||||
'The kind-agnostic background-job controller: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers\' `ctx.jobs.start()`.',
|
||||
'The kind-agnostic background-job controller: background bash commands and PTY sends are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers\' `ctx.jobs.start()`.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-experimental-tool-agent-team',
|
||||
@@ -609,6 +608,31 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
note:
|
||||
'todo_write is session-owned state; UIs render the latest todo/write event as a checklist. `allowParallelInProgress` is required with no default, so the catalog states its choice: `true`, whose description invites several `in_progress` items. A deployment choosing `false` receives the same tool with a description asking for exactly one active task.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-working-directory',
|
||||
dir: 'tool-working-directory',
|
||||
source: 'packages/session/tool-working-directory/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.workingDirectory'],
|
||||
writes: ['tool/call', 'working-directory/change', 'user context for directory changes', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(ToolWorkingDirectory)
|
||||
},
|
||||
note: 'Reads or changes the calling Session directory. Existing shells and processes retain their own directories; origin metadata and permission roots stay fixed.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-experimental-tool-worktree',
|
||||
dir: 'tool-worktree',
|
||||
source: 'packages/experimental/tool-worktree/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.worktrees', 'a calling Agent'],
|
||||
writes: ['tool/call', 'Git branch and checkout', 'working-directory/change', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
ctx.provide('worktrees', {
|
||||
create: () => Promise.reject(new Error('gen-tool-catalog: worktree creation is unavailable during schema harvest')),
|
||||
})
|
||||
await ctx.plugin(ToolWorktree)
|
||||
},
|
||||
note: 'Explicit experimental composition only. Creates a new branch and checkout from a pinned local commit under existing write permissions, then changes the Session directory. Leaving retains the branch and checkout.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-workflow',
|
||||
dir: 'tool-workflow',
|
||||
@@ -729,6 +753,8 @@ export async function collectToolCatalog(packages: ToolPackage[] = TOOL_PACKAGES
|
||||
try {
|
||||
await ctx.plugin(SessionProjectionRegistry)
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(LocalFileSystem)
|
||||
await ctx.plugin(WorkingDirectory)
|
||||
await ctx.plugin(ToolRuntime, entry.toolsConfig ?? {})
|
||||
await entry.mount(ctx)
|
||||
const schemas = ctx.tools.schemas(entry.scope?.(ctx)).sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
@@ -20,7 +20,6 @@ describe('generated tsconfig package aliases', () => {
|
||||
expect(session).toEqual({
|
||||
specifier: '@deepseek-ai/dsh-session',
|
||||
source: './packages/core/session/src',
|
||||
hasInvariant: true,
|
||||
})
|
||||
// Sorted, so a package added anywhere lands in a stable spot in the diff.
|
||||
expect([...aliases].sort((a, b) => a.specifier.localeCompare(b.specifier))).toEqual(aliases)
|
||||
@@ -31,15 +30,15 @@ describe('generated tsconfig package aliases', () => {
|
||||
|
||||
it('yields to a hand-written alias and closes without a trailing comma', () => {
|
||||
const aliases = [
|
||||
{ specifier: '@deepseek-ai/dsh-a', source: './packages/g/a/src', hasInvariant: true },
|
||||
{ specifier: '@deepseek-ai/dsh-b', source: './packages/g/b/src', hasInvariant: false },
|
||||
{ specifier: '@deepseek-ai/dsh-a', source: './packages/g/a/src' },
|
||||
{ specifier: '@deepseek-ai/dsh-b', source: './packages/g/b/src' },
|
||||
{ specifier: '@deepseek-ai/dsh-c', source: './packages/g/c/src' },
|
||||
]
|
||||
const body = renderAliases(aliases, new Set(['@deepseek-ai/dsh-a']))
|
||||
|
||||
// The hand-written bare alias is skipped; its /invariant sibling is not.
|
||||
expect(body).toBe([
|
||||
' "@deepseek-ai/dsh-a/invariant": ["./packages/g/a/src/invariant.ts"]',
|
||||
' "@deepseek-ai/dsh-b": ["./packages/g/b/src"]',
|
||||
' "@deepseek-ai/dsh-c": ["./packages/g/c/src"]',
|
||||
].join(',\n'))
|
||||
expect(body.endsWith(',')).toBe(false)
|
||||
})
|
||||
@@ -72,7 +71,7 @@ describe('generated tsconfig package aliases', () => {
|
||||
// resolve through the workspace symlink to built lib/types instead.
|
||||
expect(uncoveredPackages(
|
||||
['@deepseek-ai/dsh-a', '@deepseek-ai/dsh-b'],
|
||||
new Set(['@deepseek-ai/dsh-a', '@deepseek-ai/dsh-a/invariant']),
|
||||
new Set(['@deepseek-ai/dsh-a']),
|
||||
)).toEqual(['@deepseek-ai/dsh-b'])
|
||||
|
||||
expect(uncoveredPackages(['@deepseek-ai/dsh-a'], new Set(['@deepseek-ai/dsh-a']))).toEqual([])
|
||||
@@ -89,10 +88,9 @@ describe('generated tsconfig package aliases', () => {
|
||||
|
||||
it('leaves no wildcard that probes every package group', () => {
|
||||
const config = readFileSync(resolve(root, 'tsconfig.base.json'), 'utf8')
|
||||
// These two listed one candidate per group, so resolving a package late in
|
||||
// the list cost a filesystem probe — and under tsx a decorated module
|
||||
// A wildcard lists one candidate per group, so resolving a package late in
|
||||
// the list costs a filesystem probe — and under tsx a decorated module
|
||||
// error — for every group before it.
|
||||
expect(config).not.toContain('"@deepseek-ai/dsh-*":')
|
||||
expect(config).not.toContain('"@deepseek-ai/dsh-*/invariant":')
|
||||
})
|
||||
})
|
||||
@@ -2,10 +2,9 @@
|
||||
* Expand the workspace path aliases that a wildcard would otherwise resolve by
|
||||
* probing every package group in turn.
|
||||
*
|
||||
* `tsconfig.base.json` is the resolution facade for the whole repository, and
|
||||
* two of its aliases used one key per *group* rather than per package:
|
||||
* `@deepseek-ai/dsh-*` listed 49 candidate globs and `@deepseek-ai/dsh-*\/invariant`
|
||||
* listed 45. TypeScript and tsx try those candidates in order, so a specifier
|
||||
* `tsconfig.base.json` is the resolution facade for the whole repository. A
|
||||
* `@deepseek-ai/dsh-*` wildcard would list one candidate glob per package
|
||||
* group. TypeScript and tsx try those candidates in order, so a specifier
|
||||
* whose package sits late in the list pays for every earlier miss. Under tsx's
|
||||
* ESM hook each miss is an `ERR_MODULE_NOT_FOUND` that Node decorates with a
|
||||
* full CommonJS resolution walk, which dominated source-launch boot.
|
||||
@@ -37,8 +36,6 @@ interface PackageAlias {
|
||||
readonly specifier: string
|
||||
/** Repository-relative source directory, e.g. `./packages/session/session/src`. */
|
||||
readonly source: string
|
||||
/** Whether the package carries `src/invariant.ts`, which earns a second alias. */
|
||||
readonly hasInvariant: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,7 +59,6 @@ function packageName(manifest: string): string | undefined {
|
||||
interface WorkspacePackage {
|
||||
readonly group: string
|
||||
readonly directory: string
|
||||
readonly packageDir: string
|
||||
readonly name: string
|
||||
}
|
||||
|
||||
@@ -80,7 +76,7 @@ function workspacePackages(): WorkspacePackage[] {
|
||||
const packageDir = join(groupDir, directory)
|
||||
const name = packageName(join(packageDir, 'package.json'))
|
||||
if (name === undefined || !name.startsWith(PREFIX)) continue
|
||||
if (existsSync(join(packageDir, 'src'))) found.push({ group, directory, packageDir, name })
|
||||
if (existsSync(join(packageDir, 'src'))) found.push({ group, directory, name })
|
||||
}
|
||||
}
|
||||
return found
|
||||
@@ -100,7 +96,7 @@ function workspacePackages(): WorkspacePackage[] {
|
||||
*/
|
||||
export function collectPackageAliases(): PackageAlias[] {
|
||||
const bySpecifier = new Map<string, PackageAlias & { directory: string }>()
|
||||
for (const { group, directory, packageDir, name } of workspacePackages()) {
|
||||
for (const { group, directory, name } of workspacePackages()) {
|
||||
if (name !== `${PREFIX}${directory}`) continue
|
||||
const previous = bySpecifier.get(name)
|
||||
if (previous !== undefined) {
|
||||
@@ -112,12 +108,11 @@ export function collectPackageAliases(): PackageAlias[] {
|
||||
bySpecifier.set(name, {
|
||||
specifier: name,
|
||||
source: `./packages/${group}/${directory}/src`,
|
||||
hasInvariant: existsSync(join(packageDir, 'src', 'invariant.ts')),
|
||||
directory: `${group}/${directory}`,
|
||||
})
|
||||
}
|
||||
return [...bySpecifier.values()]
|
||||
.map(({ specifier, source, hasInvariant }) => ({ specifier, source, hasInvariant }))
|
||||
.map(({ specifier, source }) => ({ specifier, source }))
|
||||
.sort((left, right) => left.specifier.localeCompare(right.specifier))
|
||||
}
|
||||
|
||||
@@ -182,10 +177,6 @@ export function renderAliases(aliases: readonly PackageAlias[], handWritten: Rea
|
||||
if (!handWritten.has(alias.specifier)) {
|
||||
lines.push(` ${JSON.stringify(alias.specifier)}: [${JSON.stringify(alias.source)}]`)
|
||||
}
|
||||
const invariant = `${alias.specifier}/invariant`
|
||||
if (alias.hasInvariant && !handWritten.has(invariant)) {
|
||||
lines.push(` ${JSON.stringify(invariant)}: [${JSON.stringify(`${alias.source}/invariant.ts`)}]`)
|
||||
}
|
||||
}
|
||||
// The region closes `paths`, so the last member carries no trailing comma.
|
||||
return lines.join(',\n')
|
||||
|
||||
@@ -0,0 +1,559 @@
|
||||
/** Behavior of the local Official-bundle registry: closure selection, archive validation, HTTP, and shutdown. */
|
||||
|
||||
import { createHash } from 'node:crypto'
|
||||
import { connect } from 'node:net'
|
||||
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { execa } from 'execa'
|
||||
import { describe, expect, it, onTestFinished } from 'vitest'
|
||||
import { ON_DEMAND_BUNDLES } from '../packages/boot/app-boot/src/official-bundle-packages.ts'
|
||||
import { generateOfficialBundleCatalog } from './gen-official-bundle-catalog.ts'
|
||||
import {
|
||||
createRegistryManifest,
|
||||
hostPlatform,
|
||||
pnpmInvocationFor,
|
||||
readPackedArtifacts,
|
||||
readReleaseMembers,
|
||||
readWorkspacePackages,
|
||||
selectBundleDependencies,
|
||||
packBundleArtifacts,
|
||||
startBundleRegistry,
|
||||
writeRegistryManifest,
|
||||
type BundleArtifact,
|
||||
type ReleaseEntry,
|
||||
type WorkspacePackage,
|
||||
} from './local-bundle-registry.ts'
|
||||
import { releaseFamily, type ReleaseMember } from './release/families.ts'
|
||||
import { capture } from './release/process.ts'
|
||||
|
||||
const root = fileURLToPath(new URL('..', import.meta.url))
|
||||
|
||||
/** A private temporary root this spec owns, removed with the test. */
|
||||
function temporaryDirectory(prefix: string): string {
|
||||
const directory = realpathSync(mkdtempSync(join(tmpdir(), prefix)))
|
||||
onTestFinished(() => { rmSync(directory, { recursive: true, force: true }) })
|
||||
return directory
|
||||
}
|
||||
|
||||
/** One fabricated workspace package with a `1.0.0` version line. */
|
||||
function workspacePackage(name: string, manifest: Record<string, unknown>): WorkspacePackage {
|
||||
return { name, directory: `/workspace/${name.replace('@', '').replace('/', '-')}`, manifest: { name, version: '1.0.0', ...manifest } }
|
||||
}
|
||||
|
||||
/** An inventory of fabricated packages, keyed the way discovery keys them. */
|
||||
function inventory(...members: WorkspacePackage[]): Map<string, WorkspacePackage> {
|
||||
return new Map(members.map(member => [member.name, member]))
|
||||
}
|
||||
|
||||
/** One fabricated package's archive together with the identities needed to read it back. */
|
||||
interface Fixture {
|
||||
readonly member: WorkspacePackage
|
||||
readonly entry: ReleaseEntry
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack one fabricated package archive into a run directory.
|
||||
* @param options - destination, identity, source text, dependency ranges, and an optional declared bundle patch.
|
||||
* @returns The workspace identity and the release entry its tarball must satisfy.
|
||||
*/
|
||||
function packFixture(options: {
|
||||
readonly destination: string
|
||||
readonly name: string
|
||||
readonly version: string
|
||||
readonly source: string
|
||||
readonly dependencies?: Record<string, string>
|
||||
readonly patch?: string
|
||||
}): Fixture {
|
||||
const staging = join(options.destination, `${options.name.replace('@', '').replace('/', '-')}-staging`)
|
||||
const manifest: Record<string, unknown> = {
|
||||
name: options.name,
|
||||
version: options.version,
|
||||
main: 'lib/index.js',
|
||||
...options.dependencies === undefined ? {} : { dependencies: options.dependencies },
|
||||
...options.patch === undefined ? {} : { dsh: { bundle: { patch: options.patch } } },
|
||||
}
|
||||
mkdirSync(join(staging, 'package', 'lib'), { recursive: true })
|
||||
writeFileSync(join(staging, 'package', 'package.json'), JSON.stringify(manifest))
|
||||
writeFileSync(join(staging, 'package', 'lib', 'index.js'), options.source)
|
||||
if (options.patch !== undefined) writeFileSync(join(staging, 'package', options.patch), '[]\n')
|
||||
const file = `${options.name.replace('@', '').replace('/', '-')}-${options.version}.tgz`
|
||||
capture('tar', ['-czf', join(options.destination, file), '-C', staging, 'package'])
|
||||
const releaseMember: ReleaseMember = { directory: staging, name: options.name, version: options.version, manifest }
|
||||
return {
|
||||
member: workspacePackage(options.name, { version: options.version, ...manifest }),
|
||||
entry: { family: releaseFamily('vendor'), member: releaseMember },
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack a fixture archive and read it back through the real archive validation.
|
||||
* @param options - the fixture to pack.
|
||||
* @returns The servable artifact and the identities it was packed from.
|
||||
*/
|
||||
function fabricate(options: Parameters<typeof packFixture>[0]): Fixture & { readonly artifact: BundleArtifact } {
|
||||
const fixture = packFixture(options)
|
||||
const [artifact] = readPackedArtifacts({
|
||||
destination: options.destination,
|
||||
members: [fixture.member],
|
||||
release: new Map([[fixture.member.name, fixture.entry]]),
|
||||
})
|
||||
if (artifact === undefined) throw new Error('fabricated artifact was not read back')
|
||||
return { ...fixture, artifact }
|
||||
}
|
||||
|
||||
/** Start a registry for one fabricated run, closed with the test. */
|
||||
async function servingRun(options: {
|
||||
readonly destination: string
|
||||
readonly artifacts: readonly BundleArtifact[]
|
||||
readonly workspaceNames: readonly string[]
|
||||
}): Promise<string> {
|
||||
const registry = await startBundleRegistry({
|
||||
directory: options.destination, artifacts: options.artifacts, workspaceNames: options.workspaceNames,
|
||||
})
|
||||
onTestFinished(() => registry.close())
|
||||
return registry.url
|
||||
}
|
||||
|
||||
describe('catalog and local closure', () => {
|
||||
it('selects the real catalog closure from this checkout', () => {
|
||||
const entries = generateOfficialBundleCatalog(root).map(entry => entry.packageName)
|
||||
expect(entries).toEqual([...ON_DEMAND_BUNDLES])
|
||||
const packages = readWorkspacePackages(root)
|
||||
const release = readReleaseMembers(root)
|
||||
const selection = selectBundleDependencies({ entries, packages, platform: hostPlatform() })
|
||||
|
||||
expect(selection.members.map(member => member.name)).toEqual(expect.arrayContaining([...entries]))
|
||||
for (const member of selection.members) {
|
||||
expect(member.manifest.private, member.name).not.toBe(true)
|
||||
expect(release.has(member.name), member.name).toBe(true)
|
||||
}
|
||||
expect(selection.workspaceNames).toEqual(expect.arrayContaining(selection.members.map(member => member.name)))
|
||||
// A vendor package keeps its own version line, so a run must not assume one shared version.
|
||||
expect(packages.get('@deepseek-ai/cordis')?.manifest.version)
|
||||
.not.toBe(packages.get('@deepseek-ai/dsh-brand')?.manifest.version)
|
||||
// Nothing in this closure is built for another platform, on any host.
|
||||
expect(selection.foreignTargets).toEqual([])
|
||||
})
|
||||
|
||||
it('follows install, optional, and required peer edges but not optional peers', () => {
|
||||
const selection = selectBundleDependencies({
|
||||
entries: ['@scope/root'],
|
||||
packages: inventory(
|
||||
workspacePackage('@scope/root', {
|
||||
dependencies: { '@scope/required': 'workspace:*' },
|
||||
optionalDependencies: { '@scope/optional': 'workspace:*' },
|
||||
peerDependencies: { '@scope/peer': 'workspace:*', '@scope/unused-peer': 'workspace:*' },
|
||||
peerDependenciesMeta: { '@scope/unused-peer': { optional: true } },
|
||||
}),
|
||||
workspacePackage('@scope/required', {}),
|
||||
workspacePackage('@scope/optional', { dependencies: { '@scope/transitive': 'workspace:*' } }),
|
||||
workspacePackage('@scope/peer', {}),
|
||||
workspacePackage('@scope/transitive', {}),
|
||||
workspacePackage('@scope/unused-peer', {}),
|
||||
workspacePackage('@scope/unreferenced', {}),
|
||||
),
|
||||
platform: { os: ['darwin'] },
|
||||
})
|
||||
expect(selection.members.map(member => member.name)).toEqual([
|
||||
'@scope/optional', '@scope/peer', '@scope/required', '@scope/root', '@scope/transitive',
|
||||
])
|
||||
})
|
||||
|
||||
it('leaves a dependency that is not a workspace package to its own registry', () => {
|
||||
const selection = selectBundleDependencies({
|
||||
entries: ['@scope/root'],
|
||||
packages: inventory(workspacePackage('@scope/root', {
|
||||
dependencies: { '@other/external': '^1.0.0', 'plain-external': '^2.0.0' },
|
||||
})),
|
||||
platform: {},
|
||||
})
|
||||
expect(selection.members.map(member => member.name)).toEqual(['@scope/root'])
|
||||
})
|
||||
|
||||
it('fails when a catalog root is not a workspace package', () => {
|
||||
expect(() => selectBundleDependencies({ entries: ['@scope/missing'], packages: inventory(), platform: {} }))
|
||||
.toThrow(/cannot resolve workspace package @scope\/missing \(catalog entry @scope\/missing\)/u)
|
||||
})
|
||||
|
||||
it('rejects an empty or duplicate catalog', () => {
|
||||
expect(() => selectBundleDependencies({ entries: [], packages: inventory(), platform: {} }))
|
||||
.toThrow(/no on-demand entries/u)
|
||||
expect(() => selectBundleDependencies({ entries: ['@scope/a', '@scope/a'], packages: inventory(), platform: {} }))
|
||||
.toThrow(/must be distinct/u)
|
||||
})
|
||||
|
||||
it('fails on a required edge that targets another platform, naming the path', () => {
|
||||
expect(() => selectBundleDependencies({
|
||||
entries: ['@scope/root'],
|
||||
packages: inventory(
|
||||
workspacePackage('@scope/root', { dependencies: { '@scope/native-win': 'workspace:*' } }),
|
||||
workspacePackage('@scope/native-win', { os: ['win32'] }),
|
||||
),
|
||||
platform: { os: ['darwin'], cpu: ['arm64'] },
|
||||
})).toThrow(/@scope\/root -> @scope\/native-win requires @scope\/native-win, which targets os=win32; this host is os=darwin cpu=arm64/u)
|
||||
})
|
||||
|
||||
it('keeps an optional-only foreign-target member and reports it', () => {
|
||||
const selection = selectBundleDependencies({
|
||||
entries: ['@scope/root'],
|
||||
packages: inventory(
|
||||
workspacePackage('@scope/root', { optionalDependencies: { '@scope/native-win': 'workspace:*' } }),
|
||||
workspacePackage('@scope/native-win', { os: ['win32'] }),
|
||||
),
|
||||
platform: { os: ['darwin'] },
|
||||
})
|
||||
expect(selection.members.map(member => member.name)).toEqual(['@scope/native-win', '@scope/root'])
|
||||
expect(selection.foreignTargets).toEqual(['@scope/native-win'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('packed archives', () => {
|
||||
it('describes exactly the bytes it will serve', () => {
|
||||
const destination = temporaryDirectory('dsh-local-registry-archive-')
|
||||
const { artifact } = fabricate({ destination, name: '@scope/pkg', version: '1.0.0', source: 'export default 1\n' })
|
||||
const bytes = readFileSync(artifact.archive)
|
||||
expect(artifact.bytes).toBe(bytes.byteLength)
|
||||
expect(artifact.integrity).toBe(`sha512-${createHash('sha512').update(bytes).digest('base64')}`)
|
||||
expect(artifact.shasum).toBe(createHash('sha1').update(bytes).digest('hex'))
|
||||
expect(artifact.file).toBe('scope-pkg-1.0.0.tgz')
|
||||
expect(artifact.platform).toEqual({})
|
||||
expect(artifact.manifest.version).toBe('1.0.0')
|
||||
})
|
||||
|
||||
it('rejects an archive that lacks the main it declares', () => {
|
||||
const destination = temporaryDirectory('dsh-local-registry-payload-')
|
||||
const staging = join(destination, 'staging')
|
||||
mkdirSync(join(staging, 'package'), { recursive: true })
|
||||
writeFileSync(join(staging, 'package', 'package.json'), JSON.stringify({
|
||||
name: '@scope/pkg', version: '1.0.0', main: 'lib/index.js', dsh: { bundle: { patch: 'cordis.patch.yml' } },
|
||||
}))
|
||||
capture('tar', ['-czf', join(destination, 'scope-pkg-1.0.0.tgz'), '-C', staging, 'package'])
|
||||
const member = workspacePackage('@scope/pkg', { version: '1.0.0' })
|
||||
const entry: ReleaseEntry = {
|
||||
family: releaseFamily('vendor'), member: { directory: staging, name: '@scope/pkg', version: '1.0.0', manifest: member.manifest },
|
||||
}
|
||||
expect(() => readPackedArtifacts({ destination, members: [member], release: new Map([['@scope/pkg', entry]]) }))
|
||||
.toThrow(/@scope\/pkg: packed archive lacks its declared main lib\/index\.js/u)
|
||||
})
|
||||
|
||||
it('rejects a bundle whose patch is missing from its archive', () => {
|
||||
const destination = temporaryDirectory('dsh-local-registry-patch-')
|
||||
const { member, entry } = packFixture({
|
||||
destination, name: '@scope/bundle', version: '1.0.0', source: 'export default 1\n', patch: 'cordis.patch.yml',
|
||||
})
|
||||
const archive = join(destination, 'scope-bundle-1.0.0.tgz')
|
||||
const stripped = join(destination, 'stripped')
|
||||
mkdirSync(join(stripped, 'package'), { recursive: true })
|
||||
writeFileSync(join(stripped, 'package', 'package.json'), JSON.stringify({
|
||||
name: '@scope/bundle', version: '1.0.0', main: 'lib/index.js', dsh: { bundle: { patch: 'cordis.patch.yml' } },
|
||||
}))
|
||||
mkdirSync(join(stripped, 'package', 'lib'), { recursive: true })
|
||||
writeFileSync(join(stripped, 'package', 'lib', 'index.js'), 'export default 1\n')
|
||||
capture('tar', ['-czf', archive, '-C', stripped, 'package'])
|
||||
expect(() => readPackedArtifacts({ destination, members: [member], release: new Map([['@scope/bundle', entry]]) }))
|
||||
.toThrow(/@scope\/bundle: packed archive lacks its declared bundle patch cordis\.patch\.yml/u)
|
||||
})
|
||||
|
||||
it('reports a preparation stopped by a signal as a cancellation', async () => {
|
||||
const destination = temporaryDirectory('dsh-local-registry-cancel-')
|
||||
const { member, entry } = packFixture({ destination, name: '@scope/pkg', version: '1.0.0', source: 'export default 1\n' })
|
||||
const controller = new AbortController()
|
||||
controller.abort()
|
||||
await expect(packBundleArtifacts({
|
||||
root, destination, members: [member], release: new Map([[member.name, entry]]), signal: controller.signal,
|
||||
})).rejects.toThrow(/packing was cancelled/u)
|
||||
})
|
||||
|
||||
it('rejects a missing archive and a package that is not a public release member', () => {
|
||||
const destination = temporaryDirectory('dsh-local-registry-missing-')
|
||||
const missing = workspacePackage('@scope/pkg', { version: '1.0.0' })
|
||||
const entry: ReleaseEntry = {
|
||||
family: releaseFamily('vendor'), member: { directory: destination, name: '@scope/pkg', version: '1.0.0', manifest: missing.manifest },
|
||||
}
|
||||
expect(() => readPackedArtifacts({ destination, members: [missing], release: new Map([['@scope/pkg', entry]]) }))
|
||||
.toThrow(/@scope\/pkg produced no tarball/u)
|
||||
fabricate({ destination, name: '@scope/pkg', version: '1.0.0', source: 'export default 1\n' })
|
||||
expect(() => readPackedArtifacts({ destination, members: [missing], release: new Map() }))
|
||||
.toThrow(/not a public release member/u)
|
||||
})
|
||||
|
||||
it('rejects a packed manifest that keeps a workspace protocol', () => {
|
||||
const destination = temporaryDirectory('dsh-local-registry-protocol-')
|
||||
const { member, entry } = packFixture({
|
||||
destination, name: '@scope/pkg', version: '1.0.0', source: 'export default 1\n',
|
||||
dependencies: { '@scope/dependency': 'workspace:*' },
|
||||
})
|
||||
// The fixture writes the manifest verbatim, so a protocol a real pack rewrites is still observable here.
|
||||
expect(() => readPackedArtifacts({ destination, members: [member], release: new Map([['@scope/pkg', entry]]) }))
|
||||
.toThrow(/packed dependencies\.@scope\/dependency still declares workspace:\*/u)
|
||||
})
|
||||
|
||||
it('rejects a packed local dependency this run does not serve at the required version', () => {
|
||||
const destination = temporaryDirectory('dsh-local-registry-version-')
|
||||
const consumer = packFixture({
|
||||
destination, name: '@scope/pkg', version: '1.0.0', source: 'export default 1\n',
|
||||
dependencies: { '@scope/dependency': '^2.0.0' },
|
||||
})
|
||||
const dependency = packFixture({ destination, name: '@scope/dependency', version: '1.0.0', source: 'export default 2\n' })
|
||||
const release = new Map([[consumer.member.name, consumer.entry], [dependency.member.name, dependency.entry]])
|
||||
expect(() => readPackedArtifacts({ destination, members: [consumer.member, dependency.member], release }))
|
||||
.toThrow(/@scope\/pkg: packed dependencies\.@scope\/dependency requires \^2\.0\.0, but this run serves 1\.0\.0/u)
|
||||
})
|
||||
})
|
||||
|
||||
describe('registry HTTP', () => {
|
||||
it('answers scoped packuments, version documents, and dist tags for its own artifacts', async () => {
|
||||
const destination = temporaryDirectory('dsh-local-registry-http-')
|
||||
const { artifact } = fabricate({ destination, name: '@scope/local', version: '1.2.3-alpha.1', source: 'export default 1\n' })
|
||||
const url = await servingRun({ destination, artifacts: [artifact], workspaceNames: ['@scope/local', '@scope/absent'] })
|
||||
|
||||
// pnpm encodes the scope separator; an unencoded path names the same package.
|
||||
for (const path of ['@scope%2flocal', '@scope/local']) {
|
||||
const response = await fetch(`${url}${path}`)
|
||||
expect(response.status, path).toBe(200)
|
||||
const packument = await response.json() as {
|
||||
name: string
|
||||
'dist-tags': Record<string, string>
|
||||
versions: Record<string, { dist: { tarball: string; integrity: string } }>
|
||||
time: Record<string, string>
|
||||
}
|
||||
expect(packument.name).toBe('@scope/local')
|
||||
expect(packument['dist-tags']).toEqual({ latest: '1.2.3-alpha.1', next: '1.2.3-alpha.1' })
|
||||
expect(packument.versions['1.2.3-alpha.1']?.dist.tarball).toBe(`${url}tarballs/scope-local-1.2.3-alpha.1.tgz`)
|
||||
expect(packument.versions['1.2.3-alpha.1']?.dist.integrity).toBe(artifact.integrity)
|
||||
// The only publication time a run can claim is when its own archives were packed.
|
||||
expect(packument.time['1.2.3-alpha.1']).toBe(artifact.packedAt)
|
||||
}
|
||||
for (const path of ['@scope%2flocal/1.2.3-alpha.1', '@scope/local/latest', '@scope/local/next']) {
|
||||
const response = await fetch(`${url}${path}`)
|
||||
expect(response.status, path).toBe(200)
|
||||
expect(await response.json()).toMatchObject({ name: '@scope/local', version: '1.2.3-alpha.1' })
|
||||
}
|
||||
const unknown = await fetch(`${url}@scope%2flocal/9.9.9`)
|
||||
expect(unknown.status).toBe(404)
|
||||
expect(await unknown.json()).toMatchObject({ reason: 'this run serves @scope/[email protected] only' })
|
||||
})
|
||||
|
||||
it('serves exactly the archived bytes, and no body for HEAD', async () => {
|
||||
const destination = temporaryDirectory('dsh-local-registry-tarball-')
|
||||
const { artifact } = fabricate({ destination, name: '@scope/local', version: '1.0.0', source: 'export default 1\n' })
|
||||
const url = await servingRun({ destination, artifacts: [artifact], workspaceNames: ['@scope/local'] })
|
||||
const response = await fetch(`${url}tarballs/${artifact.file}`)
|
||||
expect(response.status).toBe(200)
|
||||
expect(Buffer.from(await response.arrayBuffer())).toEqual(readFileSync(artifact.archive))
|
||||
const head = await fetch(`${url}tarballs/${artifact.file}`, { method: 'HEAD' })
|
||||
expect(head.status).toBe(200)
|
||||
expect(head.headers.get('content-length')).toBe(String(artifact.bytes))
|
||||
expect(await head.text()).toBe('')
|
||||
expect((await fetch(`${url}tarballs/no-such-package-1.0.0.tgz`)).status).toBe(404)
|
||||
})
|
||||
|
||||
it('keeps a repository-owned miss local and redirects only external packages', async () => {
|
||||
const destination = temporaryDirectory('dsh-local-registry-miss-')
|
||||
const { artifact } = fabricate({ destination, name: '@scope/local', version: '1.0.0', source: 'export default 1\n' })
|
||||
const url = await servingRun({
|
||||
destination, artifacts: [artifact], workspaceNames: ['@scope/local', '@deepseek-ai/dsh-private-thing'],
|
||||
})
|
||||
const owned = await fetch(`${url}@deepseek-ai%2fdsh-private-thing`)
|
||||
expect(owned.status).toBe(404)
|
||||
const refusal = await owned.json() as { reason?: string }
|
||||
expect(refusal.reason ?? '').toContain('repository-owned')
|
||||
// A repository-owned name that is not even in the closure must never reach npm.
|
||||
expect((await fetch(`${url}@deepseek-ai%2fdsh-private-thing/1.0.0`)).status).toBe(404)
|
||||
|
||||
const external = await fetch(`${url}@anthropic-ai%2fclaude-agent-sdk`, { redirect: 'manual' })
|
||||
expect(external.status).toBe(302)
|
||||
expect(external.headers.get('location')).toBe('https://registry.npmjs.org/@anthropic-ai%2fclaude-agent-sdk')
|
||||
})
|
||||
|
||||
it('keeps serving after a client abandons a tarball request', async () => {
|
||||
const destination = temporaryDirectory('dsh-local-registry-disconnect-')
|
||||
const { artifact } = fabricate({ destination, name: '@scope/local', version: '1.0.0', source: 'export default 1\n' })
|
||||
const url = await servingRun({ destination, artifacts: [artifact], workspaceNames: ['@scope/local'] })
|
||||
const { port, pathname } = new URL(url)
|
||||
const socket = connect(Number(port), '127.0.0.1')
|
||||
await new Promise<void>((resolveConnect, rejectConnect) => {
|
||||
socket.once('connect', resolveConnect)
|
||||
socket.once('error', rejectConnect)
|
||||
})
|
||||
socket.write(`GET ${pathname}tarballs/${artifact.file} HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\n\r\n`)
|
||||
await new Promise<void>((resolveData) => { socket.once('data', () => { resolveData() }) })
|
||||
socket.destroy()
|
||||
const response = await fetch(`${url}@scope%2flocal`)
|
||||
expect(response.status).toBe(200)
|
||||
expect(await response.json()).toMatchObject({ name: '@scope/local' })
|
||||
})
|
||||
|
||||
it('rejects out-of-namespace, malformed, and non-GET requests, then stops listening', async () => {
|
||||
const destination = temporaryDirectory('dsh-local-registry-reject-')
|
||||
const { artifact } = fabricate({ destination, name: '@scope/local', version: '1.0.0', source: 'export default 1\n' })
|
||||
const registry = await startBundleRegistry({ directory: destination, artifacts: [artifact], workspaceNames: ['@scope/local'] })
|
||||
onTestFinished(() => registry.close())
|
||||
const { origin, pathname } = new URL(registry.url)
|
||||
|
||||
expect((await fetch(`${origin}/@scope%2flocal`)).status).toBe(404)
|
||||
expect((await fetch(`${origin}${pathname}%zz`)).status).toBe(404)
|
||||
expect((await fetch(`${registry.url}@scope%2flocal/%2e%2e%2f%2e%2e%2fetc%2fpasswd`)).status).toBe(404)
|
||||
expect((await fetch(`${registry.url}@scope%2flocal/1.0.0/extra`)).status).toBe(404)
|
||||
expect((await fetch(`${registry.url}tarballs/%2e%2e%2f%2e%2e%2fetc%2fpasswd`)).status).toBe(404)
|
||||
expect((await fetch(`${registry.url}-/ping`)).status).toBe(404)
|
||||
expect((await fetch(registry.url, { method: 'POST', body: 'x' })).status).toBe(405)
|
||||
|
||||
const other = await startBundleRegistry({ directory: destination, artifacts: [artifact], workspaceNames: ['@scope/local'] })
|
||||
onTestFinished(() => other.close())
|
||||
expect(other.url).not.toBe(registry.url)
|
||||
await registry.close()
|
||||
await expect(fetch(`${registry.url}@scope%2flocal`)).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('run record', () => {
|
||||
it('names the run, its membership, and the integrity of every served archive', async () => {
|
||||
const destination = temporaryDirectory('dsh-local-registry-manifest-')
|
||||
const { artifact } = fabricate({ destination, name: '@scope/local', version: '1.0.0', source: 'export default 1\n' })
|
||||
const manifest = createRegistryManifest({
|
||||
buildId: 'run-1',
|
||||
source: { commit: 'a'.repeat(40), dirty: true, dshVersion: '0.0.0-test' },
|
||||
entries: ['@scope/local'],
|
||||
workspaceNames: ['@scope/local', '@scope/absent'],
|
||||
artifacts: [artifact],
|
||||
})
|
||||
expect(manifest).toEqual({
|
||||
schemaVersion: 1,
|
||||
buildId: 'run-1',
|
||||
source: { commit: 'a'.repeat(40), dirty: true, dshVersion: '0.0.0-test' },
|
||||
entries: ['@scope/local'],
|
||||
workspaceNames: ['@scope/local', '@scope/absent'],
|
||||
packages: [{ name: '@scope/local', version: '1.0.0', file: artifact.file, bytes: artifact.bytes, integrity: artifact.integrity }],
|
||||
})
|
||||
const path = join(destination, 'bundle-registry.json')
|
||||
await writeRegistryManifest(path, manifest)
|
||||
expect(JSON.parse(readFileSync(path, 'utf8'))).toEqual(manifest)
|
||||
expect(readFileSync(path, 'utf8').endsWith('}\n')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 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-')
|
||||
const store = join(temporary, 'store')
|
||||
const cache = join(temporary, 'cache')
|
||||
const userConfig = join(temporary, 'empty.npmrc')
|
||||
writeFileSync(userConfig, '')
|
||||
// A retained store, metadata cache, and npm configuration, so the second installation cannot pass by starting empty.
|
||||
const environment = { ...process.env, npm_config_userconfig: userConfig }
|
||||
|
||||
const install = async (run: string, source: string): Promise<{ payload: string; settings: string }> => {
|
||||
const destination = join(temporary, run, 'artifacts')
|
||||
mkdirSync(destination, { recursive: true })
|
||||
const { artifact } = fabricate({ destination, name: '@dsh-local/fixture', version: '1.0.0', source })
|
||||
const registry = await startBundleRegistry({
|
||||
directory: destination, artifacts: [artifact], workspaceNames: ['@dsh-local/fixture'],
|
||||
})
|
||||
onTestFinished(() => registry.close())
|
||||
const consumer = consumerDirectory(temporary, `consumer-${run}`)
|
||||
const result = await pnpmIn({
|
||||
consumer, environment, store, cache,
|
||||
args: ['add', '@dsh-local/[email protected]', `--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: installedPayload(consumer), settings: readFileSync(join(consumer, 'pnpm-workspace.yaml'), 'utf8') }
|
||||
}
|
||||
|
||||
const first = await install('first', 'export default "first build"\n')
|
||||
expect(first.payload).toContain('first build')
|
||||
// pnpm 11's default minimumReleaseAge is 24 hours, and its non-strict mode records the exemption it granted
|
||||
// instead of faking an older publication time for a locally packed artifact.
|
||||
expect(first.settings).toContain('minimumReleaseAgeExclude')
|
||||
expect(first.settings).toContain('@dsh-local/[email protected]')
|
||||
|
||||
const second = await install('second', 'export default "second build"\n')
|
||||
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/[email protected]', `--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"')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,929 @@
|
||||
/**
|
||||
* Pack the local dependency closure of the Official on-demand bundles and serve
|
||||
* it as a loopback npm registry.
|
||||
*
|
||||
* 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
|
||||
* 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
|
||||
* fixed redirect. Publication is untouched; only one development run's
|
||||
* transport is local.
|
||||
*/
|
||||
|
||||
import { createHash, randomUUID } from 'node:crypto'
|
||||
import { createReadStream, existsSync, globSync, readFileSync, statSync } from 'node:fs'
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import { createServer, type Server, type ServerResponse } from 'node:http'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { execa } from 'execa'
|
||||
import yaml from 'js-yaml'
|
||||
import semver from 'semver'
|
||||
import { pnpmInvocation } from './pnpm-invocation.ts'
|
||||
import { capture, pnpmCommand } from './release/process.ts'
|
||||
import { releaseFamily, tarballName, type ReleaseFamily, type ReleaseMember } from './release/families.ts'
|
||||
import { packedManifest, tarballFiles } from './release/tarball.ts'
|
||||
|
||||
/** Run directories a registry invocation owns, relative to the repository root. */
|
||||
export const REGISTRY_OUTPUT_ROOT = 'dist/test-bundles'
|
||||
|
||||
/** Manifest filename whose presence marks a run directory as complete. */
|
||||
export const REGISTRY_MANIFEST_FILE = 'bundle-registry.json'
|
||||
|
||||
/** Registry every external package is redirected to, with the request path preserved. */
|
||||
export const UPSTREAM_REGISTRY = 'https://registry.npmjs.org/'
|
||||
|
||||
/** Installation edges a consumer cannot load without. */
|
||||
const REQUIRED_SECTIONS = ['dependencies'] as const
|
||||
|
||||
/** Edges pnpm may skip on this host; their packages still need local metadata. */
|
||||
const OPTIONAL_SECTIONS = ['optionalDependencies'] as const
|
||||
|
||||
/** Edges that constrain an installation without installing on the package's behalf. */
|
||||
const PEER_SECTIONS = ['peerDependencies'] as const
|
||||
|
||||
/** One workspace package available to pack and serve. */
|
||||
export interface WorkspacePackage {
|
||||
/** Package name exactly as it is declared. */
|
||||
readonly name: string
|
||||
/** Absolute package directory. */
|
||||
readonly directory: string
|
||||
/** Parsed `package.json`. */
|
||||
readonly manifest: Record<string, unknown>
|
||||
}
|
||||
|
||||
/** Platform constraints a package declares for its own artifacts. */
|
||||
export interface PlatformConstraint {
|
||||
/** Accepted `process.platform` values, which may be `!value` negations. */
|
||||
readonly os?: readonly string[]
|
||||
/** Accepted `process.arch` values, which may be `!value` negations. */
|
||||
readonly cpu?: readonly string[]
|
||||
/** Accepted libc values on Linux, which may be `!value` negations. */
|
||||
readonly libc?: readonly string[]
|
||||
}
|
||||
|
||||
/** One packed package archive the registry answers with. */
|
||||
export interface BundleArtifact {
|
||||
/** Package name from the packed manifest. */
|
||||
readonly name: string
|
||||
/** Packed version, served exactly. */
|
||||
readonly version: string
|
||||
/** Absolute archive path in the run directory. */
|
||||
readonly archive: string
|
||||
/** Tarball filename inside the run directory. */
|
||||
readonly file: string
|
||||
/** Archive size in bytes, as served. */
|
||||
readonly bytes: number
|
||||
/** SHA-512 subresource integrity of the served bytes. */
|
||||
readonly integrity: string
|
||||
/** SHA-1 hex digest of the served bytes. */
|
||||
readonly shasum: string
|
||||
/** Manifest contained in the archive, which the registry serves as the version metadata. */
|
||||
readonly manifest: Record<string, unknown>
|
||||
/** Platform constraints the packed manifest declares for itself. */
|
||||
readonly platform: PlatformConstraint
|
||||
/** Dist tag this version publishes under, from its release family. */
|
||||
readonly distTag?: string
|
||||
/** When this run packed the archive; the only publication time the run can honestly claim. */
|
||||
readonly packedAt: string
|
||||
}
|
||||
|
||||
/** The catalog roots together with the local packages an ordinary installation needs. */
|
||||
export interface BundleSelection {
|
||||
/** Catalog roots, in admission order. */
|
||||
readonly entries: readonly string[]
|
||||
/** Local closure, sorted by package name. */
|
||||
readonly members: readonly WorkspacePackage[]
|
||||
/** Every repository-owned name, so a request for one is a local miss rather than an upstream redirect. */
|
||||
readonly workspaceNames: readonly string[]
|
||||
/** Members built for another platform, kept so pnpm reads their metadata and applies its own skip. */
|
||||
readonly foreignTargets: readonly string[]
|
||||
}
|
||||
|
||||
/** What one run records beside its artifacts; the file is written only once the run is complete. */
|
||||
export interface RegistryManifest {
|
||||
/** Manifest layout version. */
|
||||
readonly schemaVersion: 1
|
||||
/** Unique run directory name. */
|
||||
readonly buildId: string
|
||||
/** Which bytes these artifacts were packed from. */
|
||||
readonly source: {
|
||||
/** Full Git object id of the packed checkout. */
|
||||
readonly commit: string
|
||||
/** Whether the checkout had uncommitted changes, so the commit alone is not the snapshot. */
|
||||
readonly dirty: boolean
|
||||
/** Running DSH version the catalog installation target names. */
|
||||
readonly dshVersion: string
|
||||
}
|
||||
/** Catalog roots in admission order. */
|
||||
readonly entries: readonly string[]
|
||||
/** Every repository-owned name. */
|
||||
readonly workspaceNames: readonly string[]
|
||||
/** Packed packages, sorted by name. */
|
||||
readonly packages: readonly {
|
||||
readonly name: string
|
||||
readonly version: string
|
||||
readonly file: string
|
||||
readonly bytes: number
|
||||
readonly integrity: string
|
||||
}[]
|
||||
}
|
||||
|
||||
/** A running loopback registry serving one immutable run directory. */
|
||||
export interface BundleRegistry {
|
||||
/** Registry URL ending in the run's fresh namespace and a slash, as `--registry` takes it. */
|
||||
readonly url: string
|
||||
/** Stop dispatching, drop open connections, and resolve once the listener is gone. */
|
||||
close(): Promise<void>
|
||||
}
|
||||
|
||||
/** One public release member and the family that owns its payload rules. */
|
||||
export interface ReleaseEntry {
|
||||
/** Family owning this member's payload and version rules. */
|
||||
readonly family: ReleaseFamily
|
||||
/** The member as its family publishes it. */
|
||||
readonly member: ReleaseMember
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve pnpm for a shell-free child process.
|
||||
*
|
||||
* A package script's own pnpm is authoritative, because it owns the workspace
|
||||
* lockfile and store; a direct invocation (`pnpm exec`, a test runner) has no
|
||||
* lifecycle environment, so the workspace's own pnpm dependency runs instead.
|
||||
* @param args - pnpm arguments.
|
||||
* @returns A command and argument array suitable for `spawn` without a shell.
|
||||
*/
|
||||
export function pnpmInvocationFor(args: readonly string[]): { command: string; args: string[] } {
|
||||
const entrypoint = process.env.npm_execpath
|
||||
// `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)
|
||||
}
|
||||
const [command, ...prefix] = pnpmCommand()
|
||||
return { command, args: [...prefix, ...args] }
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the pnpm workspace inventory.
|
||||
* @param root - repository root containing `pnpm-workspace.yaml`.
|
||||
* @returns Workspace packages indexed by name.
|
||||
*/
|
||||
export function readWorkspacePackages(root: string): Map<string, WorkspacePackage> {
|
||||
const settings: unknown = yaml.load(readFileSync(join(root, 'pnpm-workspace.yaml'), 'utf8'))
|
||||
if (!isRecord(settings)) throw new Error('pnpm-workspace.yaml must be a mapping')
|
||||
const patterns = settings.packages
|
||||
if (!Array.isArray(patterns) || patterns.length === 0 || !patterns.every(value => typeof value === 'string')) {
|
||||
throw new Error('pnpm-workspace.yaml must list package patterns')
|
||||
}
|
||||
const packages = new Map<string, WorkspacePackage>()
|
||||
const paths = globSync(patterns.map(pattern => `${pattern}/package.json`), { cwd: root })
|
||||
.map(path => path.replaceAll('\\', '/'))
|
||||
.sort()
|
||||
for (const path of paths) {
|
||||
const manifest = manifestAt(join(root, path))
|
||||
const name = manifest.name
|
||||
if (typeof name !== 'string' || name === '') throw new Error(`${path} must declare a string name`)
|
||||
if (packages.has(name)) throw new Error(`duplicate workspace package ${name}`)
|
||||
packages.set(name, { name, directory: resolve(root, path, '..'), manifest })
|
||||
}
|
||||
if (packages.size === 0) throw new Error('the pnpm workspace inventory is empty')
|
||||
return packages
|
||||
}
|
||||
|
||||
/**
|
||||
* Index the release families' public members by package name.
|
||||
* @param root - repository root containing the release families.
|
||||
* @returns Public release members keyed by package name.
|
||||
*/
|
||||
export function readReleaseMembers(root: string): Map<string, ReleaseEntry> {
|
||||
const members = new Map<string, ReleaseEntry>()
|
||||
for (const id of ['dsh', 'vendor']) {
|
||||
const family = releaseFamily(id)
|
||||
for (const member of family.members(root)) members.set(member.name, { family, member })
|
||||
}
|
||||
return members
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert each family's own version baseline, which is what one tag publishes.
|
||||
* @param root - repository root containing the release families.
|
||||
*/
|
||||
export function verifyReleaseVersions(root: string): void {
|
||||
for (const id of ['dsh', 'vendor']) {
|
||||
const family = releaseFamily(id)
|
||||
family.verifyVersions(family.members(root))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Require every catalog root to be a built, patch-carrying bundle in this checkout.
|
||||
*
|
||||
* The catalog generator already checks admission metadata; this checks the
|
||||
* artifacts a packed consumer would actually load, so a stale or partial build
|
||||
* fails before any archive is served.
|
||||
* @param options - catalog roots in admission order and the workspace inventory.
|
||||
*/
|
||||
export function verifyBuiltBundles(options: {
|
||||
readonly entries: readonly string[]
|
||||
readonly packages: ReadonlyMap<string, WorkspacePackage>
|
||||
}): void {
|
||||
for (const name of options.entries) {
|
||||
const member = options.packages.get(name)
|
||||
if (member === undefined) throw new Error(`catalog entry ${name} is not a workspace package`)
|
||||
const main = member.manifest.main
|
||||
if (typeof main !== 'string' || main === '') throw new Error(`${name} declares no package main to load`)
|
||||
const entry = resolve(member.directory, main)
|
||||
if (!existsSync(entry) || !statSync(entry).isFile()) {
|
||||
throw new Error(`${name}: built runtime is missing at ${entry}; run pnpm run build:official first`)
|
||||
}
|
||||
const dsh = member.manifest.dsh
|
||||
if (!isRecord(dsh) || !isRecord(dsh.bundle)) throw new Error(`${name} declares no dsh.bundle`)
|
||||
const patches = declaredBundlePatches(member.manifest)
|
||||
if (patches.length === 0) throw new Error(`${name}: dsh.bundle.patch must be a file path or a list of file paths`)
|
||||
for (const patch of patches) {
|
||||
const path = join(member.directory, patch)
|
||||
if (!existsSync(path) || !statSync(path).isFile()) throw new Error(`${name}: bundle patch ${path} is missing`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the platform constraints a manifest declares for its own artifacts.
|
||||
* @param manifest - package manifest.
|
||||
* @returns The declared `os`, `cpu`, and `libc` lists.
|
||||
*/
|
||||
export function platformConstraint(manifest: Record<string, unknown>): PlatformConstraint {
|
||||
const lists: { os?: readonly string[]; cpu?: readonly string[]; libc?: readonly string[] } = {}
|
||||
for (const field of ['os', 'cpu', 'libc'] as const) {
|
||||
const value = manifest[field]
|
||||
if (Array.isArray(value) && value.every(entry => typeof entry === 'string')) lists[field] = value
|
||||
}
|
||||
return lists
|
||||
}
|
||||
|
||||
/**
|
||||
* This host's platform values, for comparing against a package's own constraints.
|
||||
* @returns Host `os`, `cpu`, and libc.
|
||||
*/
|
||||
export function hostPlatform(): PlatformConstraint {
|
||||
if (process.platform !== 'linux') return { os: [process.platform], cpu: [process.arch] }
|
||||
const report = process.report.getReport() as { header?: { glibcVersionRuntime?: string } }
|
||||
return { os: [process.platform], cpu: [process.arch], libc: [report.header?.glibcVersionRuntime === undefined ? 'musl' : 'glibc'] }
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a package's declared platform accepts this host.
|
||||
* @param constraint - declared constraints.
|
||||
* @param host - host `os`, `cpu`, and libc values.
|
||||
* @returns True when the package may run here.
|
||||
*/
|
||||
export function matchesPlatform(constraint: PlatformConstraint, host: PlatformConstraint): boolean {
|
||||
return allowsValue(constraint.os, host.os?.[0])
|
||||
&& allowsValue(constraint.cpu, host.cpu?.[0])
|
||||
&& allowsValue(constraint.libc, host.libc?.[0])
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the catalog roots and the local packages an ordinary installation needs.
|
||||
*
|
||||
* Every dependency naming a workspace package is followed, whatever its section
|
||||
* or protocol, so a locally owned requirement is never left to npm. A required
|
||||
* edge naming a package built for another platform fails here, because the run
|
||||
* cannot supply the artifact that installer would need; an optional edge is
|
||||
* kept so pnpm still reads its metadata and applies its own platform skip.
|
||||
* @param options - catalog roots, workspace inventory, and the host to judge platform edges against.
|
||||
* @returns The closure, the repository-owned names, and the foreign-target members it kept.
|
||||
*/
|
||||
export function selectBundleDependencies(options: {
|
||||
readonly entries: readonly string[]
|
||||
readonly packages: ReadonlyMap<string, WorkspacePackage>
|
||||
readonly platform: PlatformConstraint
|
||||
}): BundleSelection {
|
||||
const { entries, packages, platform } = options
|
||||
if (entries.length === 0) throw new Error('the Official catalog has no on-demand entries')
|
||||
if (new Set(entries).size !== entries.length) throw new Error('Official catalog entries must be distinct')
|
||||
const selected = new Map<string, WorkspacePackage>()
|
||||
const parents = new Map<string, string>()
|
||||
const queued = new Set<string>()
|
||||
const queue: string[] = []
|
||||
const enqueue = (name: string, parent: string | undefined): void => {
|
||||
if (selected.has(name) || queued.has(name)) return
|
||||
queued.add(name)
|
||||
if (parent !== undefined) parents.set(name, parent)
|
||||
queue.push(name)
|
||||
}
|
||||
for (const entry of entries) enqueue(entry, undefined)
|
||||
for (let index = 0; index < queue.length; index += 1) {
|
||||
const name = queue[index] as string
|
||||
const member = packages.get(name)
|
||||
if (member === undefined) {
|
||||
throw new Error(`local bundle closure cannot resolve workspace package ${name} (${dependencyPath(name, parents, entries)})`)
|
||||
}
|
||||
selected.set(name, member)
|
||||
for (const name of installedEdges(member.manifest)) {
|
||||
if (packages.has(name)) enqueue(name, member.name)
|
||||
}
|
||||
}
|
||||
|
||||
const foreign: string[] = []
|
||||
for (const [name, member] of selected) {
|
||||
if (matchesPlatform(platformConstraint(member.manifest), platform)) continue
|
||||
if (reachedByRequiredEdge(name, entries, selected)) {
|
||||
throw new Error(
|
||||
`${dependencyPath(name, parents, entries)} requires ${name}, which targets`
|
||||
+ ` ${describePlatform(platformConstraint(member.manifest))}; this host is ${describePlatform(platform)}.`
|
||||
+ ' This run cannot supply a matching artifact for that package.',
|
||||
)
|
||||
}
|
||||
foreign.push(name)
|
||||
}
|
||||
|
||||
return {
|
||||
entries: [...entries],
|
||||
members: [...selected.values()].sort((left, right) => left.name.localeCompare(right.name)),
|
||||
workspaceNames: [...packages.keys()].sort(),
|
||||
foreignTargets: foreign.sort(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack the selected packages into one run directory and validate every archive
|
||||
* against the release family that owns it.
|
||||
* @param options - repository root, destination, selected members, and the public release index.
|
||||
* @returns Packed artifacts sorted by package name.
|
||||
*/
|
||||
export async function packBundleArtifacts(options: {
|
||||
readonly root: string
|
||||
readonly destination: string
|
||||
readonly members: readonly WorkspacePackage[]
|
||||
readonly release: ReadonlyMap<string, ReleaseEntry>
|
||||
readonly signal?: AbortSignal
|
||||
}): Promise<BundleArtifact[]> {
|
||||
const { root, destination, members, release } = options
|
||||
if (members.length === 0) throw new Error('no local packages were selected to pack')
|
||||
await mkdir(destination, { recursive: true })
|
||||
const invocation = pnpmInvocationFor([
|
||||
'pack', '--recursive', `--workspace-concurrency=${String(Math.min(4, members.length))}`,
|
||||
...members.map(member => `--filter=${member.name}`), '--pack-destination', destination,
|
||||
])
|
||||
let result
|
||||
try {
|
||||
result = await execa(invocation.command, invocation.args, {
|
||||
cwd: root, reject: false, killDescendants: true,
|
||||
...options.signal === undefined ? {} : { cancelSignal: options.signal },
|
||||
})
|
||||
} catch (error) {
|
||||
if (isCanceled(error)) throw new Error('packing was cancelled')
|
||||
throw error
|
||||
}
|
||||
if (result.isCanceled) throw new Error('packing was cancelled')
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`pnpm pack exited with ${String(result.exitCode ?? result.signal)}:\n${result.stdout}\n${result.stderr}`)
|
||||
}
|
||||
return readPackedArtifacts({ destination, members, release })
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a settled or thrown child-process outcome was a cancellation.
|
||||
* @param value - the settled execa result or the error it rejected with.
|
||||
* @returns True when an abort signal ended the child.
|
||||
*/
|
||||
function isCanceled(value: unknown): boolean {
|
||||
return typeof value === 'object' && value !== null && 'isCanceled' in value && value.isCanceled === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate already packed archives and describe them as servable artifacts.
|
||||
* @param options - run directory, the members that were packed into it, and the public release index.
|
||||
* @returns Packed artifacts sorted by package name.
|
||||
*/
|
||||
export function readPackedArtifacts(options: {
|
||||
readonly destination: string
|
||||
readonly members: readonly WorkspacePackage[]
|
||||
readonly release: ReadonlyMap<string, ReleaseEntry>
|
||||
}): BundleArtifact[] {
|
||||
const { destination, members, release } = options
|
||||
const packed = members.map((member) => {
|
||||
const entry = release.get(member.name)
|
||||
if (entry === undefined) {
|
||||
throw new Error(`${member.name}: not a public release member; the local registry does not republish a private or unreleased package`)
|
||||
}
|
||||
const file = tarballName(entry.member)
|
||||
const archive = join(destination, file)
|
||||
if (!existsSync(archive)) throw new Error(`${member.name} produced no tarball at ${archive}`)
|
||||
const contained = tarballFiles(archive)
|
||||
entry.family.validatePayload(entry.member, contained)
|
||||
const manifest = packedManifest(archive)
|
||||
entry.family.validatePackedManifest(entry.member, manifest)
|
||||
const main = manifest.main
|
||||
if (typeof main === 'string' && main !== '' && !contained.includes(`package/${payloadPath(main)}`)) {
|
||||
throw new Error(`${entry.member.name}: packed archive lacks its declared main ${main}; rebuild before serving`)
|
||||
}
|
||||
// A bundle whose patch is missing installs and then never loads its rows.
|
||||
for (const patch of declaredBundlePatches(manifest)) {
|
||||
if (!contained.includes(`package/${payloadPath(patch)}`)) {
|
||||
throw new Error(`${entry.member.name}: packed archive lacks its declared bundle patch ${patch}`)
|
||||
}
|
||||
}
|
||||
const bytes = readFileSync(archive)
|
||||
const distTag = entry.family.distTagForVersion(entry.member.version)
|
||||
return {
|
||||
name: entry.member.name,
|
||||
version: entry.member.version,
|
||||
archive,
|
||||
file,
|
||||
bytes: bytes.byteLength,
|
||||
integrity: `sha512-${createHash('sha512').update(bytes).digest('base64')}`,
|
||||
shasum: createHash('sha1').update(bytes).digest('hex'),
|
||||
manifest,
|
||||
platform: platformConstraint(manifest),
|
||||
...distTag === undefined ? {} : { distTag },
|
||||
packedAt: new Date().toISOString(),
|
||||
}
|
||||
})
|
||||
verifyPackedClosure(packed)
|
||||
return packed.sort((left, right) => left.name.localeCompare(right.name))
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose the record a completed run directory carries.
|
||||
* @param options - run identity, selection, artifacts, and source evidence.
|
||||
* @returns The manifest to write last.
|
||||
*/
|
||||
export function createRegistryManifest(options: {
|
||||
readonly buildId: string
|
||||
readonly source: RegistryManifest['source']
|
||||
readonly entries: readonly string[]
|
||||
readonly workspaceNames: readonly string[]
|
||||
readonly artifacts: readonly BundleArtifact[]
|
||||
}): RegistryManifest {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
buildId: options.buildId,
|
||||
source: options.source,
|
||||
entries: [...options.entries],
|
||||
workspaceNames: [...options.workspaceNames],
|
||||
packages: [...options.artifacts]
|
||||
.sort((left, right) => left.name.localeCompare(right.name))
|
||||
.map(artifact => ({
|
||||
name: artifact.name,
|
||||
version: artifact.version,
|
||||
file: artifact.file,
|
||||
bytes: artifact.bytes,
|
||||
integrity: artifact.integrity,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the completion marker into a run directory.
|
||||
* @param path - absolute manifest path inside the run directory.
|
||||
* @param manifest - the record to serialize.
|
||||
*/
|
||||
export async function writeRegistryManifest(path: string, manifest: RegistryManifest): Promise<void> {
|
||||
await writeFile(path, `${JSON.stringify(manifest, undefined, 2)}\n`, { mode: 0o600 })
|
||||
}
|
||||
|
||||
/**
|
||||
* Record which checkout produced a run.
|
||||
* @param root - repository root.
|
||||
* @param dshVersion - running DSH version the catalog installation target names.
|
||||
* @returns The commit, whether the tree was dirty, and the version served.
|
||||
*/
|
||||
export function sourceEvidence(root: string, dshVersion: string): RegistryManifest['source'] {
|
||||
return {
|
||||
commit: capture('git', ['rev-parse', 'HEAD'], { cwd: root }),
|
||||
dirty: capture('git', ['status', '--porcelain'], { cwd: root }) !== '',
|
||||
dshVersion,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the loopback registry for one run's artifacts.
|
||||
*
|
||||
* The listener binds `127.0.0.1` on an allocated port and answers under a fresh
|
||||
* namespace, so two runs never share a URL even when the port is reused.
|
||||
* @param options - the run's artifacts and directory, repository-owned names, and upstream registry.
|
||||
* @returns The registry URL and its close operation.
|
||||
*/
|
||||
export async function startBundleRegistry(options: {
|
||||
readonly directory: string
|
||||
readonly artifacts: readonly BundleArtifact[]
|
||||
readonly workspaceNames: readonly string[]
|
||||
readonly upstream?: string
|
||||
}): Promise<BundleRegistry> {
|
||||
if (options.artifacts.length === 0) throw new Error('the local registry was started without artifacts')
|
||||
const upstream = options.upstream ?? UPSTREAM_REGISTRY
|
||||
if (!upstream.endsWith('/')) throw new Error(`upstream registry must end with a slash: ${upstream}`)
|
||||
const index = new Map(options.artifacts.map(artifact => [artifact.name, artifact]))
|
||||
const owned = new Set(options.workspaceNames)
|
||||
const namespace = randomUUID()
|
||||
// Assigned once the listener reports its allocated port; requests cannot arrive before then.
|
||||
let origin = ''
|
||||
const server = createServer((request, response) => {
|
||||
const method = request.method ?? 'GET'
|
||||
const head = method === 'HEAD'
|
||||
if (method !== 'GET' && method !== 'HEAD') {
|
||||
respond(response, 405, { error: 'Method Not Allowed', reason: `${method} is not served` }, false)
|
||||
return
|
||||
}
|
||||
const raw = request.url ?? '/'
|
||||
const query = raw.indexOf('?')
|
||||
const path = query === -1 ? raw : raw.slice(0, query)
|
||||
const prefix = `/${namespace}/`
|
||||
if (!path.startsWith(prefix)) {
|
||||
respond(response, 404, { error: 'Not found', reason: 'request is outside this run\'s registry namespace' }, head)
|
||||
return
|
||||
}
|
||||
const suffix = path.slice(prefix.length)
|
||||
if (suffix.startsWith('tarballs/')) {
|
||||
serveTarball(suffix.slice('tarballs/'.length), response, head, options.directory, index)
|
||||
return
|
||||
}
|
||||
const requested = selection(suffix)
|
||||
if (requested === undefined) {
|
||||
respond(response, 404, { error: 'Not found', reason: 'malformed package path' }, head)
|
||||
return
|
||||
}
|
||||
const artifact = index.get(requested.name)
|
||||
if (artifact !== undefined) {
|
||||
serveMetadata(artifact, requested.version, response, head, origin)
|
||||
return
|
||||
}
|
||||
if (owned.has(requested.name)) {
|
||||
respond(response, 404, {
|
||||
error: 'Not found',
|
||||
reason: `${requested.name} is repository-owned but not part of this run's local closure; rebuild the registry after changing the catalog`,
|
||||
}, head)
|
||||
return
|
||||
}
|
||||
// Only package publication is local: third-party metadata and payloads keep npm's real bytes.
|
||||
response.writeHead(302, { location: `${upstream}${suffix}`, 'cache-control': 'no-store' })
|
||||
response.end()
|
||||
})
|
||||
|
||||
const listening = new Promise<void>((resolveListen, rejectListen) => {
|
||||
server.once('error', rejectListen)
|
||||
server.once('listening', () => {
|
||||
server.off('error', rejectListen)
|
||||
resolveListen()
|
||||
})
|
||||
})
|
||||
server.listen({ port: 0, host: '127.0.0.1' })
|
||||
await listening
|
||||
const address = server.address()
|
||||
if (address === null || typeof address === 'string') {
|
||||
await closeServer(server)
|
||||
throw new Error('the local registry has no TCP address')
|
||||
}
|
||||
origin = `http://127.0.0.1:${String(address.port)}/${namespace}/`
|
||||
let closing: Promise<void> | undefined
|
||||
return {
|
||||
url: origin,
|
||||
close: () => {
|
||||
closing ??= closeServer(server)
|
||||
return closing
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a JSON manifest from disk.
|
||||
* @param path - absolute `package.json` path.
|
||||
* @returns The parsed object.
|
||||
*/
|
||||
function manifestAt(path: string): Record<string, unknown> {
|
||||
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
|
||||
if (!isRecord(parsed)) throw new Error(`${path} is not a JSON object`)
|
||||
return parsed
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a parsed value is a JSON object.
|
||||
* @param value - value to inspect.
|
||||
* @returns True for a non-null, non-array object.
|
||||
*/
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read one dependency section as declared ranges.
|
||||
* @param manifest - package manifest.
|
||||
* @param section - section name.
|
||||
* @returns Name and range pairs, with malformed entries rejected.
|
||||
*/
|
||||
function dependencyRanges(manifest: Record<string, unknown>, section: string): [string, string][] {
|
||||
const value = manifest[section]
|
||||
if (value === undefined) return []
|
||||
if (!isRecord(value)) throw new Error(`${section} must be a mapping`)
|
||||
return Object.entries(value).map(([name, range]) => {
|
||||
if (typeof range !== 'string') throw new Error(`${section}.${name} must be a string range`)
|
||||
return [name, range]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Every package this manifest installs on a consumer: required, optional, and
|
||||
* required peers.
|
||||
* @param manifest - package manifest.
|
||||
* @returns Dependency names in section order.
|
||||
*/
|
||||
function installedEdges(manifest: Record<string, unknown>): string[] {
|
||||
const names = [...REQUIRED_SECTIONS, ...OPTIONAL_SECTIONS].flatMap(section => dependencyRanges(manifest, section).map(([name]) => name))
|
||||
for (const section of PEER_SECTIONS) {
|
||||
for (const [name] of dependencyRanges(manifest, section)) {
|
||||
if (!optionalPeer(manifest, name)) names.push(name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a package opts out of a peer it declares.
|
||||
* @param manifest - package manifest.
|
||||
* @param name - peer package name.
|
||||
* @returns True when the peer is optional.
|
||||
*/
|
||||
function optionalPeer(manifest: Record<string, unknown>, name: string): boolean {
|
||||
const metadata = manifest.peerDependenciesMeta
|
||||
if (!isRecord(metadata)) return false
|
||||
const entry = metadata[name]
|
||||
return isRecord(entry) && entry.optional === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether one constrained value accepts the host's own value.
|
||||
* @param constraint - accepted values, which may be only `!value` negations.
|
||||
* @param current - the host's value, or undefined when the field does not apply here.
|
||||
* @returns True when the host value is allowed.
|
||||
*/
|
||||
function allowsValue(constraint: readonly string[] | undefined, current: string | undefined): boolean {
|
||||
if (constraint === undefined || constraint.length === 0) return true
|
||||
// A field the host does not have (libc off Linux) constrains nothing here.
|
||||
if (current === undefined) return true
|
||||
const positives = constraint.filter(value => !value.startsWith('!'))
|
||||
if (positives.length > 0 && !positives.includes('any') && !positives.includes(current)) return false
|
||||
return !constraint.includes(`!${current}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe a platform constraint for a failure message.
|
||||
* @param constraint - declared or host constraints.
|
||||
* @returns A compact description.
|
||||
*/
|
||||
function describePlatform(constraint: PlatformConstraint): string {
|
||||
const parts = (['os', 'cpu', 'libc'] as const).flatMap(field => (constraint[field] ?? []).map(value => `${field}=${value}`))
|
||||
return parts.length === 0 ? 'any platform' : parts.join(' ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether any required edge reaches a package, ignoring optional and peer edges.
|
||||
* @param target - package to look for.
|
||||
* @param entries - catalog roots.
|
||||
* @param selected - the whole closure.
|
||||
* @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) {
|
||||
const name = pending[index] as string
|
||||
if (visited.has(name)) continue
|
||||
visited.add(name)
|
||||
const member = selected.get(name)
|
||||
if (member === undefined) continue
|
||||
for (const section of REQUIRED_SECTIONS) {
|
||||
for (const [dependency] of dependencyRanges(member.manifest, section)) {
|
||||
if (dependency === target) return true
|
||||
if (selected.has(dependency)) pending.push(dependency)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the dependency path that reached a package, for a failure message.
|
||||
* @param name - the package being explained.
|
||||
* @param parents - the first edge that reached each package.
|
||||
* @param entries - catalog roots, which have no parent.
|
||||
* @returns A `root -> ... -> name` chain.
|
||||
*/
|
||||
function dependencyPath(name: string, parents: ReadonlyMap<string, string>, entries: readonly string[]): string {
|
||||
const chain = [name]
|
||||
let current = parents.get(name)
|
||||
while (current !== undefined) {
|
||||
chain.unshift(current)
|
||||
current = parents.get(current)
|
||||
}
|
||||
return chain.length === 1 && entries.includes(name) ? `catalog entry ${name}` : chain.join(' -> ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a manifest path or tarball member to its package-relative form, the
|
||||
* same rule the publication payload check applies.
|
||||
* @param file - manifest path or archive member.
|
||||
* @returns The package-relative path.
|
||||
*/
|
||||
function payloadPath(file: string): string {
|
||||
return file.replaceAll('\\', '/').replace(/^\.\/+/u, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* The bundle patch files a manifest declares, as written.
|
||||
* @param manifest - packed or declared package manifest.
|
||||
* @returns Package-relative patch paths, or nothing when the package is not a bundle.
|
||||
*/
|
||||
function declaredBundlePatches(manifest: Record<string, unknown>): string[] {
|
||||
const dsh = manifest.dsh
|
||||
if (!isRecord(dsh) || !isRecord(dsh.bundle)) return []
|
||||
const declared = dsh.bundle.patch
|
||||
if (typeof declared === 'string') return [declared]
|
||||
return Array.isArray(declared) && declared.every(entry => typeof entry === 'string') ? declared : []
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject packed manifests that would send installation back to the checkout or
|
||||
* to a package this run does not serve.
|
||||
* @param artifacts - packed artifacts of one run.
|
||||
*/
|
||||
function verifyPackedClosure(artifacts: readonly BundleArtifact[]): void {
|
||||
const served = new Map(artifacts.map(artifact => [artifact.name, artifact]))
|
||||
for (const artifact of artifacts) {
|
||||
for (const section of [...REQUIRED_SECTIONS, ...OPTIONAL_SECTIONS, ...PEER_SECTIONS]) {
|
||||
for (const [name, range] of dependencyRanges(artifact.manifest, section)) {
|
||||
if (/^(?:workspace|file|link|portal):/u.test(range)) {
|
||||
throw new Error(`${artifact.name}: packed ${section}.${name} still declares ${range}`)
|
||||
}
|
||||
const local = served.get(name)
|
||||
if (local !== undefined && !semver.satisfies(local.version, range, { includePrerelease: true })) {
|
||||
throw new Error(`${artifact.name}: packed ${section}.${name} requires ${range}, but this run serves ${local.version}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The package a registry request path selects. */
|
||||
interface PackageRequest {
|
||||
/** Package name, with its scope when it has one. */
|
||||
readonly name: string
|
||||
/** Exact version or dist tag the path asks for, absent for a packument request. */
|
||||
readonly version?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a request path into the package name and version it selects.
|
||||
* @param suffix - the raw request path after the namespace.
|
||||
* @returns The selected package, or undefined when the path is not a package path.
|
||||
*/
|
||||
function selection(suffix: string): PackageRequest | undefined {
|
||||
const decoded = decodeSegment(suffix)
|
||||
if (decoded === undefined) return undefined
|
||||
const segments = decoded.split('/')
|
||||
// Reject traversal, registry administration paths, and empty segments before any name is considered.
|
||||
if (segments.some(segment => segment === '' || segment === '.' || segment === '..') || segments[0]?.startsWith('-') === true) {
|
||||
return undefined
|
||||
}
|
||||
if (segments.length === 1 && segments[0] !== undefined) return { name: segments[0] }
|
||||
if (!decoded.startsWith('@')) {
|
||||
const [name, version] = segments
|
||||
return name !== undefined && version !== undefined && segments.length === 2 ? { name, version } : undefined
|
||||
}
|
||||
if (segments.length === 2) return { name: decoded }
|
||||
if (segments.length === 3) {
|
||||
const version = segments[2]
|
||||
return version === undefined ? undefined : { name: segments.slice(0, 2).join('/'), version }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode one percent-encoded path.
|
||||
* @param value - raw path text.
|
||||
* @returns The decoded text, or undefined when the encoding is malformed.
|
||||
*/
|
||||
function decodeSegment(value: string): string | undefined {
|
||||
try {
|
||||
return decodeURIComponent(value)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve one archive's bytes from the artifact index.
|
||||
* @param file - raw requested tarball filename.
|
||||
* @param response - the response to write.
|
||||
* @param head - whether this is a HEAD request, whose body is omitted.
|
||||
* @param directory - the run directory holding the archives.
|
||||
* @param index - the run's artifact index.
|
||||
*/
|
||||
function serveTarball(
|
||||
file: string,
|
||||
response: ServerResponse,
|
||||
head: boolean,
|
||||
directory: string,
|
||||
index: ReadonlyMap<string, BundleArtifact>,
|
||||
): void {
|
||||
const decoded = decodeSegment(file)
|
||||
const artifact = decoded === undefined ? undefined : [...index.values()].find(entry => entry.file === decoded)
|
||||
if (artifact === undefined) {
|
||||
respond(response, 404, { error: 'Not found', reason: 'no archive with that name in this run' }, head)
|
||||
return
|
||||
}
|
||||
response.writeHead(200, {
|
||||
'content-type': 'application/octet-stream',
|
||||
'content-length': String(artifact.bytes),
|
||||
'cache-control': 'no-store',
|
||||
})
|
||||
if (head) {
|
||||
response.end()
|
||||
return
|
||||
}
|
||||
const stream = createReadStream(join(directory, artifact.file))
|
||||
// A client that disconnects mid-archive must not fail an unrelated request.
|
||||
response.on('close', () => stream.destroy())
|
||||
stream.on('error', (error) => { response.destroy(error) })
|
||||
stream.pipe(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve the packument, a version document, or a dist-tag document for one artifact.
|
||||
* @param artifact - the artifact being described.
|
||||
* @param version - requested version or dist tag, or undefined for the whole packument.
|
||||
* @param response - the response to write.
|
||||
* @param head - whether this is a HEAD request, whose body is omitted.
|
||||
* @param origin - the run's registry URL.
|
||||
*/
|
||||
function serveMetadata(
|
||||
artifact: BundleArtifact,
|
||||
version: string | undefined,
|
||||
response: ServerResponse,
|
||||
head: boolean,
|
||||
origin: string,
|
||||
): void {
|
||||
const tags: Record<string, string> = { latest: artifact.version }
|
||||
if (artifact.distTag !== undefined && artifact.distTag !== 'latest') tags[artifact.distTag] = artifact.version
|
||||
const document = {
|
||||
...artifact.manifest,
|
||||
dist: {
|
||||
tarball: `${origin}tarballs/${encodeURIComponent(artifact.file)}`,
|
||||
integrity: artifact.integrity,
|
||||
shasum: artifact.shasum,
|
||||
},
|
||||
}
|
||||
if (version === undefined) {
|
||||
respond(response, 200, {
|
||||
name: artifact.name,
|
||||
'dist-tags': tags,
|
||||
versions: { [artifact.version]: document },
|
||||
time: { created: artifact.packedAt, modified: artifact.packedAt, [artifact.version]: artifact.packedAt },
|
||||
}, head)
|
||||
return
|
||||
}
|
||||
if (version !== artifact.version && !Object.hasOwn(tags, version)) {
|
||||
respond(response, 404, {
|
||||
error: 'Not found',
|
||||
reason: `this run serves ${artifact.name}@${artifact.version} only`,
|
||||
}, head)
|
||||
return
|
||||
}
|
||||
respond(response, 200, document, head)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write one JSON response.
|
||||
* @param response - the response to write.
|
||||
* @param status - HTTP status.
|
||||
* @param body - JSON body.
|
||||
* @param head - whether this is a HEAD request, whose body is omitted.
|
||||
*/
|
||||
function respond(response: ServerResponse, status: number, body: unknown, head: boolean): void {
|
||||
const text = `${JSON.stringify(body)}\n`
|
||||
response.writeHead(status, {
|
||||
'content-type': 'application/json',
|
||||
'content-length': String(Buffer.byteLength(text)),
|
||||
'cache-control': 'no-store',
|
||||
})
|
||||
response.end(head ? undefined : text)
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a server and everything it is still serving.
|
||||
* @param server - the listener to close.
|
||||
* @returns Resolves once the listener is gone.
|
||||
*/
|
||||
function closeServer(server: Server): Promise<void> {
|
||||
return new Promise<void>((resolveClose, rejectClose) => {
|
||||
server.closeAllConnections()
|
||||
server.close((error) => {
|
||||
if (error === undefined) resolveClose()
|
||||
else rejectClose(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -8,9 +8,6 @@
|
||||
"apps/cli/tests/profiles/acp/tests/cleanup.ts": {
|
||||
"04bc8a44533574365dfedf563e2356458bcf95f990a00906938c977c1d683356": 1
|
||||
},
|
||||
"apps/cli/tests/profiles/headless/tests/ptc.e2e.ts": {
|
||||
"25e22fc52656ecfa64043e399e125dcb91feff05049727e056a46482a3d34867": 1
|
||||
},
|
||||
"apps/cli/tests/profiles/headless/tests/session-format-guard.expected.e2e.ts": {
|
||||
"340837865a9f6222ae46d41b594c4c079ae3935c004433c76da047df31c8008f": 1
|
||||
},
|
||||
@@ -125,9 +122,6 @@
|
||||
"packages/acp/acp/tests/approval.spec.ts": {
|
||||
"cfa5e037e20d61e7139f1482c8be264ec74720280b531c39791eca1947834b5c": 1
|
||||
},
|
||||
"packages/acp/acp/tests/bridge.spec.ts": {
|
||||
"a0a51a92f7a4db90baaf3a95b40a9816d4d628534f6fc97e766b8da3f1ea71d8": 1
|
||||
},
|
||||
"packages/acp/acp/tests/content.spec.ts": {
|
||||
"45e226235f6b865973de0f0fe92892cfb0007e0596ed4263015df7e4db1e578d": 1,
|
||||
"806c78568a3ca86f0863f896ddc033cb78cd927981b012dd1187e1069de3da50": 1,
|
||||
@@ -751,7 +745,6 @@
|
||||
},
|
||||
"packages/client/ui-tool/tests/image-card.client.spec.tsx": {
|
||||
"1a5ae4c2528dae3d43a3decbe30410647b6e60104341e070fd4de2c6a668a607": 1,
|
||||
"1d34efc5aadced56f018a7f1b453e5b2117ab13ab6e414a7bf32584a9b168af4": 1,
|
||||
"7266b2c682605ac318dad266d9371b6061a73746beac20f2fd94e9391eca5d4a": 1,
|
||||
"a4968fcd795d3444b0896dcd7e9688e84335d4fcc72e138c46f951d157a421c7": 1
|
||||
},
|
||||
@@ -784,20 +777,15 @@
|
||||
"189b21b2a9360e0384602b7a81c935ca9a419f9d97d526fb1f6684343486c4fe": 1,
|
||||
"24334994394c1d4336d192b2f38986beadcd0f6b235108370c3a736c34eebe98": 1,
|
||||
"33e462c9f8d9ef55f1b54a408f047caaa9467facc4fc88887fa5979fdce6c09f": 1,
|
||||
"3e385a12e8e6c592fbcc51a8e897d2ad4f21ab7a0c8ef00727e1b37b52af2c16": 1,
|
||||
"455072902745ebd120bdeaaebf1b00b2d747a300409066715375263bcc4e8a63": 1,
|
||||
"4df26af834cb4f8d55bb6908736ddeb6971b4c4c1cbd5a9ace2f9cfe9c0b94e4": 1,
|
||||
"6078d7e1390a0c3c94892edd74526a76e6cbbda8360656bf9f97d367371c0c4c": 1,
|
||||
"71b9c7a18658556ab36c9af394f29dac5885fab402304b0b6f4d5127d6acdeff": 1,
|
||||
"8bd3bc4fed398e1420c3ff656c7f9847a9b5aba1cf8b653623907922004ff038": 1,
|
||||
"9117562f15005ef8ec18dbad9c29cdc723c4af2c6a9c217af3cd7d8380de999b": 1,
|
||||
"98169406d3c872458f70f3f8f70abdd6eeaca296816ce91cdcf8392cfa6a77bf": 1,
|
||||
"afecf8092cd8de095ffbe00a1df00816897e8205854fd33a9d2beeaee88eca95": 1,
|
||||
"b32e3f8c876615b2607309ee288de85817f0dd0480c4e16dfb3695fb84aa2674": 1,
|
||||
"b389d62887b2c8991189e944ab6e6da49be6e907e3d85417926cd578cc212f73": 1,
|
||||
"b8af8a60d1b9ee562bc77cc2f504fac357dd8b8ea37aea670d768d6a5b4ae285": 1,
|
||||
"bc448993c30c4cc92c43cc077a2b00d033ef01a64427c609a8d89b0ba257dfcd": 1,
|
||||
"c0d6774c76d86ae3c7881f2292fc41f3fb30e74ebd41a8130a3000c0466bc2b5": 1,
|
||||
"e888d93b9171a1022992aecebca9c67f8b564ae541bbf0f9129eda3ea423b95c": 1,
|
||||
"efed1d00d0f288a0c2716c88ee57ba53ace8f627abba4bd2c20de596755b5bad": 1,
|
||||
"f37cbef85628f8df72f3d455868ac0715105f463a9bd858f10e2cb5f984baefb": 1
|
||||
@@ -904,16 +892,9 @@
|
||||
"336d4996145aa5694f6c8fd39fee7d248ba667330de106330224e92fd4511554": 1,
|
||||
"6061de0a1e01547eb1abf9d4e1b5fa79fda112fa35dda980775491123413dcce": 1
|
||||
},
|
||||
"packages/core/agent/tests/invariant.spec.ts": {
|
||||
"2ed6179a261a6cd1d9e84d8e8c5858eef36330853f2af62ea4222e00a9e578a3": 1
|
||||
},
|
||||
"packages/core/scope/src/index.ts": {
|
||||
"e590ead7cd68f7ec90a646dc2622e3631bf63ee3e7a04cf4c39a4f1b07184865": 1
|
||||
},
|
||||
"packages/core/scope/tests/invariant.spec.ts": {
|
||||
"001bfd2c7b05e2c76752d6895be67c0640b43aed6b8c9725c7321f02db6de745": 1,
|
||||
"6cd200a3be79594ca783f345b6047758c19a4c2d744dbf4564ef78ccba0433ca": 1
|
||||
},
|
||||
"packages/core/scope/tests/store.spec.ts": {
|
||||
"612bd4047fa63f957ecb33d7fc7544c489301c3c0592e9a42dafe2cb85311165": 1
|
||||
},
|
||||
@@ -1180,7 +1161,7 @@
|
||||
},
|
||||
"packages/experimental/inspector/tests/plugin.client.spec.ts": {
|
||||
"148509a0193d8d3c16a606a15f5145770f454c2ebc70dd0ccaa51fc1a0f43e99": 1,
|
||||
"2066ad746457f15b6737d62124c4ec81f2140fc3fa425187eac8e3b1c3b6ab0b": 9
|
||||
"2066ad746457f15b6737d62124c4ec81f2140fc3fa425187eac8e3b1c3b6ab0b": 8
|
||||
},
|
||||
"packages/experimental/ptc-runtime-python/src/index.ts": {
|
||||
"b121cee14635b00f7891217013e4682493cf96f676710be60722fdf8b862aea3": 1
|
||||
@@ -1295,18 +1276,18 @@
|
||||
"packages/goal/goal/src/index.ts": {
|
||||
"09a26f55e58db3477446e73f59bd3cdf678ae76ff8f54d185ec4a0ce0dc7da2d": 1
|
||||
},
|
||||
"packages/hooks/hook-protocol/tests/events.spec.ts": {
|
||||
"packages/experimental/hook-protocol/tests/events.spec.ts": {
|
||||
"a59fe57e5f3994018a6bd25208b9c2f56a7bcf7353d6ec2d453564f3f5c225f2": 1
|
||||
},
|
||||
"packages/hooks/hooks-claude-code/tests/bridge.spec.ts": {
|
||||
"packages/experimental/hooks-claude-code/tests/bridge.spec.ts": {
|
||||
"38a7cf3dfd5da2f739390f5b233b97731f3bfa1ac3a51fdb8e1594735764bb80": 1
|
||||
},
|
||||
"packages/hooks/hooks-claude-code/tests/coverage-cases.ts": {
|
||||
"packages/experimental/hooks-claude-code/tests/coverage-cases.ts": {
|
||||
"38a7cf3dfd5da2f739390f5b233b97731f3bfa1ac3a51fdb8e1594735764bb80": 1,
|
||||
"54f541ed10b92b1386306754b4a96741f6d9e40058ddac191f5aed03af30c32a": 1,
|
||||
"593ac2fabae51a87153bf285d326f2de6ddb84bf2581f2ac280083b9bc130188": 1
|
||||
},
|
||||
"packages/hooks/hooks-codex/tests/bridge.spec.ts": {
|
||||
"packages/experimental/hooks-codex/tests/bridge.spec.ts": {
|
||||
"bced8a97654435422589a4bdd1b46b5147d4120cd5b496dcf055c453494ce0ab": 1
|
||||
},
|
||||
"packages/host/directory-picker-auto/tests/loader-composition.spec.ts": {
|
||||
@@ -1450,9 +1431,6 @@
|
||||
"packages/plan/plan-mode/src/index.ts": {
|
||||
"1422d291f0118635ac06f265a9018dd86dbbd4a97f76a7ed5abb5c2a42e81b53": 1
|
||||
},
|
||||
"packages/plan/plan-mode/tests/invariant.spec.ts": {
|
||||
"6f54ffd70daee0f9ad7b6d42e14b6aae5af1ec5a39004d541ca224facc2ad576": 1
|
||||
},
|
||||
"packages/plan/plan-mode/tests/plan-mode.spec.ts": {
|
||||
"036770a58292b891357228338d9876329b657defb6371ecb0b7133fc117771ce": 1,
|
||||
"0ca57693819b64476ce22f00311089435afd085d4a38cfff61852dbf3cfa38c3": 1,
|
||||
@@ -1545,12 +1523,11 @@
|
||||
"71fa45c63f1d2dc12e6a4da1120a9ca227ef1b1ffbce510cd0d8847d85f278c0": 9,
|
||||
"80fa2ee81263d11fc1fafe6b7dbb608e8ed6e7c7e8e7fa3e726b600e82425294": 1,
|
||||
"82025e3228e5dab025f1c3434c0303baf066b7f40f30352764abc7374a693928": 1,
|
||||
"9234f1edeb197a3cc5e423e04ea3b9d79da97e1f231db0bca529810ab3e0d100": 1,
|
||||
"926edbfe0199937da6ec9d119cfd16f6cbc674a242de8f433980da6e6ea8b13b": 1,
|
||||
"9b52ca4ca1a86eb1d34576ca3c210d2e09677553e2a0b0e505675782034c147e": 1,
|
||||
"9c3213cd8f09df05b9aceba2033b68f6b61c1abe2231167622ef3fa462d77424": 1,
|
||||
"a03ef245e286d905868294bee2e81ac282cfa0a5161a1edd6d8b0aa167932fb6": 1,
|
||||
"ae0162b189937f84fb9a162eabbc7496f7e196c00e960aef4fe8e5b3e4b87d8d": 4,
|
||||
"ae0162b189937f84fb9a162eabbc7496f7e196c00e960aef4fe8e5b3e4b87d8d": 3,
|
||||
"d54d74835af1518a4723a7cc803efc0d4e8e337ebb80b813b6ef76bdb813be17": 1
|
||||
},
|
||||
"packages/session-query/session-log-export/tests/archive.host.spec.ts": {
|
||||
@@ -1606,10 +1583,10 @@
|
||||
"09a269bed9b7eb21c5d84c1826987ceed50b4d193728e8cd1d01015088762f08": 1,
|
||||
"4cb55fef1f53567526f153fbdf22f46c3e83bb3f6be77d5b42baddbae0b07344": 1
|
||||
},
|
||||
"packages/session-query/tool-session-query/tests/sqlite-integration.spec.ts": {
|
||||
"packages/experimental/tool-session-query/tests/sqlite-integration.spec.ts": {
|
||||
"a66cea6887fa5e33f4d504a7899fcb4b99d79958f6aed01288af1ad6ff557298": 1
|
||||
},
|
||||
"packages/session-query/tool-session-query/tests/tool-session-query.spec.ts": {
|
||||
"packages/experimental/tool-session-query/tests/tool-session-query.spec.ts": {
|
||||
"0ec8a91117e78ae941807094fc63035e115a3e02412917e632be4f34dbfddc62": 1,
|
||||
"6370792878103b8fe2963dfeb036963572c1da2bb6dd7fcc10681be1c866b490": 1,
|
||||
"a66cea6887fa5e33f4d504a7899fcb4b99d79958f6aed01288af1ad6ff557298": 1
|
||||
@@ -1675,9 +1652,6 @@
|
||||
"packages/session/session-log-deepseek/tests/feedback-composition.spec.ts": {
|
||||
"59c63f734e8bba862325f776e3fcbebd78a84cfb4161f8df41c216950d9bf351": 1
|
||||
},
|
||||
"packages/session/session-log-deepseek/tests/invariant.spec.ts": {
|
||||
"7f99ec5e224c4475fc9ce0069040eeef36add5dd27d0941e74a814786ad90062": 1
|
||||
},
|
||||
"packages/session/session-log-deepseek/tests/upload.spec.ts": {
|
||||
"0c25d15bf9489ae7483b0a31a64a047c12174602071e0aa29714e0f9196889c6": 1,
|
||||
"119461c1a63f392b4db54f75a86f852f49b72b7f0342c04d1bc1c8d1677f37c1": 3,
|
||||
@@ -1721,9 +1695,6 @@
|
||||
"packages/session/session-persistence-jsonl/tests/lease.spec.ts": {
|
||||
"94e6c4d15a19729cf60005d29c0985aa4e5bd984b92f40c2b5d8e996bb651997": 1
|
||||
},
|
||||
"packages/session/session-persistence-jsonl/tests/migration-verifier.spec.ts": {
|
||||
"6bad3f6cf7fc26c871be029dc2a4f12d01ce7a49f314b0d728453e7575af28d1": 1
|
||||
},
|
||||
"packages/session/session-persistence-jsonl/tests/native-source-admission.spec.ts": {
|
||||
"24b3f41708c436a3b161317d9b04439dff484aa8f5a913ed3c68a299f490e3b9": 1
|
||||
},
|
||||
@@ -1845,34 +1816,21 @@
|
||||
"packages/storage/storage-domain/src/index.ts": {
|
||||
"0fae329186de631f08edbf22f01cbfbf15d7c1ea8dce64e4cefa8ad0a8d43dea": 1
|
||||
},
|
||||
"packages/storage/storage-domain/tests/invariant.spec.ts": {
|
||||
"9199dc978ec64150a1a706ca2e3e5a39925f3d6011c6c0b5453c4e41b89cd896": 1
|
||||
},
|
||||
"packages/subagent/subagent-acp/tests/subagent-acp.e2e.ts": {
|
||||
"daa15206e19d6a8fbae646d678889b2e6933ece824e48dd06c95d2096861d5c1": 1
|
||||
},
|
||||
"packages/subagent/subagent-acp/tests/subagent-acp.spec.ts": {
|
||||
"3b530b6b250a0ea0e48b5802ae6ad5cf361cd7ead3046d9e5d09087abd735fce": 1,
|
||||
"7f8d86b28f8d58460ec0a1efc0dc3b2333a3dce0bddbfb5461761258795bb87f": 1,
|
||||
"b1eb8cf60e80ab0aaed7dae202d2a8c485b23d5797bf9162209d666182d6c703": 1,
|
||||
"bf51b161c3c9ca3603b93db55b2c5721bab63a993fef808f2e6cdd619aacc190": 1,
|
||||
"daa15206e19d6a8fbae646d678889b2e6933ece824e48dd06c95d2096861d5c1": 2,
|
||||
"ea9ae2bf5224204cbf222eccd26cd385ed84a3843e48c47452597d36a616ae65": 1,
|
||||
"f77e5cbd2d2d911087f7faecc4b0b5bffdfb44ca5d0bcf7823ba9777feca7799": 2
|
||||
},
|
||||
"packages/subagent/subagent-claude-code/tests/real-deepseek.e2e.ts": {
|
||||
"dee4487a97778a2383f1b378d59b3dae098152273d956dc360f23c69680fe0ea": 1
|
||||
},
|
||||
"packages/subagent/subagent-claude-code/tests/real-product.spec.ts": {
|
||||
"03847806b1cfd410cc85f1e90d671aac43e667c5c41e5792c729fd4ff655fb6b": 1,
|
||||
"656905a0db6cd229fa72610416ef7ba9d2d320c8c99352bd0d205dee4e13bd8d": 1,
|
||||
"adda026f7ad49b47496f381a0c04997300597d040b8a81045a848573485ed299": 1
|
||||
},
|
||||
"packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts": {
|
||||
"101af668daf0fb9a7df21757c781ed58f7cf6770a1f5021a5da4ce58080c1f5f": 1,
|
||||
"2cedee48f4fb8ce4bbd10567fdfbee69e8dd11dbc85af9b824de33c2b77abfbf": 1,
|
||||
"4e94c461ab08f535e20f71ebe8bb9a8c911a936c7068f24e259956c06d17c05d": 2,
|
||||
"aa1a9402165afaeb270967390b13cae2a59916bc52f13b4f600315d7a0fe2032": 1,
|
||||
"4e94c461ab08f535e20f71ebe8bb9a8c911a936c7068f24e259956c06d17c05d": 1,
|
||||
"cdcbd6cc73a09b6613864cdfb5b14d0cf66dc2484d26916cb32929deca1032de": 3,
|
||||
"f85c96616fff1bb1e21886bd2fad64e9e34d89d090ab96ab7804f702278cd23d": 4,
|
||||
"fa05a5e504ed74b5a07cb6281c787b507eb6c44db768803f4dad034dd69cb3b9": 1
|
||||
@@ -1886,28 +1844,12 @@
|
||||
"adda026f7ad49b47496f381a0c04997300597d040b8a81045a848573485ed299": 1
|
||||
},
|
||||
"packages/subagent/subagent-codex/tests/subagent-codex.spec.ts": {
|
||||
"101af668daf0fb9a7df21757c781ed58f7cf6770a1f5021a5da4ce58080c1f5f": 1,
|
||||
"aa1a9402165afaeb270967390b13cae2a59916bc52f13b4f600315d7a0fe2032": 1,
|
||||
"fa05a5e504ed74b5a07cb6281c787b507eb6c44db768803f4dad034dd69cb3b9": 1
|
||||
},
|
||||
"packages/subagent/subagent-dsh-sdk/tests/subagent-dsh-sdk.spec.ts": {
|
||||
"daa15206e19d6a8fbae646d678889b2e6933ece824e48dd06c95d2096861d5c1": 2,
|
||||
"f77e5cbd2d2d911087f7faecc4b0b5bffdfb44ca5d0bcf7823ba9777feca7799": 1
|
||||
},
|
||||
"packages/subagent/subagent-in-process-driver/src/structured.ts": {
|
||||
"2994ddac72096564a65b1d11cfe50c5b5f21de0cca775ed7b758d031b9a4df99": 1
|
||||
},
|
||||
"packages/subagent/subagent-in-process-driver/tests/structured.spec.ts": {
|
||||
"647bbba1ee35ea6acd23faa1e2988e24c49f0284be571ab3e716ab36a11cc38c": 1,
|
||||
"fd4628b951a78392c07e54a33588263377c3c85c90dc33ce0b7f78e904b1c2ad": 1
|
||||
},
|
||||
"packages/subagent/subagent-in-process-driver/tests/subagent-in-process-driver.spec.ts": {
|
||||
"5bc99a74ad50605215a43105e8b5d19ab8126149a99bcafcb6cf20c9992874d4": 1,
|
||||
"69d9dd6917cd45e70ef0ed273bea2e4a01fef7cb808458a7f0606b90f260cb2b": 1,
|
||||
"79d2d214c7600ab9023c27b97ec626b9e3ce33f0b42f3f6233dfd142387fbdcf": 1,
|
||||
"996a2d673209758e45b46af235339d8dccc9d20cfea97ea34f536d3ed8842812": 1,
|
||||
"b50b6842b3565ea7f1441afb3214ab0ccf027df0e44bb485005935849c46f8ea": 1
|
||||
},
|
||||
"packages/subagent/subagent/src/catalog.ts": {
|
||||
"3e704e331ed2fc5d4c0855e2455b31b82924c525f2e267416e393cdbd36c251b": 1,
|
||||
"fdc5fcade0ba1abbc426ccccb07a1ad4998253799b8561815047d95178441cdf": 1
|
||||
@@ -1927,11 +1869,6 @@
|
||||
"packages/subagent/subagent/tests/catalog.spec.ts": {
|
||||
"7e2e5f940dc4eaeefee0650619f7a736c5bead03fb248b2394c1a29a3328b105": 1
|
||||
},
|
||||
"packages/subagent/subagent/tests/continuation-internals.ts": {
|
||||
"3745fdae681f5fd4fa6825b3987604ceed308de0ef68181e9b3b8405a288b1b0": 1,
|
||||
"ae5776af8c1e9965a4eca94bcbc07b65da8d2e9e24e4570d2141b34d60b4f90f": 1,
|
||||
"dacc9babb26371c6c047d8a504e4c7da0e2032f83620ab63ec667276284d1c35": 1
|
||||
},
|
||||
"packages/subagent/subagent/tests/continuation.spec.ts": {
|
||||
"ae5776af8c1e9965a4eca94bcbc07b65da8d2e9e24e4570d2141b34d60b4f90f": 1
|
||||
},
|
||||
@@ -1945,12 +1882,8 @@
|
||||
"e770821c1f637aca0a346d5f6e6f0873ca4d7e3b070b557cddf773c160ec599a": 1
|
||||
},
|
||||
"packages/subagent/tool-subagent/src/index.ts": {
|
||||
"11233be4a60a9dfb99acc5fbf90c5185c2101516379f3224866557301bbb048c": 1,
|
||||
"7fe58280287ff63d40e979e72329f975e11015a95e9a8eec207a5a78748d3c89": 4
|
||||
},
|
||||
"packages/subagent/tool-subagent/tests/harness.ts": {
|
||||
"1a2098d6ff345b66d13f9822385af407ac03acdabeb607c2bd18a75987c410a5": 1
|
||||
},
|
||||
"packages/subagent/tool-subagent/tests/model-selection.spec.ts": {
|
||||
"9a362ded6cf17e795dc9cb0c88a672eb17c03d90ae87987f53dbb3a3f6bcc467": 1
|
||||
},
|
||||
@@ -1958,10 +1891,7 @@
|
||||
"e770821c1f637aca0a346d5f6e6f0873ca4d7e3b070b557cddf773c160ec599a": 1
|
||||
},
|
||||
"packages/subagent/tool-subagent/tests/tool-subagent.spec.ts": {
|
||||
"65e1dff6ee6fedd8c89582d5197e791126c328ff859ce4509a69619afbc4c63f": 1,
|
||||
"719322ba287998bcfb906150f393012229c1ec3e4b59bd106469319ba402a09a": 1,
|
||||
"a5bde0aa0b7d2193204df547d0a9b7e9e116afead259c1914dc93486a8af4f0a": 1,
|
||||
"b6277d8ca00f4653c46b62f27a1743fdbb00465b2aaa4687ea0cb4119adee4e3": 1
|
||||
"719322ba287998bcfb906150f393012229c1ec3e4b59bd106469319ba402a09a": 1
|
||||
},
|
||||
"packages/subprocess/subprocess-local/tests/linux-scope.spec.ts": {
|
||||
"8497f236582c842e59edf49dce625d59d16a828048731d7c2b9625bb1270f32f": 1
|
||||
@@ -2047,7 +1977,7 @@
|
||||
"237785adfc64f6fe0cf84ce292d1da4b2065b11f82093029312356e10a3703f0": 6,
|
||||
"e87443b2dcbfcd641eba56e2afb1b9daaa697f89df846c40cdea6bfb9b8a7ef3": 2
|
||||
},
|
||||
"packages/terminal/tool-terminal/tests/loader-composition.spec.ts": {
|
||||
"packages/experimental/tool-terminal/tests/loader-composition.spec.ts": {
|
||||
"59c63f734e8bba862325f776e3fcbebd78a84cfb4161f8df41c216950d9bf351": 1
|
||||
},
|
||||
"packages/test-support/client-runtime/src/assembly/modules.ts": {
|
||||
@@ -2172,37 +2102,34 @@
|
||||
"598014dcfe77101781e3c6a02709097e3b0151a335e909c44719fac08fdea433": 2,
|
||||
"ed2fec09418bc1a78fde5097fd01d5aabf903166ed1bf480a5e38bccdd3948a6": 1
|
||||
},
|
||||
"packages/webhook/webhook-github/tests/body.spec.ts": {
|
||||
"packages/experimental/webhook-github/tests/body.spec.ts": {
|
||||
"130dbbbe723b0d72d5a736596b3513f42b65a6e3909164b05bce810393f36b96": 1
|
||||
},
|
||||
"packages/webhook/webhook-github/tests/handler.spec.ts": {
|
||||
"packages/experimental/webhook-github/tests/handler.spec.ts": {
|
||||
"171a57417a610abfca8b6ef7c8a8bfe71a2c5cc27f6a52158bb9de709d51c234": 1,
|
||||
"981ca4f4ab5668c2c8d0d0bddf9bff66535bbdd27c8bb47816a83e610b60432f": 1,
|
||||
"e60b9922c7a65986547062e8ce78ac09d83b0c0ac9533030deed3a596d3067b3": 2,
|
||||
"f4fa3c346edf323b9d6e18fa1fc720d828de054c14daf95822308634e0e7fe15": 1
|
||||
},
|
||||
"packages/webhook/webhook-github/tests/loader-composition.spec.ts": {
|
||||
"packages/experimental/webhook-github/tests/loader-composition.spec.ts": {
|
||||
"59c63f734e8bba862325f776e3fcbebd78a84cfb4161f8df41c216950d9bf351": 1
|
||||
},
|
||||
"packages/webhook/webhook/tests/loader-composition.spec.ts": {
|
||||
"packages/experimental/webhook/tests/loader-composition.spec.ts": {
|
||||
"59c63f734e8bba862325f776e3fcbebd78a84cfb4161f8df41c216950d9bf351": 1
|
||||
},
|
||||
"packages/webhook/webhook/tests/session.spec.ts": {
|
||||
"packages/experimental/webhook/tests/session.spec.ts": {
|
||||
"febbe0687d7079e44a15b802235157043bad5a5f188348db89fe00f50532bee8": 1
|
||||
},
|
||||
"packages/workflow/tool-ralph/src/index.ts": {
|
||||
"packages/experimental/tool-ralph/src/index.ts": {
|
||||
"6fe9024d30e0d4757a348b83d246925b3f5aeaf2e79a0eff4bf12f3c9fb5826b": 1,
|
||||
"cec7714d93bd00a87b261199e97562ce99a73ae4b4e2414728d874d24bfed7a4": 1
|
||||
},
|
||||
"packages/workflow/tool-ralph/tests/tool-ralph.spec.ts": {
|
||||
"packages/experimental/tool-ralph/tests/tool-ralph.spec.ts": {
|
||||
"47df8a1ea0e482122f196eb20bb3ae93a50e59d7c80d822c5e8a29efd1c05eb8": 1
|
||||
},
|
||||
"packages/workflow/tool-workflow/tests/tool-workflow.spec.ts": {
|
||||
"061944ebda38f8da144d88cfa668e6336efc68c33441a9fe3215d8c001124526": 2
|
||||
},
|
||||
"packages/workflow/workflow-ptc/tests/setup.ts": {
|
||||
"eedb5ebd5377314cae568128ab901b78a063216d34f322aa8ad8a33d9e14df54": 1
|
||||
},
|
||||
"packages/workflow/workflow-ptc/tests/workflow-ptc.spec.ts": {
|
||||
"3ddf655a4c4522404be3c2758f0511c7605887c72d9d91cb4ad521433cbdb0b4": 1,
|
||||
"f382d714ca5cc4bc76a5e36d3e1e99c535590cce3ca1b04e5374315e9d8f51df": 1
|
||||
@@ -2240,9 +2167,6 @@
|
||||
"63a77f914e85aa8d63dd649be95c82f64756e06e8faa0a9aa3b53d14daebd2e5": 1,
|
||||
"9cf0e19dc31ae2d244530fb4853d2877c0490271a5f930ad330e210051b41cd8": 1
|
||||
},
|
||||
"scripts/test-invariants.spec.ts": {
|
||||
"1d25e54371f9bac32d21541bfe094eb3eb3bfc39e010982c9f25bb8b0f3bea14": 1
|
||||
},
|
||||
"snapshots/sdk/sdk.snapshot.ts": {
|
||||
"782d52268c631008e0fec08f76f87751765690287d5acdfae45021a754900290": 1
|
||||
},
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
/** The delivered Web composition's Schedule rows, the optional bundles it ships switched off, and their display metadata. */
|
||||
|
||||
import { globSync, readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { globSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it, onTestFinished } from 'vitest'
|
||||
import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import { loadOverlayPatches } from '../packages/boot/app-boot/src/index.ts'
|
||||
import { readPluginMeta } from '../packages/boot/app-boot/src/package-meta.ts'
|
||||
import { OPTIONAL_BUNDLES, bundlePatchPaths, composeEntries } from '../packages/boot/app-boot/src/profile.ts'
|
||||
import { OPTIONAL_BUNDLES, PROFILE_TEMPLATES, bundlePatchPaths, composeEntries, initProfile, readProfileManifest } from '../packages/boot/app-boot/src/profile.ts'
|
||||
import type { DshBundleManifest } from '../packages/util/package-manifest/src/types.ts'
|
||||
import { createPluginProfile } from '../apps/desktop/src/project-manager.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
@@ -27,6 +30,89 @@ function bundle(name: string): { dir: string; patches: ReturnType<typeof loadOve
|
||||
return { dir: entry.dir, patches: bundlePatchPaths(entry.dir, entry.manifest.dsh.bundle).flatMap(path => loadOverlayPatches('test', path)) }
|
||||
}
|
||||
|
||||
const lightweight = [
|
||||
'@deepseek-ai/dsh-experimental-session-search',
|
||||
'@deepseek-ai/dsh-experimental-ralph-bundle',
|
||||
'@deepseek-ai/dsh-experimental-terminal-bundle',
|
||||
'@deepseek-ai/dsh-experimental-badge-skill-bundle',
|
||||
'@deepseek-ai/dsh-experimental-session-titles-bundle',
|
||||
]
|
||||
|
||||
function presetRows(entries: EntryOptions[], id: string): EntryOptions[] {
|
||||
const preset = entries.find(entry => entry.id === id)
|
||||
if (preset === undefined) throw new Error(`missing preset ${id}`)
|
||||
return (preset.config as { plugins: EntryOptions[] }).plugins
|
||||
}
|
||||
|
||||
function flattenRows(entries: EntryOptions[]): EntryOptions[] {
|
||||
return entries.flatMap(entry => [entry, ...entry.group && Array.isArray(entry.config) ? flattenRows(entry.config as EntryOptions[]) : []])
|
||||
}
|
||||
|
||||
function profileLayers(surface: 'web' | 'desktop'): ReturnType<typeof loadOverlayPatches>[] {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'dsh-bundle-compatibility-'))
|
||||
onTestFinished(() => { rmSync(directory, { recursive: true, force: true }) })
|
||||
if (surface === 'desktop') createPluginProfile(directory)
|
||||
else initProfile(directory, PROFILE_TEMPLATES.web!.bundles)
|
||||
const names = readProfileManifest('test', directory).dsh?.profile?.bundles
|
||||
if (names === undefined) throw new Error(`${surface} profile has no bundle list`)
|
||||
return names.map(name => bundle(name).patches)
|
||||
}
|
||||
|
||||
describe.each(['web', 'desktop'] as const)('%s optional bundle overrides', (surface) => {
|
||||
const selectedNames = ['@deepseek-ai/dsh-experimental-badge-skill-bundle', '@deepseek-ai/dsh-experimental-ralph-bundle']
|
||||
|
||||
it('keeps established selectors for later configuration and disabled overrides', () => {
|
||||
const shipped = profileLayers(surface)
|
||||
const config = { subagentProvider: 'spawn', maxRounds: 3 }
|
||||
const warnings: string[] = []
|
||||
const entries = composeEntries([...shipped, ...selectedNames.map(name => bundle(name).patches), [
|
||||
{ id: 'skill-badge', disabled: true },
|
||||
...['preset-standard', 'preset-cordis', 'preset-ptc'].map(preset => ({ preset, id: 'tool-ralph', config, disabled: true })),
|
||||
]], warning => warnings.push(warning))
|
||||
expect(warnings).toEqual([])
|
||||
expect(entries.find(row => row.id === 'skill-badge')).toMatchObject({ name: '@deepseek-ai/dsh-skill-badge', disabled: true })
|
||||
for (const preset of ['preset-standard', 'preset-cordis', 'preset-ptc']) {
|
||||
const roots = presetRows(entries, preset)
|
||||
expect(flattenRows(roots).find(row => row.id === 'tool-ralph'))
|
||||
.toMatchObject({ name: '@deepseek-ai/dsh-tool-ralph', config, disabled: true })
|
||||
expect(roots.find(row => row.id === 'optional-ralph')?.isolate).toEqual({ workflowEngine: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps a later complete preset replacement and its manually declared disabled tool', () => {
|
||||
const shipped = profileLayers(surface)
|
||||
const baseline = composeEntries(shipped)
|
||||
const preset = baseline.find(row => row.id === 'preset-standard')!
|
||||
const config = { ...preset.config as object, plugins: [...presetRows(baseline, 'preset-standard'), {
|
||||
id: 'tool-ralph', name: '@deepseek-ai/dsh-tool-ralph', disabled: true,
|
||||
config: { subagentProvider: 'spawn', maxRounds: 2 },
|
||||
}] }
|
||||
const warnings: string[] = []
|
||||
const entries = composeEntries([...shipped, ...selectedNames.map(name => bundle(name).patches), [
|
||||
{ id: 'preset-standard', config },
|
||||
]], warning => warnings.push(warning))
|
||||
expect(warnings).toEqual([])
|
||||
expect(entries.find(row => row.id === 'preset-standard')?.config).toEqual(config)
|
||||
expect(presetRows(entries, 'preset-standard').some(row => row.id === 'optional-ralph')).toBe(false)
|
||||
const inherited = flattenRows(presetRows(entries, 'preset-cordis')).find(row => row.id === 'tool-ralph')
|
||||
expect(inherited).toMatchObject({ name: '@deepseek-ai/dsh-tool-ralph' })
|
||||
expect(inherited?.disabled).not.toBe(true)
|
||||
})
|
||||
|
||||
it('does not select absent bundles through enable-only row overrides', () => {
|
||||
const warnings: string[] = []
|
||||
const entries = composeEntries([...profileLayers(surface), [
|
||||
{ id: 'skill-badge', disabled: false },
|
||||
{ preset: 'preset-standard', id: 'tool-ralph', disabled: false },
|
||||
]], warning => warnings.push(warning))
|
||||
expect(warnings).toHaveLength(2)
|
||||
expect(warnings.some(warning => warning.includes('skill-badge'))).toBe(true)
|
||||
expect(warnings.some(warning => warning.includes('tool-ralph'))).toBe(true)
|
||||
expect(entries.some(row => row.id === 'skill-badge')).toBe(false)
|
||||
expect(flattenRows(presetRows(entries, 'preset-standard')).some(row => row.id === 'tool-ralph')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('optional bundles', () => {
|
||||
const shipped = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'].map(name => bundle(name).patches)
|
||||
|
||||
@@ -42,12 +128,15 @@ describe('optional bundles', () => {
|
||||
const { patches } = bundle(name)
|
||||
const warnings: string[] = []
|
||||
const composed = composeEntries([...shipped, patches], message => warnings.push(message))
|
||||
const ids = new Set(composed.map(entry => entry.id))
|
||||
expect(warnings).toEqual([])
|
||||
// Inserted rows carry stable ids at the profile root, so a later profile patch can configure or disable them.
|
||||
for (const row of patches.flatMap(patch => patch.insert ?? [])) {
|
||||
expect(typeof row.id).toBe('string')
|
||||
expect(ids.has(row.id)).toBe(true)
|
||||
// Inserted ids remain addressable inside their selected profile or preset tree.
|
||||
for (const patch of patches) {
|
||||
const rows = typeof patch.preset === 'string' ? presetRows(composed, patch.preset) : composed
|
||||
const ids = new Set(flattenRows(rows).map(entry => entry.id))
|
||||
for (const row of patch.insert ?? []) {
|
||||
expect(typeof row.id).toBe('string')
|
||||
expect(ids.has(row.id)).toBe(true)
|
||||
}
|
||||
}
|
||||
// One top-level row per id: a duplicate declaration leaves the Loader with the last one, silently
|
||||
// replacing the layer that declared the id first.
|
||||
@@ -57,13 +146,75 @@ describe('optional bundles', () => {
|
||||
// row, and the override keeps the package the shipped layer declared on it.
|
||||
const shippedComposed = composeEntries([...shipped])
|
||||
for (const patch of patches) {
|
||||
if (patch.insert !== undefined || typeof patch.id !== 'string') continue
|
||||
if (patch.insert !== undefined || typeof patch.id !== 'string' || patch.preset !== undefined) continue
|
||||
const matches = composed.filter(entry => entry.id === patch.id)
|
||||
expect(matches).toHaveLength(1)
|
||||
expect(matches[0]?.name).toBe(shippedComposed.find(entry => entry.id === patch.id)?.name)
|
||||
}
|
||||
})
|
||||
|
||||
it('offers Git Worktrees as one disabled bundle with its runtime and tool', () => {
|
||||
const name = '@deepseek-ai/dsh-experimental-tool-worktree'
|
||||
expect(OPTIONAL_BUNDLES).toContain(name)
|
||||
for (const template of Object.values(PROFILE_TEMPLATES)) expect(template.bundles).not.toContain(name)
|
||||
const rows = composeEntries([...shipped, bundle(name).patches])
|
||||
for (const moduleName of [name, '@deepseek-ai/dsh-experimental-worktree']) {
|
||||
expect(composeEntries(shipped).some(row => row.name === moduleName)).toBe(false)
|
||||
expect(rows.filter(row => row.name === moduleName && row.disabled !== true)).toHaveLength(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('ships all five lightweight bundles without selecting them in a default template', () => {
|
||||
const defaults = Object.values(PROFILE_TEMPLATES).flatMap(template => template.bundles)
|
||||
for (const name of lightweight) {
|
||||
expect(OPTIONAL_BUNDLES).toContain(name)
|
||||
expect(defaults).not.toContain(name)
|
||||
}
|
||||
})
|
||||
|
||||
it('composes every lightweight selection without changing minimal or adding host tools', () => {
|
||||
const baseline = composeEntries(shipped)
|
||||
const layers = lightweight.map(name => bundle(name).patches)
|
||||
for (let mask = 0; mask < 2 ** lightweight.length; mask += 1) {
|
||||
const selected = layers.filter((_layer, index) => (mask & 2 ** index) !== 0)
|
||||
const warnings: string[] = []
|
||||
const composed = composeEntries([...shipped, ...selected], warning => warnings.push(warning))
|
||||
expect(warnings).toEqual([])
|
||||
expect(presetRows(composed, 'preset-minimal')).toEqual(presetRows(baseline, 'preset-minimal'))
|
||||
const addedHost = composed.filter(row => !baseline.some(existing => existing.id === row.id))
|
||||
expect(addedHost.every(row => ['skill-badge', 'optional-session-title-all-prompts'].includes(row.id ?? ''))).toBe(true)
|
||||
for (const id of ['preset-standard', 'preset-cordis', 'preset-ptc']) {
|
||||
const rows = flattenRows(presetRows(composed, id))
|
||||
const ids = rows.map(row => row.id)
|
||||
expect(new Set(ids).size).toBe(ids.length)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('contributes tools inside all full presets with independently isolated services', () => {
|
||||
const composed = composeEntries([...shipped, ...lightweight.map(name => bundle(name).patches)])
|
||||
for (const id of ['preset-standard', 'preset-cordis', 'preset-ptc']) {
|
||||
const roots = presetRows(composed, id)
|
||||
const names = flattenRows(roots).map(row => row.name)
|
||||
for (const name of [
|
||||
'@deepseek-ai/dsh-tool-session-query',
|
||||
'@deepseek-ai/dsh-tool-ralph', '@deepseek-ai/dsh-tool-terminal',
|
||||
]) expect(names).toContain(name)
|
||||
expect(roots.find(row => row.id === 'optional-ralph')?.isolate).toEqual({ workflowEngine: true })
|
||||
expect(roots.find(row => row.id === 'optional-persistent-terminals')?.isolate).toEqual({ terminals: true })
|
||||
const search = roots.find(row => row.id === 'optional-session-search')
|
||||
expect(search?.isolate).toEqual({ sessionQuery: true })
|
||||
expect(search?.config).toEqual([
|
||||
{ id: 'optional-session-query-sqlite', name: '@deepseek-ai/dsh-session-query-sqlite', config: { path: ':memory:', openAt: 'first-search' } },
|
||||
{ id: 'optional-tool-session-query', name: '@deepseek-ai/dsh-tool-session-query' },
|
||||
])
|
||||
}
|
||||
expect(composed.find(row => row.id === 'session-query-sqlite')?.config).toEqual({ path: ':memory:', openAt: 'never' })
|
||||
expect(composed.find(row => row.id === 'session-title-llm')?.disabled).toBe(true)
|
||||
expect(composed.find(row => row.id === 'optional-session-title-all-prompts')?.name)
|
||||
.toBe('@deepseek-ai/dsh-session-title-all-prompts-llm')
|
||||
})
|
||||
|
||||
it('delivers the Schedule service and task page without a Host clock row', () => {
|
||||
const composed = composeEntries(shipped)
|
||||
// The delivered composition carries the Host Schedule service and its task
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Process-spawning cases set Vitest's case timeout to 90 seconds, including
|
||||
* plain local runs without a CI override. Configuration-only cases keep the default.
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { existsSync } from 'node:fs'
|
||||
|
||||
@@ -15,6 +15,11 @@ const HOST_DEPENDENCY_PACKAGES: readonly string[] = [
|
||||
'@deepseek-ai/dsh-session',
|
||||
]
|
||||
|
||||
/** Required Cordis services whose Host imports are type-only. */
|
||||
const REQUIRED_SERVICE_PEERS = {
|
||||
'@deepseek-ai/dsh-api-terminal-controller': ['@deepseek-ai/dsh-subprocess'],
|
||||
} as const satisfies Readonly<Record<string, readonly string[]>>
|
||||
|
||||
/** Development-only package relationships not represented by source imports. */
|
||||
const CONFIGURATION_ONLY_DEV_DEPENDENCIES = {
|
||||
'@deepseek-ai/dsh-client-locale': ['@deepseek-ai/dsh-api-remotes'],
|
||||
@@ -57,7 +62,6 @@ const SAFE_HOST_DEPENDENCY_EXPORTS = {
|
||||
/** Runtime exports that require every consumer to resolve the provider's shared peer instance. */
|
||||
const PEER_REQUIRED_HOST_EXPORTS = {
|
||||
'@deepseek-ai/dsh-client-connection': ['OperatorPeer'],
|
||||
'@deepseek-ai/dsh-subprocess': ['SubprocessExecutableNotFoundError'],
|
||||
'@deepseek-ai/dsh-scope': ['carrierKeyOf', 'createScope', 'scopeOf', 'scopeTarget'],
|
||||
'@deepseek-ai/dsh-session': ['SESSION_FORMAT_VERSION'],
|
||||
'@deepseek-ai/dsh-session-persistence': ['SessionPersistenceNotFoundError'],
|
||||
@@ -71,6 +75,7 @@ export interface PackageDependencyPolicy {
|
||||
readonly clientFaceInclude: readonly string[]
|
||||
readonly clientFaceExclude: readonly string[]
|
||||
readonly hostPackages: readonly string[]
|
||||
readonly requiredServicePeers?: Readonly<Record<string, readonly string[]>>
|
||||
readonly configurationOnlyDevDependencies: Readonly<Record<string, readonly string[]>>
|
||||
readonly duplicateSafePackages?: readonly string[]
|
||||
readonly safeHostDependencyExports: HostDependencyExports
|
||||
@@ -82,6 +87,7 @@ export const PACKAGE_DEPENDENCY_POLICY: PackageDependencyPolicy = {
|
||||
clientFaceInclude: CLIENT_FACE_INCLUDE,
|
||||
clientFaceExclude: CLIENT_FACE_EXCLUDE,
|
||||
hostPackages: HOST_DEPENDENCY_PACKAGES,
|
||||
requiredServicePeers: REQUIRED_SERVICE_PEERS,
|
||||
configurationOnlyDevDependencies: CONFIGURATION_ONLY_DEV_DEPENDENCIES,
|
||||
duplicateSafePackages: DUPLICATE_SAFE_PACKAGES,
|
||||
safeHostDependencyExports: SAFE_HOST_DEPENDENCY_EXPORTS,
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
collectPackageInvariantViolations,
|
||||
packageInvariantOwners,
|
||||
} from './package-invariants.ts'
|
||||
import { usesFlattenedPackageDependencies } from './package-dependency-policy.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function handwrittenInvariant(packageName: string): string {
|
||||
return `
|
||||
export const name = 'probe-invariant'
|
||||
export const inject = ['invariants']
|
||||
const install = (ctx: { on(name: string, listener: (value: number) => void): void }, fail: (message: string) => never) => {
|
||||
ctx.on('probe/value', (value) => {
|
||||
if (value < 0) fail('observed values must be non-negative')
|
||||
})
|
||||
}
|
||||
export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) =>
|
||||
Promise.resolve(ctx.invariants.register(${JSON.stringify(packageName)}, install))
|
||||
`
|
||||
}
|
||||
|
||||
function fixture(options: {
|
||||
companion?: boolean
|
||||
packageName?: string
|
||||
packageDirectory?: string
|
||||
source?: string
|
||||
clientDeclaration?: boolean
|
||||
clientExport?: boolean
|
||||
invariantExport?: boolean
|
||||
invariantFile?: boolean
|
||||
invariantDependency?: boolean
|
||||
invariantRange?: string
|
||||
invariantReference?: boolean
|
||||
buildEntry?: boolean
|
||||
omissionReason?: boolean
|
||||
} = {}): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-package-invariants-'))
|
||||
roots.push(root)
|
||||
const packageDirectory = options.packageDirectory ?? 'packages/core/probe'
|
||||
const dir = join(root, packageDirectory)
|
||||
mkdirSync(join(dir, 'src'), { recursive: true })
|
||||
const packageName = options.packageName ?? '@deepseek-ai/dsh-probe'
|
||||
const companion = options.companion ?? true
|
||||
const invariantExport = options.invariantExport ?? companion
|
||||
const invariantFile = options.invariantFile ?? companion
|
||||
const invariantDependency = options.invariantDependency ?? companion
|
||||
const invariantReference = options.invariantReference ?? companion
|
||||
const buildEntry = options.buildEntry ?? companion
|
||||
const exports = {
|
||||
...(invariantExport ? { './invariant': {
|
||||
types: './lib/types/invariant.d.ts',
|
||||
default: './lib/invariant.js',
|
||||
} } : {}),
|
||||
...(options.clientExport === true ? {
|
||||
'./client': {
|
||||
types: './lib/types/client/index.d.ts',
|
||||
default: './lib/client.js',
|
||||
},
|
||||
} : {}),
|
||||
}
|
||||
const dsh = options.clientDeclaration === true ? { client: {} } : undefined
|
||||
const developmentOnlyInvariant = usesFlattenedPackageDependencies(
|
||||
`${packageDirectory}/package.json`,
|
||||
packageName,
|
||||
dsh,
|
||||
)
|
||||
const manifest = {
|
||||
name: packageName,
|
||||
...(dsh === undefined ? {} : { dsh }),
|
||||
exports,
|
||||
files: ['lib/index.js', ...invariantFile ? ['lib/invariant.js'] : []],
|
||||
peerDependencies: !invariantDependency || developmentOnlyInvariant ? {} : {
|
||||
'@deepseek-ai/dsh-invariants': options.invariantRange ?? 'workspace:*',
|
||||
},
|
||||
devDependencies: !invariantDependency ? {} : {
|
||||
'@deepseek-ai/dsh-invariants': options.invariantRange ?? 'workspace:*',
|
||||
},
|
||||
}
|
||||
writeFileSync(join(dir, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`)
|
||||
writeFileSync(join(dir, 'tsconfig.json'), `${JSON.stringify({
|
||||
references: invariantReference ? [{ path: '../../runtime-diagnostics/invariants' }] : [],
|
||||
}, null, 2)}\n`)
|
||||
if (companion) {
|
||||
writeFileSync(join(dir, 'src/invariant.ts'), options.source ?? handwrittenInvariant(packageName))
|
||||
}
|
||||
writeFileSync(
|
||||
join(dir, 'tsdown.config.ts'),
|
||||
buildEntry ? "export default { entry: ['lib/types/index.js', 'lib/types/invariant.js'] }\n" : "export default { entry: ['lib/types/index.js'] }\n",
|
||||
)
|
||||
writeFileSync(
|
||||
join(dir, 'README.md'),
|
||||
options.omissionReason === false ? '# Probe\n' : '# Probe\n\nNo runtime invariant companion is published because this fixture owns no diverging observations.\n',
|
||||
)
|
||||
return root
|
||||
}
|
||||
|
||||
describe('package invariant gate', () => {
|
||||
it('accepts a hand-owned checking companion with publication metadata', () => {
|
||||
expect(collectPackageInvariantViolations(fixture())).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects caret ranges for the invariant peer and development dependency', () => {
|
||||
expect(collectPackageInvariantViolations(fixture({ invariantRange: 'workspace:^' }))).toEqual([
|
||||
{ path: 'packages/core/probe/package.json', message: '@deepseek-ai/dsh-invariants must be a workspace:* peerDependency' },
|
||||
{ path: 'packages/core/probe/package.json', message: '@deepseek-ai/dsh-invariants must be a workspace:* devDependency' },
|
||||
])
|
||||
})
|
||||
|
||||
it('accepts a package that cleanly omits an invariant companion', () => {
|
||||
const root = fixture({ companion: false })
|
||||
expect(collectPackageInvariantViolations(root)).toEqual([])
|
||||
expect(packageInvariantOwners(root)).toEqual([])
|
||||
})
|
||||
|
||||
it('requires an omitted companion to have a README reason sentence', () => {
|
||||
const violations = collectPackageInvariantViolations(fixture({
|
||||
companion: false,
|
||||
omissionReason: false,
|
||||
}))
|
||||
expect(violations).toContainEqual({
|
||||
path: 'packages/core/probe/README.md',
|
||||
message: 'omitted companion requires a README "No ... companion is published" reason sentence',
|
||||
})
|
||||
})
|
||||
|
||||
it('accepts development-only invariants for configured Host dependencies', () => {
|
||||
expect(collectPackageInvariantViolations(fixture({ packageName: '@deepseek-ai/dsh-llm' }))).toEqual([])
|
||||
})
|
||||
|
||||
it('accepts development-only invariants for client packages', () => {
|
||||
expect(collectPackageInvariantViolations(fixture({
|
||||
packageName: '@deepseek-ai/dsh-client-probe',
|
||||
packageDirectory: 'packages/client/probe',
|
||||
}))).toEqual([])
|
||||
})
|
||||
|
||||
it('accepts development-only invariants for packages with a dsh.client entry', () => {
|
||||
expect(collectPackageInvariantViolations(fixture({ clientDeclaration: true, clientExport: true }))).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps invariant peers for packages that only export a Client API', () => {
|
||||
expect(collectPackageInvariantViolations(fixture({ clientExport: true }))).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps invariant peers for experimental packages with a dsh.client entry', () => {
|
||||
expect(collectPackageInvariantViolations(fixture({
|
||||
packageDirectory: 'packages/experimental/probe',
|
||||
clientDeclaration: true,
|
||||
clientExport: true,
|
||||
}))).toEqual([])
|
||||
})
|
||||
|
||||
it('accepts an invariant reference owned by a package-local leaf project', () => {
|
||||
const root = fixture({ invariantReference: false })
|
||||
const dir = join(root, 'packages/core/probe')
|
||||
writeFileSync(join(dir, 'tsconfig.json'), `${JSON.stringify({
|
||||
files: [],
|
||||
references: [{ path: './tsconfig.host.json' }],
|
||||
}, null, 2)}\n`)
|
||||
writeFileSync(join(dir, 'tsconfig.host.json'), `${JSON.stringify({
|
||||
references: [{ path: '../../runtime-diagnostics/invariants' }],
|
||||
}, null, 2)}\n`)
|
||||
|
||||
expect(collectPackageInvariantViolations(root)).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects missing publication metadata and build output', () => {
|
||||
const violations = collectPackageInvariantViolations(fixture({
|
||||
invariantExport: false,
|
||||
invariantFile: false,
|
||||
invariantDependency: false,
|
||||
invariantReference: false,
|
||||
buildEntry: false,
|
||||
}))
|
||||
expect(violations.map(violation => violation.message)).toEqual(expect.arrayContaining([
|
||||
expect.stringContaining('exports["./invariant"]'),
|
||||
expect.stringContaining('peerDependency'),
|
||||
expect.stringContaining('devDependency'),
|
||||
expect.stringContaining('TypeScript project references'),
|
||||
expect.stringContaining('must bundle lib/types/invariant.js'),
|
||||
]))
|
||||
})
|
||||
|
||||
it('rejects publication and build wiring left behind after omission', () => {
|
||||
const violations = collectPackageInvariantViolations(fixture({
|
||||
companion: false,
|
||||
invariantExport: true,
|
||||
invariantFile: true,
|
||||
invariantReference: true,
|
||||
buildEntry: true,
|
||||
}))
|
||||
expect(violations.map(violation => violation.message)).toEqual(expect.arrayContaining([
|
||||
expect.stringContaining('exports["./invariant"] must be omitted'),
|
||||
expect.stringContaining('files must omit lib/invariant.js'),
|
||||
expect.stringContaining('TypeScript project references must omit ../../runtime-diagnostics/invariants'),
|
||||
expect.stringContaining('build override must omit lib/types/invariant.js'),
|
||||
]))
|
||||
})
|
||||
|
||||
it('rejects foreign, duplicate, and unresolved registrations', () => {
|
||||
const source = `
|
||||
export const name = 'probe-invariant'
|
||||
export const inject = ['invariants']
|
||||
const selected = process.env.PACKAGE_NAME
|
||||
const install = (_ctx: unknown, fail: (message: string) => never) => { fail('probe') }
|
||||
export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) => {
|
||||
ctx.invariants.register('@deepseek-ai/dsh-foreign', install)
|
||||
return ctx.invariants.register(selected!, install)
|
||||
}
|
||||
`
|
||||
const violations = collectPackageInvariantViolations(fixture({ source }))
|
||||
expect(violations.map(violation => violation.message)).toEqual(expect.arrayContaining([
|
||||
expect.stringContaining('must resolve to a local string constant'),
|
||||
expect.stringContaining('must register exactly its own package name'),
|
||||
]))
|
||||
})
|
||||
|
||||
it('rejects generated markers and reporter-free executable installers', () => {
|
||||
const generated = fixture({
|
||||
source: `/** @generated */\n${handwrittenInvariant('@deepseek-ai/dsh-probe')}`,
|
||||
})
|
||||
expect(collectPackageInvariantViolations(generated).map(violation => violation.message))
|
||||
.toContain('invariant companions must be hand-owned and may not carry @generated markers')
|
||||
|
||||
const reporterFree = fixture({
|
||||
source: `
|
||||
export const name = 'probe-invariant'
|
||||
export const inject = ['invariants']
|
||||
const install = () => { void 0 }
|
||||
export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) =>
|
||||
Promise.resolve(ctx.invariants.register('@deepseek-ai/dsh-probe', install))
|
||||
`,
|
||||
})
|
||||
expect(collectPackageInvariantViolations(reporterFree).map(violation => violation.message))
|
||||
.toContain('install function must accept the bound failure reporter as its second parameter')
|
||||
|
||||
const unused = fixture({
|
||||
source: `
|
||||
export const name = 'probe-invariant'
|
||||
export const inject = ['invariants']
|
||||
const install = (_ctx: unknown, _fail: (message: string) => never) => { void 0 }
|
||||
export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) =>
|
||||
Promise.resolve(ctx.invariants.register('@deepseek-ai/dsh-probe', install))
|
||||
`,
|
||||
})
|
||||
expect(collectPackageInvariantViolations(unused).map(violation => violation.message))
|
||||
.toContain('install function must use its bound failure reporter')
|
||||
})
|
||||
|
||||
it('rejects registering a different installer than the checked local function', () => {
|
||||
const decoy = fixture({
|
||||
source: `
|
||||
export const name = 'probe-invariant'
|
||||
export const inject = ['invariants']
|
||||
const install = (_ctx: unknown, fail: (message: string) => never) => { fail('checked decoy') }
|
||||
export const apply = (ctx: { invariants: { register(name: string, install: () => void): () => void } }) =>
|
||||
ctx.invariants.register('@deepseek-ai/dsh-probe', () => {})
|
||||
`,
|
||||
})
|
||||
expect(collectPackageInvariantViolations(decoy).map(violation => violation.message))
|
||||
.toContain('line 6: ctx.invariants.register must use the checked local install function')
|
||||
})
|
||||
|
||||
it.each([
|
||||
'export default { name, inject, apply }',
|
||||
"export * as default from './probe.ts'",
|
||||
])('rejects a default export that would collapse the Loader namespace', (defaultExport) => {
|
||||
const source = `${handwrittenInvariant('@deepseek-ai/dsh-probe')}\n${defaultExport}\n`
|
||||
expect(collectPackageInvariantViolations(fixture({ source })).map(violation => violation.message))
|
||||
.toContain('must not default-export; Loader must retain the companion namespace')
|
||||
})
|
||||
|
||||
it('rejects empty installers because packages without a check omit the companion', () => {
|
||||
const source = `
|
||||
export const name = 'probe-invariant'
|
||||
export const inject = ['invariants']
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-probe'
|
||||
const install = () => {}
|
||||
export const apply = (ctx: { invariants: { register(name: string, install: () => void): () => void } }) =>
|
||||
ctx.invariants.register(PACKAGE_NAME, install)
|
||||
`
|
||||
expect(collectPackageInvariantViolations(fixture({ source })).map(violation => violation.message))
|
||||
.toContain('empty install function is unnecessary; omit the companion and its publication wiring')
|
||||
})
|
||||
})
|
||||
@@ -1,418 +0,0 @@
|
||||
/**
|
||||
* Package-invariant companion discovery and structural checks.
|
||||
* The runtime registry stays product-independent; this gate keeps each
|
||||
* published companion complete without requiring synthetic empty companions.
|
||||
*/
|
||||
|
||||
import { existsSync, globSync, readFileSync } from 'node:fs'
|
||||
import { dirname, relative, resolve, sep } from 'node:path'
|
||||
import ts from 'typescript'
|
||||
import { usesFlattenedPackageDependencies } from './package-dependency-policy.ts'
|
||||
|
||||
/** Package README sentence that records why an invariant companion is omitted. */
|
||||
const OMITTED_COMPANION_REASON = /No (?:(?:runtime )?invariant )?companion is published(?: because|[.:;—])\s+\S/i
|
||||
|
||||
interface PackageManifest {
|
||||
name?: string
|
||||
dsh?: unknown
|
||||
exports?: Record<string, { types?: string; default?: string } | string | null | undefined>
|
||||
files?: string[]
|
||||
peerDependencies?: Record<string, string>
|
||||
devDependencies?: Record<string, string>
|
||||
}
|
||||
|
||||
/** One package and the files participating in its invariant publication rules. */
|
||||
export interface PackageInvariantOwner {
|
||||
readonly dir: string
|
||||
readonly manifestPath: string
|
||||
readonly sourcePath: string
|
||||
readonly packageName: string
|
||||
}
|
||||
|
||||
/** One gate violation with a repo-relative owner path. */
|
||||
export interface PackageInvariantViolation {
|
||||
readonly path: string
|
||||
readonly message: string
|
||||
}
|
||||
|
||||
/** Discover packages that own an invariant companion. */
|
||||
export function packageInvariantOwners(root: string): PackageInvariantOwner[] {
|
||||
return packageInvariantPackages(root)
|
||||
.filter(owner => existsSync(resolve(root, owner.sourcePath)))
|
||||
}
|
||||
|
||||
/** Discover every package under the repository package tree. */
|
||||
function packageInvariantPackages(root: string): PackageInvariantOwner[] {
|
||||
return globSync('packages/*/*/package.json', { cwd: root })
|
||||
.map(path => path.split(sep).join('/'))
|
||||
.sort()
|
||||
.map((manifestPath) => {
|
||||
const manifest = readManifest(resolve(root, manifestPath))
|
||||
if (manifest.name === undefined || manifest.name === '') {
|
||||
throw new Error(`${manifestPath}: package invariant owner must declare a package name`)
|
||||
}
|
||||
const dir = dirname(manifestPath)
|
||||
return {
|
||||
dir,
|
||||
manifestPath,
|
||||
sourcePath: `${dir}/src/invariant.ts`,
|
||||
packageName: manifest.name,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Return all violations of the package-invariant companion rules. */
|
||||
export function collectPackageInvariantViolations(root: string): PackageInvariantViolation[] {
|
||||
const violations: PackageInvariantViolation[] = []
|
||||
for (const owner of packageInvariantPackages(root)) {
|
||||
const manifest = readManifest(resolve(root, owner.manifestPath))
|
||||
const hasCompanion = existsSync(resolve(root, owner.sourcePath))
|
||||
checkManifest(owner, manifest, hasCompanion, violations)
|
||||
checkBuild(owner, root, hasCompanion, violations)
|
||||
if (hasCompanion) {
|
||||
checkSource(owner, root, violations)
|
||||
} else {
|
||||
checkOmissionReason(owner, root, violations)
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
function readManifest(path: string): PackageManifest {
|
||||
return JSON.parse(readFileSync(path, 'utf8')) as PackageManifest
|
||||
}
|
||||
|
||||
function addViolation(
|
||||
violations: PackageInvariantViolation[],
|
||||
path: string,
|
||||
message: string,
|
||||
): void {
|
||||
violations.push({ path, message })
|
||||
}
|
||||
|
||||
function checkManifest(
|
||||
owner: PackageInvariantOwner,
|
||||
manifest: PackageManifest,
|
||||
hasCompanion: boolean,
|
||||
violations: PackageInvariantViolation[],
|
||||
): void {
|
||||
const invariantExport = manifest.exports?.['./invariant']
|
||||
if (!hasCompanion) {
|
||||
if (invariantExport !== undefined) {
|
||||
addViolation(
|
||||
violations,
|
||||
owner.manifestPath,
|
||||
'exports["./invariant"] must be omitted when src/invariant.ts is absent',
|
||||
)
|
||||
}
|
||||
if (manifest.files?.includes('lib/invariant.js')) {
|
||||
addViolation(
|
||||
violations,
|
||||
owner.manifestPath,
|
||||
'files must omit lib/invariant.js when src/invariant.ts is absent',
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (typeof invariantExport !== 'object'
|
||||
|| invariantExport === null
|
||||
|| invariantExport.types !== './lib/types/invariant.d.ts'
|
||||
|| invariantExport.default !== './lib/invariant.js') {
|
||||
addViolation(
|
||||
violations,
|
||||
owner.manifestPath,
|
||||
'exports["./invariant"] must target ./lib/types/invariant.d.ts and ./lib/invariant.js',
|
||||
)
|
||||
}
|
||||
if (!manifest.files?.includes('lib/invariant.js')) {
|
||||
addViolation(violations, owner.manifestPath, 'files must publish lib/invariant.js')
|
||||
}
|
||||
if (owner.packageName === '@deepseek-ai/dsh-invariants') return
|
||||
const developmentOnlyInvariant = usesFlattenedPackageDependencies(
|
||||
owner.manifestPath,
|
||||
owner.packageName,
|
||||
manifest.dsh,
|
||||
)
|
||||
const expectedRange = 'workspace:*'
|
||||
const peerRange = manifest.peerDependencies?.['@deepseek-ai/dsh-invariants']
|
||||
if (developmentOnlyInvariant ? peerRange !== undefined : peerRange !== expectedRange) {
|
||||
addViolation(violations, owner.manifestPath, developmentOnlyInvariant
|
||||
? '@deepseek-ai/dsh-invariants must not be a peerDependency under this package dependency policy'
|
||||
: '@deepseek-ai/dsh-invariants must be a workspace:* peerDependency')
|
||||
}
|
||||
if (manifest.devDependencies?.['@deepseek-ai/dsh-invariants'] !== expectedRange) {
|
||||
addViolation(
|
||||
violations,
|
||||
owner.manifestPath,
|
||||
`@deepseek-ai/dsh-invariants must be a ${expectedRange} devDependency`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function checkBuild(
|
||||
owner: PackageInvariantOwner,
|
||||
root: string,
|
||||
hasCompanion: boolean,
|
||||
violations: PackageInvariantViolation[],
|
||||
): void {
|
||||
const tsconfigPath = `${owner.dir}/tsconfig.json`
|
||||
if (hasCompanion
|
||||
&& owner.packageName !== '@deepseek-ai/dsh-invariants'
|
||||
&& !projectReferencesInvariants(root, owner.dir, tsconfigPath)) {
|
||||
addViolation(
|
||||
violations,
|
||||
tsconfigPath,
|
||||
'TypeScript project references must include ../../runtime-diagnostics/invariants',
|
||||
)
|
||||
} else if (!hasCompanion && projectReferencesInvariants(root, owner.dir, tsconfigPath)) {
|
||||
addViolation(
|
||||
violations,
|
||||
tsconfigPath,
|
||||
'TypeScript project references must omit ../../runtime-diagnostics/invariants when src/invariant.ts is absent',
|
||||
)
|
||||
}
|
||||
|
||||
const configPath = `${owner.dir}/tsdown.config.ts`
|
||||
if (!existsSync(resolve(root, configPath))) return
|
||||
const source = readFileSync(resolve(root, configPath), 'utf8')
|
||||
const bundlesCompanion = source.includes('lib/types/invariant.js')
|
||||
if (hasCompanion && !bundlesCompanion) {
|
||||
addViolation(violations, configPath, 'package build override must bundle lib/types/invariant.js')
|
||||
} else if (!hasCompanion && bundlesCompanion) {
|
||||
addViolation(
|
||||
violations,
|
||||
configPath,
|
||||
'package build override must omit lib/types/invariant.js when src/invariant.ts is absent',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function checkOmissionReason(
|
||||
owner: PackageInvariantOwner,
|
||||
root: string,
|
||||
violations: PackageInvariantViolation[],
|
||||
): void {
|
||||
const readmePath = `${owner.dir}/README.md`
|
||||
const absolutePath = resolve(root, readmePath)
|
||||
if (!existsSync(absolutePath) || !OMITTED_COMPANION_REASON.test(readFileSync(absolutePath, 'utf8'))) {
|
||||
addViolation(
|
||||
violations,
|
||||
readmePath,
|
||||
'omitted companion requires a README "No ... companion is published" reason sentence',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function projectReferencesInvariants(root: string, ownerDir: string, entryPath: string): boolean {
|
||||
const ownerRoot = resolve(root, ownerDir)
|
||||
const target = resolve(root, 'packages/runtime-diagnostics/invariants')
|
||||
const pending = [resolve(root, entryPath)]
|
||||
const visited = new Set<string>()
|
||||
while (pending.length > 0) {
|
||||
const configPath = pending.pop()
|
||||
if (configPath === undefined) break
|
||||
if (visited.has(configPath)) continue
|
||||
visited.add(configPath)
|
||||
const config = JSON.parse(readFileSync(configPath, 'utf8')) as {
|
||||
references?: Array<{ path?: string }>
|
||||
}
|
||||
for (const reference of config.references ?? []) {
|
||||
if (reference.path === undefined) continue
|
||||
const referenced = resolve(dirname(configPath), reference.path)
|
||||
if (referenced === target) return true
|
||||
if (!referenced.startsWith(`${ownerRoot}${sep}`)) continue
|
||||
const childConfig = referenced.endsWith('.json') ? referenced : resolve(referenced, 'tsconfig.json')
|
||||
if (existsSync(childConfig)) pending.push(childConfig)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function checkSource(
|
||||
owner: PackageInvariantOwner,
|
||||
root: string,
|
||||
violations: PackageInvariantViolation[],
|
||||
): void {
|
||||
const absolutePath = resolve(root, owner.sourcePath)
|
||||
const sourceText = readFileSync(absolutePath, 'utf8')
|
||||
if (sourceText.includes('@generated')) {
|
||||
addViolation(
|
||||
violations,
|
||||
owner.sourcePath,
|
||||
'invariant companions must be hand-owned and may not carry @generated markers',
|
||||
)
|
||||
}
|
||||
|
||||
const sourceFile = ts.createSourceFile(
|
||||
absolutePath,
|
||||
sourceText,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.TS,
|
||||
)
|
||||
const constants = topLevelStringConstants(sourceFile)
|
||||
const registrations: string[] = []
|
||||
const unresolved: number[] = []
|
||||
const mismatchedInstallers: number[] = []
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isCallExpression(node) && isInvariantRegistration(node.expression)) {
|
||||
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1
|
||||
const argument = node.arguments[0]
|
||||
const packageName = argument === undefined ? undefined : stringValue(argument, constants)
|
||||
if (packageName === undefined) unresolved.push(line)
|
||||
else registrations.push(packageName)
|
||||
const installer = node.arguments[1]
|
||||
if (installer === undefined || !ts.isIdentifier(installer) || installer.text !== 'install') {
|
||||
mismatchedInstallers.push(line)
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
visit(sourceFile)
|
||||
|
||||
for (const line of unresolved) {
|
||||
addViolation(
|
||||
violations,
|
||||
owner.sourcePath,
|
||||
`line ${line}: ctx.invariants.register package name must resolve to a local string constant`,
|
||||
)
|
||||
}
|
||||
for (const line of mismatchedInstallers) {
|
||||
addViolation(
|
||||
violations,
|
||||
owner.sourcePath,
|
||||
`line ${line}: ctx.invariants.register must use the checked local install function`,
|
||||
)
|
||||
}
|
||||
if (registrations.length !== 1 || registrations[0] !== owner.packageName) {
|
||||
addViolation(
|
||||
violations,
|
||||
owner.sourcePath,
|
||||
`must register exactly its own package name ${JSON.stringify(owner.packageName)}; saw ${JSON.stringify(registrations)}`,
|
||||
)
|
||||
}
|
||||
for (const exportedName of ['name', 'inject', 'apply']) {
|
||||
if (!hasNamedExport(sourceFile, exportedName)) {
|
||||
addViolation(violations, owner.sourcePath, `must named-export ${exportedName}`)
|
||||
}
|
||||
}
|
||||
if (hasDefaultExport(sourceFile)) {
|
||||
addViolation(violations, owner.sourcePath, 'must not default-export; Loader must retain the companion namespace')
|
||||
}
|
||||
checkInstaller(owner, sourceFile, violations)
|
||||
}
|
||||
|
||||
function checkInstaller(
|
||||
owner: PackageInvariantOwner,
|
||||
sourceFile: ts.SourceFile,
|
||||
violations: PackageInvariantViolation[],
|
||||
): void {
|
||||
let initializer: ts.Expression | undefined
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (!ts.isVariableStatement(statement)) continue
|
||||
for (const declaration of statement.declarationList.declarations) {
|
||||
if (ts.isIdentifier(declaration.name)
|
||||
&& declaration.name.text === 'install'
|
||||
&& declaration.initializer !== undefined) {
|
||||
initializer = declaration.initializer
|
||||
}
|
||||
}
|
||||
}
|
||||
const installer = initializer === undefined ? undefined : installerFunction(initializer)
|
||||
if (installer === undefined) {
|
||||
addViolation(violations, owner.sourcePath, 'must declare a local install function for package-owned checks')
|
||||
return
|
||||
}
|
||||
if (ts.isBlock(installer.body) && installer.body.statements.length === 0) {
|
||||
addViolation(
|
||||
violations,
|
||||
owner.sourcePath,
|
||||
'empty install function is unnecessary; omit the companion and its publication wiring',
|
||||
)
|
||||
return
|
||||
}
|
||||
const reporter = installer.parameters[1]?.name
|
||||
if (reporter === undefined || !ts.isIdentifier(reporter)) {
|
||||
addViolation(violations, owner.sourcePath, 'install function must accept the bound failure reporter as its second parameter')
|
||||
return
|
||||
}
|
||||
if (!usesIdentifier(installer.body, reporter.text)) {
|
||||
addViolation(violations, owner.sourcePath, 'install function must use its bound failure reporter')
|
||||
}
|
||||
}
|
||||
|
||||
function usesIdentifier(node: ts.Node, name: string): boolean {
|
||||
return ts.isIdentifier(node) && node.text === name
|
||||
|| node.getChildren().some(child => usesIdentifier(child, name))
|
||||
}
|
||||
|
||||
function installerFunction(
|
||||
initializer: ts.Expression,
|
||||
): ts.ArrowFunction | ts.FunctionExpression | undefined {
|
||||
if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) return initializer
|
||||
if (ts.isCallExpression(initializer)
|
||||
&& ts.isPropertyAccessExpression(initializer.expression)
|
||||
&& ts.isIdentifier(initializer.expression.expression)
|
||||
&& initializer.expression.expression.text === 'Object'
|
||||
&& initializer.expression.name.text === 'assign') {
|
||||
const target = initializer.arguments[0]
|
||||
if (target !== undefined && (ts.isArrowFunction(target) || ts.isFunctionExpression(target))) return target
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function topLevelStringConstants(sourceFile: ts.SourceFile): ReadonlyMap<string, string> {
|
||||
const constants = new Map<string, string>()
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (!ts.isVariableStatement(statement)) continue
|
||||
for (const declaration of statement.declarationList.declarations) {
|
||||
if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined) continue
|
||||
const value = stringValue(declaration.initializer, constants)
|
||||
if (value !== undefined) constants.set(declaration.name.text, value)
|
||||
}
|
||||
}
|
||||
return constants
|
||||
}
|
||||
|
||||
function stringValue(node: ts.Expression, constants: ReadonlyMap<string, string>): string | undefined {
|
||||
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text
|
||||
if (ts.isIdentifier(node)) return constants.get(node.text)
|
||||
return undefined
|
||||
}
|
||||
|
||||
function isInvariantRegistration(expression: ts.LeftHandSideExpression): boolean {
|
||||
return ts.isPropertyAccessExpression(expression)
|
||||
&& expression.name.text === 'register'
|
||||
&& ts.isPropertyAccessExpression(expression.expression)
|
||||
&& expression.expression.name.text === 'invariants'
|
||||
}
|
||||
|
||||
function hasNamedExport(sourceFile: ts.SourceFile, name: string): boolean {
|
||||
return sourceFile.statements.some((statement) => {
|
||||
if (!ts.isVariableStatement(statement)
|
||||
|| !statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) return false
|
||||
return statement.declarationList.declarations.some(declaration => ts.isIdentifier(declaration.name) && declaration.name.text === name)
|
||||
})
|
||||
}
|
||||
|
||||
function hasDefaultExport(sourceFile: ts.SourceFile): boolean {
|
||||
return sourceFile.statements.some((statement) => {
|
||||
if (ts.isExportAssignment(statement)) return true
|
||||
const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined
|
||||
if (modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)) return true
|
||||
if (!ts.isExportDeclaration(statement) || statement.exportClause === undefined) return false
|
||||
if (ts.isNamespaceExport(statement.exportClause)) {
|
||||
return statement.exportClause.name.text === 'default'
|
||||
}
|
||||
return statement.exportClause.elements.some(element => element.name.text === 'default')
|
||||
})
|
||||
}
|
||||
|
||||
/** Format violations for the command-line gate. */
|
||||
export function formatPackageInvariantViolation(
|
||||
root: string,
|
||||
violation: PackageInvariantViolation,
|
||||
): string {
|
||||
const path = resolve(root, violation.path)
|
||||
return `${relative(root, path)}: ${violation.message}`
|
||||
}
|
||||
@@ -185,6 +185,10 @@ export function collectLogEvents(scanRoot: string = root): LogEventEntry[] {
|
||||
}
|
||||
const name = member.name.text
|
||||
const where = `log event '${name}' (${src})`
|
||||
if (name.startsWith('plugin:')) {
|
||||
violations.push(`${where} uses the plugin: namespace, which holds ignorable plugin records outside SessionEventMap; choose a scope/name event type.`)
|
||||
continue
|
||||
}
|
||||
const prior = seen.get(name)
|
||||
if (prior) {
|
||||
violations.push(`${where} is already declared at ${prior}; an event type has exactly one declaration.`)
|
||||
|
||||
@@ -5,10 +5,17 @@ export type PersistenceCatalogLocale = 'en' | 'zh'
|
||||
|
||||
const english = {
|
||||
title: 'Session Persistence Event Catalog',
|
||||
intro: 'Every repository-declared durable Session event appears here with its source declaration and resolved types. The catalog covers the logical and physical headers, event envelopes, and every plugin declaration merge. See [Session](subsystems/session.md) for replay and [persistence](subsystems/persistence.md) for storage.',
|
||||
intro: 'Every repository-declared durable Session event appears here with its source declaration and resolved types. The catalog covers the logical and physical headers, event envelopes, and every SessionEventMap declaration merge. See [Session](subsystems/session.md) for replay and [persistence](subsystems/persistence.md) for storage.',
|
||||
generation: 'Run `pnpm run gen-persistence-catalog` to regenerate both catalog languages, their pairing record, the known-event module, and the machine schema inventory. `pnpm run verify-persistence-catalog` checks all generated files. Declaration fences preserve source JSDoc and type references; resolved definitions expose their transitive structure.',
|
||||
envelopeIntro: 'The envelope carries `type`, `seq`, `time`, `data`, optional `ignorable`, and conditional `surfaceOp` / `sourceEventSeqs`. A **surface** event produces model history; a **log-only** event does not. The inventory covers this repository; external plugin types require their own declarations and are outside this catalog.',
|
||||
envelope: 'Event envelope', events: 'Events', sources: 'Sources: ', source: 'Source: ', types: 'Types: ',
|
||||
pluginRecords: 'Experimental Plugin Record Catalog',
|
||||
pluginRecordsLink: 'Experimental records appear in the separate [experimental plugin record catalog](experimental-persistence-catalog.md).',
|
||||
pluginRecordsIntro: 'These current-source PluginRecordMap declarations describe ignorable, log-only records owned by experimental packages. Payload annotations link to their owning source without expanding referenced types. Records stay outside the known-event set, persistence schema fingerprints, and compatibility history; format migrations retain them on a best-effort basis.',
|
||||
pluginRecordsGeneration: 'Run `pnpm run gen-persistence-catalog` to regenerate both persistence catalogs, their language pairs, and associated artifacts. `pnpm run verify-persistence-catalog` checks all generated files.',
|
||||
pluginRecordsMainLink: 'Shared Session event declarations and resolved schemas appear in the [Session persistence event catalog](persistence-catalog.md).',
|
||||
pluginRecordsEmpty: 'No production package currently declares a plugin record.',
|
||||
pluginRecordColumns: '| Record | Owner | Purpose | Payload and source |',
|
||||
fingerprints: 'Persistence type fingerprints',
|
||||
fingerprintsIntro: 'The [machine inventory](persistence-schema.json) contains every reachable normalized type and its SHA-256 digest. Root digests include referenced types. Comments, source locations, alias names, erased brands, readonly markers, and harmless field, union, or intersection reordering do not affect these fingerprints. Tuple order, property names, value types, and optionality do. Catalog text and source locations can still produce a diff when digests stay unchanged.',
|
||||
historyIntro: 'The [format references](persistence-changes/historical-formats/README.md) cover every historical Session format. The [change records](persistence-changes/README.md) acknowledge exact transitions using snapshots kept in this tree. Follow the [review workflow](cookbook/reviewing-persistence-type-changes.md) to classify a change and record it. These checks cover declared type structure; opaque payload contents and behavior without type changes are outside their scope.',
|
||||
@@ -26,10 +33,17 @@ const english = {
|
||||
|
||||
const chinese: Record<keyof typeof english, string> = {
|
||||
title: '会话持久化事件目录',
|
||||
intro: '本目录列出仓库声明的每个持久化 Session 事件及其源码声明和解析类型,覆盖逻辑与物理 header、事件信封以及各插件的声明合并。回放规则参见 [Session](subsystems/session.zh.md),存储规则参见[持久化](subsystems/persistence.zh.md)。',
|
||||
intro: '本目录列出仓库声明的每个持久化 Session 事件及其源码声明和解析类型,覆盖逻辑与物理 header、事件信封及所有 SessionEventMap 声明合并。回放规则参见 [Session](subsystems/session.zh.md),存储规则参见[持久化](subsystems/persistence.zh.md)。',
|
||||
generation: '运行 `pnpm run gen-persistence-catalog` 可重新生成目录的两种语言、配对记录、已知事件模块和机器 schema 目录。`pnpm run verify-persistence-catalog` 检查所有生成文件。声明围栏保留源码 JSDoc 和类型引用;解析后的定义展开其传递引用结构。',
|
||||
envelopeIntro: '信封包含 `type`、`seq`、`time`、`data`、可选的 `ignorable` 以及条件字段 `surfaceOp` / `sourceEventSeqs`。**surface** 事件产生模型历史,**log-only** 事件不产生模型历史。目录覆盖本仓库;外部插件类型需要独立声明,不属于本目录。',
|
||||
envelope: '事件信封', events: '事件', sources: '来源:', source: '来源:', types: '类型:',
|
||||
pluginRecords: '实验性插件记录目录',
|
||||
pluginRecordsLink: '实验性记录单独列在[实验性插件记录目录](experimental-persistence-catalog.zh.md)中。',
|
||||
pluginRecordsIntro: '这些当前源码中的 PluginRecordMap 声明描述实验性包拥有的 ignorable、仅日志记录。载荷类型注解链接到所属源码,不展开引用类型。记录不进入已知事件集合、持久化 schema 指纹或兼容性历史;格式迁移以尽力而为的方式保留它们。',
|
||||
pluginRecordsGeneration: '运行 `pnpm run gen-persistence-catalog` 可重新生成两份持久化目录及其中英配对和关联制品。`pnpm run verify-persistence-catalog` 检查所有生成文件。',
|
||||
pluginRecordsMainLink: '共享 Session 事件声明与解析后的 schema 见[会话持久化事件目录](persistence-catalog.zh.md)。',
|
||||
pluginRecordsEmpty: '当前没有生产包声明插件记录。',
|
||||
pluginRecordColumns: '| 记录 | 所属包 | 用途 | 载荷与源码 |',
|
||||
fingerprints: '持久化类型指纹',
|
||||
fingerprintsIntro: '[机器可读目录](persistence-schema.json)包含所有可达的规范化类型及其 SHA-256 摘要。根类型的摘要涵盖引用类型。注释、源码位置、别名、擦除的品牌标记、readonly 标记以及无语义变化的字段、联合类型或交叉类型重排不影响摘要;元组顺序、属性名称、值类型和可选性会影响摘要。摘要不变时,目录文本和源码位置仍可能产生 diff。',
|
||||
historyIntro: '[格式参考](persistence-changes/historical-formats/README.zh.md)覆盖每个历史 Session 格式。[变更记录](persistence-changes/README.zh.md)通过保存在本源码树中的快照确认精确的类型转换。按照[评审流程](cookbook/reviewing-persistence-type-changes.zh.md)分类并记录变更。这些检查覆盖已声明的类型结构;不透明载荷的内部内容和未改变类型的行为变更不在检查范围内。',
|
||||
|
||||
@@ -811,6 +811,7 @@ describe('persistence changes current-tree commands', () => {
|
||||
paths: { '@deepseek-ai/dsh-session/types': ['./packages/core/session/src/types.ts'] },
|
||||
} }))
|
||||
const source = [
|
||||
'/** Experimental plugin records. */', 'export interface PluginRecordMap {}',
|
||||
'/** Stored header. */', 'export interface SessionHeader { version: 3; id: string }',
|
||||
'/** Stored event payloads. */', 'export interface SessionEventMap {', '/** Saved value. */',
|
||||
"'example/value': { value: string }", '}', '/** Surface event names. */', "export type SurfaceEventType = 'example/value'",
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
/** Plugin record inventory rejects incomplete declarations without creating compatibility roots. */
|
||||
|
||||
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS } from './experimental-package-policy.ts'
|
||||
import { collectPluginRecords } from './plugin-record-catalog.ts'
|
||||
import { persistenceCatalogArtifacts, render, renderPluginRecordCatalog } from './gen-persistence-catalog.ts'
|
||||
import { extractPersistenceSchema } from './persistence-schema.ts'
|
||||
import { removeFixtureSafely } from './test-fixture-cleanup.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
const OWNER = 'packages/core/session/src/types.ts'
|
||||
const RECORDS = 'packages/experimental/bridge/src/records.ts'
|
||||
const OWNER_SOURCE = '/** Experimental records declared by their owning packages. */\nexport interface PluginRecordMap {}\n'
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) removeFixtureSafely(root)
|
||||
})
|
||||
|
||||
function put(root: string, file: string, source: string): void {
|
||||
const path = join(root, file)
|
||||
mkdirSync(dirname(path), { recursive: true })
|
||||
writeFileSync(path, source)
|
||||
}
|
||||
|
||||
function fixture(files: Record<string, string> = {}): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-plugin-record-catalog-'))
|
||||
roots.push(root)
|
||||
for (const [file, source] of Object.entries({
|
||||
'packages/core/session/package.json': '{"name":"@deepseek-ai/dsh-session"}',
|
||||
'packages/experimental/bridge/package.json': '{"name":"@deepseek-ai/dsh-experimental-bridge"}',
|
||||
[OWNER]: OWNER_SOURCE,
|
||||
...files,
|
||||
})) put(root, file, source)
|
||||
return root
|
||||
}
|
||||
|
||||
function augmentation(members: string, heritage = ''): string {
|
||||
return `export {}\ndeclare module '@deepseek-ai/dsh-session/types' {\n interface PluginRecordMap ${heritage}{\n${members}\n }\n}\n`
|
||||
}
|
||||
|
||||
const ENTRY = " /** Retains a bridge entry. */\n 'plugin:bridge/entry': SavedEntry"
|
||||
|
||||
describe('plugin record declarations', () => {
|
||||
it.each(Object.entries(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS))('retains the persisted namespace for %s', (directory, owner) => {
|
||||
const namespace = directory.split('/').at(-1)!
|
||||
const source = `${directory}/src/records.ts`
|
||||
const root = fixture({
|
||||
[`${directory}/package.json`]: JSON.stringify({ name: owner }),
|
||||
[source]: augmentation(`/** Retains the capability event. */\n'plugin:${namespace}/entry': SavedEntry`),
|
||||
})
|
||||
expect(collectPluginRecords(root)).toEqual([{
|
||||
name: `plugin:${namespace}/entry`, owner, payload: 'SavedEntry', doc: 'Retains the capability event.', source,
|
||||
}])
|
||||
put(root, source, augmentation("/** Another package event. */\n'plugin:unrelated/entry': SavedEntry"))
|
||||
expect(() => collectPluginRecords(root)).toThrow(`must use 'plugin:${namespace}/<name>'`)
|
||||
})
|
||||
|
||||
it('accepts an empty inventory only when the owning empty map exists', () => {
|
||||
expect(collectPluginRecords(fixture())).toEqual([])
|
||||
expect(() => collectPluginRecords(fixture({ [OWNER]: 'export {}\n' })))
|
||||
.toThrow('missing exported empty PluginRecordMap')
|
||||
})
|
||||
|
||||
it('collects owner, purpose, unexpanded payload annotation and a stable source file', () => {
|
||||
const root = fixture({ [RECORDS]: augmentation(ENTRY) })
|
||||
const records = collectPluginRecords(root)
|
||||
expect(records).toEqual([{
|
||||
name: 'plugin:bridge/entry', owner: '@deepseek-ai/dsh-experimental-bridge', payload: 'SavedEntry',
|
||||
doc: 'Retains a bridge entry.', source: RECORDS,
|
||||
}])
|
||||
put(root, RECORDS, `\n\n${augmentation(ENTRY)}`)
|
||||
expect(collectPluginRecords(root)).toEqual(records)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['optional property', "'plugin:bridge/entry'?: SavedEntry", /required string-literal property/],
|
||||
['index signature', '[name: string]: SavedEntry', /required string-literal property/],
|
||||
['computed name', "['plugin:bridge/entry']: SavedEntry", /required string-literal property/],
|
||||
['identifier name', 'entry: SavedEntry', /required string-literal property/],
|
||||
['method', "'plugin:bridge/entry'(): SavedEntry", /required string-literal property/],
|
||||
['missing type', "'plugin:bridge/entry'", /explicit payload type/],
|
||||
['another owner', "'plugin:other/entry': SavedEntry", /must use 'plugin:bridge\/<name>'/],
|
||||
['missing suffix', "'plugin:bridge': SavedEntry", /must use 'plugin:bridge\/<name>'/],
|
||||
['invalid grammar', "'plugin:bridge/Entry': SavedEntry", /lowercase slash-separated segments/],
|
||||
])('rejects a declaration with %s', (_name, member, error) => {
|
||||
const root = fixture({ [RECORDS]: augmentation(`/** Entry. */\n${member}`) })
|
||||
expect(() => collectPluginRecords(root)).toThrow(error)
|
||||
})
|
||||
|
||||
it('rejects missing prose, Cordis mode tags, inheritance and duplicate names', () => {
|
||||
for (const [source, error] of [
|
||||
[augmentation("'plugin:bridge/entry': SavedEntry"), /has no description prose/],
|
||||
[augmentation("/** Entry.\n * @mode emit\n */\n'plugin:bridge/entry': SavedEntry"), /must not carry a Cordis @mode/],
|
||||
[augmentation(ENTRY, 'extends Other '), /without extends or type parameters/],
|
||||
[augmentation(`${ENTRY}\n${ENTRY}`), /already declared/],
|
||||
] as const) expect(() => collectPluginRecords(fixture({ [RECORDS]: source }))).toThrow(error)
|
||||
})
|
||||
|
||||
it('requires one empty exported map in the session package', () => {
|
||||
for (const [source, error] of [
|
||||
['interface PluginRecordMap {}\n', /must export/],
|
||||
[`${OWNER_SOURCE}${OWNER_SOURCE}`, /duplicates the owning declaration/],
|
||||
[OWNER_SOURCE.replace('{}', "{ 'plugin:session/entry': string }"), /must remain empty/],
|
||||
[OWNER_SOURCE.replace('Map {}', 'Map<T> {}'), /without extends or type parameters/],
|
||||
] as const) expect(() => collectPluginRecords(fixture({ [OWNER]: source }))).toThrow(error)
|
||||
expect(() => collectPluginRecords(fixture({ [RECORDS]: OWNER_SOURCE }))).toThrow('is outside @deepseek-ai/dsh-session')
|
||||
})
|
||||
|
||||
it('finds and rejects record declarations outside experimental production source', () => {
|
||||
const root = fixture({
|
||||
'packages/core/bridge/package.json': '{"name":"@deepseek-ai/dsh-experimental-bridge"}',
|
||||
'packages/core/bridge/src/records.ts': augmentation(ENTRY),
|
||||
})
|
||||
expect(() => collectPluginRecords(root)).toThrow('must belong to a package declared experimental')
|
||||
})
|
||||
|
||||
it('requires the types module augmentation and verifiable package ownership', () => {
|
||||
expect(() => collectPluginRecords(fixture({
|
||||
[RECORDS]: augmentation(ENTRY).replace("dsh-session/types'", "dsh-session'"),
|
||||
}))).toThrow("must augment declare module '@deepseek-ai/dsh-session/types'")
|
||||
for (const manifest of ['{}', 'broken json', '{"name":"@external/bridge"}']) {
|
||||
expect(() => collectPluginRecords(fixture({
|
||||
[RECORDS]: augmentation(ENTRY), 'packages/experimental/bridge/package.json': manifest,
|
||||
}))).toThrow(/ownership|must belong/)
|
||||
}
|
||||
})
|
||||
|
||||
it('omits tests and handles escaped declaration names through syntax discovery', () => {
|
||||
const root = fixture({
|
||||
'packages/experimental/bridge/tests/fixture.ts': augmentation(ENTRY),
|
||||
[RECORDS]: augmentation(ENTRY).replace('PluginRecordMap', '\\u0050luginRecordMap'),
|
||||
})
|
||||
expect(collectPluginRecords(root).map(record => record.name)).toEqual(['plugin:bridge/entry'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('plugin record catalogue output', () => {
|
||||
it('renders honest empty states in both languages', () => {
|
||||
const english = renderPluginRecordCatalog([])
|
||||
const chinese = renderPluginRecordCatalog([], 'zh')
|
||||
expect(english).toContain('# Experimental Plugin Record Catalog')
|
||||
expect(english).toContain('English | [中文](experimental-persistence-catalog.zh.md)')
|
||||
expect(english).toContain('No production package currently declares a plugin record.')
|
||||
expect(english).toContain('[Session persistence event catalog](persistence-catalog.md)')
|
||||
expect(chinese).toContain('[English](experimental-persistence-catalog.md) | 中文')
|
||||
expect(chinese).toContain('当前没有生产包声明插件记录。')
|
||||
expect(chinese).toContain('[会话持久化事件目录](persistence-catalog.zh.md)')
|
||||
})
|
||||
|
||||
it('links the main catalogue to the experimental page without embedding its inventory', () => {
|
||||
const english = render([], [])
|
||||
const chinese = render([], [], undefined, 'zh')
|
||||
expect(english).toContain('[experimental plugin record catalog](experimental-persistence-catalog.md)')
|
||||
expect(chinese).toContain('[实验性插件记录目录](experimental-persistence-catalog.zh.md)')
|
||||
expect(english).not.toContain('No production package currently declares')
|
||||
expect(english).not.toContain('| Record | Owner |')
|
||||
expect(chinese).not.toContain('当前没有生产包声明')
|
||||
expect(chinese).not.toContain('| 记录 | 所属包 |')
|
||||
})
|
||||
|
||||
it('renders compact, sorted rows with escaped annotations and no line-number churn', () => {
|
||||
const records = collectPluginRecords(fixture({ [RECORDS]: augmentation(`
|
||||
/** Retains state | metadata. */
|
||||
'plugin:bridge/state': { value: string | null; marker: 'a b' }
|
||||
${ENTRY}
|
||||
`) }))
|
||||
const output = renderPluginRecordCatalog([...records].reverse())
|
||||
expect(output).toContain('| `plugin:bridge/entry` | `@deepseek-ai/dsh-experimental-bridge` | Retains a bridge entry. | <code>SavedEntry</code>')
|
||||
expect(output).toContain('Retains state \\| metadata.')
|
||||
expect(output).toContain("<code>{ value: string | null; marker: 'a b'; }</code>")
|
||||
expect(output).toContain(`[\`${RECORDS}\`](../${RECORDS})`)
|
||||
expect(output.indexOf('| `plugin:bridge/entry`')).toBeLessThan(output.indexOf('| `plugin:bridge/state`'))
|
||||
expect(output).not.toMatch(/records\.ts:\d/u)
|
||||
})
|
||||
|
||||
it('limits record declaration changes to the experimental artifact pair and its metadata', () => {
|
||||
const root = fixture({
|
||||
'tsconfig.host.json': JSON.stringify({ compilerOptions: {
|
||||
target: 'es2024', module: 'esnext', moduleResolution: 'bundler', strict: true, types: [],
|
||||
paths: { '@deepseek-ai/dsh-session/types': ['./packages/core/session/src/types.ts'] },
|
||||
}, include: ['packages/**/src/**/*.ts'] }),
|
||||
[OWNER]: `${OWNER_SOURCE}
|
||||
export interface SessionHeader { version: 4; id: string; createdAt: number }
|
||||
export interface SessionEventMap {
|
||||
/** One ordinary log event. */
|
||||
'test/record': { value: string }
|
||||
}
|
||||
/** Declared ordinary event names. */
|
||||
export type SessionEventType = keyof SessionEventMap
|
||||
/** Surface placement. */
|
||||
export type SurfaceOp = 'append'
|
||||
/** Events which can produce model history. */
|
||||
export type SurfaceEventType = 'test/record'
|
||||
/** One persisted event envelope. */
|
||||
export type SessionEvent<T extends keyof SessionEventMap = keyof SessionEventMap> = {
|
||||
[K in keyof SessionEventMap]: { type: K; seq: number; time: number; data: SessionEventMap[K]; surfaceOp: SurfaceOp }
|
||||
}[T]
|
||||
`,
|
||||
'packages/session/session-persistence-jsonl/src/format.ts': "interface HeaderLine {type: 'session'; version: number; id: string; delegationDepth: number}\nexport {}\n",
|
||||
})
|
||||
const schema = extractPersistenceSchema(root)
|
||||
const empty = persistenceCatalogArtifacts(root, schema)
|
||||
const experimentalPaths = [
|
||||
'docs/experimental-persistence-catalog.md',
|
||||
'docs/experimental-persistence-catalog.zh.md',
|
||||
'docs/experimental-persistence-catalog.i18n.yaml',
|
||||
]
|
||||
expect(empty.map(artifact => artifact.path)).toEqual([
|
||||
'docs/persistence-catalog.md', 'docs/persistence-catalog.zh.md', 'docs/persistence-catalog.i18n.yaml',
|
||||
...experimentalPaths,
|
||||
'packages/core/session/src/known-event-types.ts', 'docs/persistence-schema.json',
|
||||
])
|
||||
put(root, RECORDS, `interface SavedEntry { value: string }\n${augmentation(ENTRY)}`)
|
||||
const declared = persistenceCatalogArtifacts(root, schema)
|
||||
expect(declared.filter((artifact, index) => artifact.content !== empty[index]?.content).map(artifact => artifact.path))
|
||||
.toEqual(experimentalPaths)
|
||||
expect(extractPersistenceSchema(root)).toEqual(schema)
|
||||
const nullable = augmentation(ENTRY.replace(': SavedEntry', ': SavedEntry | null'))
|
||||
put(root, RECORDS, `interface SavedEntry { value: string }\n${nullable}`)
|
||||
const changedAnnotation = persistenceCatalogArtifacts(root, schema)
|
||||
expect(changedAnnotation.filter((artifact, index) => artifact.content !== declared[index]?.content).map(artifact => artifact.path))
|
||||
.toEqual(experimentalPaths)
|
||||
put(root, RECORDS, `interface SavedEntry { nested: { count: number } }\n${nullable}`)
|
||||
expect(persistenceCatalogArtifacts(root, schema)).toEqual(changedAnnotation)
|
||||
expect(extractPersistenceSchema(root)).toEqual(schema)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,133 @@
|
||||
/** Discover package-owned experimental Session records without adding persistence schema roots. */
|
||||
|
||||
import { globSync, readFileSync } from 'node:fs'
|
||||
import { resolve, sep } from 'node:path'
|
||||
import ts from 'typescript'
|
||||
import { experimentalPackageRecordNamespace } from './experimental-package-policy.ts'
|
||||
import { parseJsDoc, pointer, rawJsDoc, reportViolations } from './jsdoc.ts'
|
||||
|
||||
const SESSION_PACKAGE = '@deepseek-ai/dsh-session'
|
||||
const SESSION_TYPES_MODULE = `${SESSION_PACKAGE}/types`
|
||||
const RECORD_NAME = /^plugin:[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)+$/u
|
||||
const printer = ts.createPrinter({ removeComments: true })
|
||||
|
||||
/** One current-source plugin record declaration, independent of compatibility history. */
|
||||
export interface PluginRecordEntry {
|
||||
/** Literal stored record name. */
|
||||
readonly name: string
|
||||
/** Owning workspace package's npm name. */
|
||||
readonly owner: string
|
||||
/** Authored payload annotation, without resolving referenced types. */
|
||||
readonly payload: string
|
||||
/** Declaration JSDoc description. */
|
||||
readonly doc: string
|
||||
/** Repository-relative declaration file, without a line number. */
|
||||
readonly source: string
|
||||
}
|
||||
|
||||
/** Read package ownership from the source file's workspace manifest. */
|
||||
function packageName(source: string, root: string): string | undefined {
|
||||
const manifest: unknown = JSON.parse(readFileSync(resolve(root, source.split('/').slice(0, 3).join('/'), 'package.json'), 'utf8'))
|
||||
return typeof manifest === 'object' && manifest !== null && 'name' in manifest && typeof manifest.name === 'string'
|
||||
? manifest.name : undefined
|
||||
}
|
||||
|
||||
/** Print compact type syntax while preserving whitespace within literal values. */
|
||||
function payloadText(type: ts.TypeNode, source: ts.SourceFile): string {
|
||||
const singleLine = (node: ts.Node): void => {
|
||||
ts.setEmitFlags(node, ts.EmitFlags.SingleLine)
|
||||
ts.forEachChild(node, singleLine)
|
||||
}
|
||||
singleLine(type)
|
||||
return printer.printNode(ts.EmitHint.Unspecified, type, source).trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect documented `PluginRecordMap` augmentations from production package source.
|
||||
* @param scanRoot - repository root containing the owning empty map and package manifests.
|
||||
* @returns unique record declarations sorted by owner and name, with unexpanded payload annotations.
|
||||
* @throws when ownership, namespace, declaration syntax, or description requirements fail.
|
||||
*/
|
||||
export function collectPluginRecords(scanRoot: string): PluginRecordEntry[] {
|
||||
const entries: PluginRecordEntry[] = []
|
||||
const violations: string[] = []
|
||||
const seen = new Map<string, string>()
|
||||
let owningDeclaration: string | undefined
|
||||
const files = globSync('packages/*/*/src/**/*.{ts,tsx,mts,cts}', { cwd: scanRoot })
|
||||
.map(file => file.split(sep).join('/')).sort()
|
||||
for (const file of files) {
|
||||
const text = readFileSync(resolve(scanRoot, file), 'utf8')
|
||||
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true)
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (!ts.isInterfaceDeclaration(node) || node.name.text !== 'PluginRecordMap') {
|
||||
ts.forEachChild(node, visit)
|
||||
return
|
||||
}
|
||||
const where = `PluginRecordMap (${pointer(file, source, node)})`
|
||||
let owner: string | undefined
|
||||
try {
|
||||
owner = packageName(file, scanRoot)
|
||||
} catch (error: unknown) {
|
||||
violations.push(`${where} has no readable package ownership: ${String(error)}`)
|
||||
return
|
||||
}
|
||||
if (node.heritageClauses?.length || node.typeParameters?.length) {
|
||||
violations.push(`${where} must declare record properties directly, without extends or type parameters.`)
|
||||
}
|
||||
if (ts.isSourceFile(node.parent)) {
|
||||
if (!file.startsWith('packages/core/session/src/') || owner !== SESSION_PACKAGE) {
|
||||
violations.push(`${where} is outside ${SESSION_PACKAGE}; contribute records through declare module '${SESSION_TYPES_MODULE}'.`)
|
||||
} else if (!node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
|
||||
violations.push(`${where} must export the owning empty interface.`)
|
||||
} else if (owningDeclaration !== undefined) {
|
||||
violations.push(`${where} duplicates the owning declaration at ${owningDeclaration}.`)
|
||||
} else {
|
||||
owningDeclaration = pointer(file, source, node)
|
||||
}
|
||||
if (node.members.length > 0) violations.push(`${where} must remain empty; experimental packages own record declarations.`)
|
||||
return
|
||||
}
|
||||
const container = node.parent
|
||||
const module = ts.isModuleBlock(container) ? container.parent : undefined
|
||||
if (module === undefined || !ts.isModuleDeclaration(module) || !ts.isStringLiteral(module.name)
|
||||
|| module.name.text !== SESSION_TYPES_MODULE || !ts.isSourceFile(module.parent)) {
|
||||
violations.push(`${where} must augment declare module '${SESSION_TYPES_MODULE}'.`)
|
||||
return
|
||||
}
|
||||
const namespace = owner === undefined ? undefined : experimentalPackageRecordNamespace(owner)
|
||||
if (!file.startsWith('packages/experimental/') || owner === undefined || namespace === undefined) {
|
||||
violations.push(`${where} must belong to a package declared experimental by the package naming policy.`)
|
||||
return
|
||||
}
|
||||
const prefix = `plugin:${namespace}/`
|
||||
for (const member of node.members) {
|
||||
const location = pointer(file, source, member)
|
||||
if (!ts.isPropertySignature(member) || member.type === undefined || member.questionToken !== undefined
|
||||
|| !ts.isStringLiteral(member.name)) {
|
||||
violations.push(`plugin record at ${location} must be a required string-literal property with an explicit payload type.`)
|
||||
continue
|
||||
}
|
||||
const name = member.name.text
|
||||
if (!RECORD_NAME.test(name) || !name.startsWith(prefix)) {
|
||||
violations.push(`plugin record '${name}' (${location}) must use '${prefix}<name>' with lowercase slash-separated segments.`)
|
||||
continue
|
||||
}
|
||||
const prior = seen.get(name)
|
||||
if (prior !== undefined) {
|
||||
violations.push(`plugin record '${name}' (${location}) is already declared at ${prior}.`)
|
||||
continue
|
||||
}
|
||||
seen.set(name, location)
|
||||
const { doc, hasMode } = parseJsDoc(rawJsDoc(text, member))
|
||||
if (doc === '') violations.push(`plugin record '${name}' (${location}) has no description prose.`)
|
||||
if (hasMode) violations.push(`plugin record '${name}' (${location}) must not carry a Cordis @mode tag.`)
|
||||
const payload = payloadText(member.type, source)
|
||||
entries.push({ name, owner, payload, doc, source: file })
|
||||
}
|
||||
}
|
||||
visit(source)
|
||||
}
|
||||
if (owningDeclaration === undefined) violations.push(`missing exported empty PluginRecordMap in ${SESSION_PACKAGE}.`)
|
||||
reportViolations('plugin-record-catalog', violations)
|
||||
return entries.sort((left, right) => left.owner.localeCompare(right.owner) || left.name.localeCompare(right.name))
|
||||
}
|
||||
@@ -21,19 +21,19 @@ describe('PR preview workflow', () => {
|
||||
expect(preview['runs-on']).toBe('ubuntu-24.04')
|
||||
expect(workflow.on).toEqual({ pull_request: { types: ['opened', 'synchronize', 'reopened'] } })
|
||||
expect(workflow.permissions).toEqual({ contents: 'read', 'pull-requests': 'write' })
|
||||
expect(preview.steps.find(step => step.uses === 'actions/checkout@v6')?.with).toEqual({ 'persist-credentials': false })
|
||||
expect(preview.steps.find(step => step.uses === 'actions/checkout@v7.0.1')?.with).toEqual({ 'persist-credentials': false })
|
||||
})
|
||||
|
||||
it('keeps the immutable full build and restore-only dependency cache', () => {
|
||||
it('keeps the immutable complete artifact build and restore-only dependency cache', () => {
|
||||
expect(workflow.env.PRIMARY_NODE_VERSION).toBe('24')
|
||||
expect(workflow.env.DSH_TELEMETRY_DISABLED).toBe('1')
|
||||
const commands = preview.steps.map(step => step.run)
|
||||
expect(commands).toContain('pnpm install --frozen-lockfile')
|
||||
expect(commands).toContain('pnpm run build')
|
||||
expect(commands).toContain('pnpm run build --artifacts-only')
|
||||
expect(commands).toContain('pnpm --filter @deepseek-ai/dsh-web-frontend run build:preview')
|
||||
expect(commands.indexOf('pnpm run build')).toBeLessThan(commands.indexOf('pnpm --filter @deepseek-ai/dsh-web-frontend run build:preview'))
|
||||
expect(commands.indexOf('pnpm run build --artifacts-only')).toBeLessThan(commands.indexOf('pnpm --filter @deepseek-ai/dsh-web-frontend run build:preview'))
|
||||
expect(preview.steps.filter(step => step.uses?.startsWith('actions/cache'))).toHaveLength(1)
|
||||
expect(preview.steps.find(step => step.uses === 'actions/cache/restore@v4')?.with).toMatchObject({
|
||||
expect(preview.steps.find(step => step.uses === 'actions/cache/restore@v6.1.0')?.with).toMatchObject({
|
||||
key: "${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}",
|
||||
})
|
||||
})
|
||||
|
||||
@@ -86,7 +86,8 @@ it('fully extracts a large deflate-compressed wheel entry', async () => {
|
||||
const archive = join(root, 'large.whl')
|
||||
await writeFile(archive, zipSync({ 'large.bin': expected }, { level: 6 }))
|
||||
await unpackPrimaryRuntimeWheel(archive, root)
|
||||
expect(await readFile(join(root, 'large.bin'))).toEqual(expected)
|
||||
// Native byte equality avoids a per-byte matcher traversal for this 1 MiB expected output.
|
||||
expect((await readFile(join(root, 'large.bin'))).equals(expected)).toBe(true)
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/** Non-default package roles retained outside the experimental group. */
|
||||
|
||||
/** Product role that does not require a default runtime mount. */
|
||||
export type ProductPackageCategory = 'optional' | 'sdk' | 'build' | 'test' | 'web-distribution' | 'declarations'
|
||||
|
||||
/** One explicitly maintained package role and its reason. */
|
||||
export interface ProductPackagePolicy {
|
||||
/** Why this package remains outside the experimental group. */
|
||||
readonly reason: string
|
||||
/** The supported product role checked against its directory. */
|
||||
readonly category: ProductPackageCategory
|
||||
}
|
||||
|
||||
/**
|
||||
* Exceptions to default runtime reachability, keyed by repository-relative package directory.
|
||||
* Optional integrations may be reached by an optional bundle without becoming default capabilities.
|
||||
*/
|
||||
export const PRODUCT_PACKAGE_POLICY: Readonly<Record<string, ProductPackagePolicy>> = {
|
||||
'packages/browser-use/browser-use': { category: 'optional', reason: 'Service definition used by explicitly selected browser providers.' },
|
||||
'packages/computer-use/computer-use': { category: 'optional', reason: 'Service definition used by explicitly selected computer providers.' },
|
||||
'packages/lsp/lsp': { category: 'optional', reason: 'Service definition for deployment-configured language servers.' },
|
||||
'packages/lsp/lsp-stdio': { category: 'optional', reason: 'Requires an explicit server list and language-server executables.' },
|
||||
'packages/lsp/tool-lsp': { category: 'optional', reason: 'Model tools selected together with a configured LSP provider.' },
|
||||
'packages/ssh/ssh': { category: 'optional', reason: 'Explicit remote connection and helper configuration.' },
|
||||
'packages/ssh/fs-ssh': { category: 'optional', reason: 'Remote filesystem provider selected in an SSH composition.' },
|
||||
'packages/ssh/sandbox-ssh': { category: 'optional', reason: 'Remote confinement provider selected in an SSH composition.' },
|
||||
'packages/ssh/subprocess-ssh': { category: 'optional', reason: 'Remote process provider selected in an SSH composition.' },
|
||||
'packages/storage/storage-sqlite': { category: 'optional', reason: 'Persistent storage requires an explicit database path and backend selection.' },
|
||||
'packages/subagent/subagent-acp': { category: 'optional', reason: 'Requires a configured external ACP agent command.' },
|
||||
'packages/subagent/subagent-claude-code': { category: 'optional', reason: 'Separately installed native Claude Code agent integration.' },
|
||||
'packages/subagent/subagent-codex': { category: 'optional', reason: 'Separately installed native Codex agent integration.' },
|
||||
'packages/subagent/subagent-dsh-sdk': { category: 'optional', reason: 'Requires an explicitly configured external DSH home.' },
|
||||
'packages/web/web-search-exa': { category: 'optional', reason: 'Alternative search provider requiring its own API credentials.' },
|
||||
'packages/web/web-search-perplexity': { category: 'optional', reason: 'Alternative search provider requiring its own API credentials.' },
|
||||
'packages/context/tmux-context': { category: 'optional', reason: 'Context contribution requiring a tmux pane.' },
|
||||
'packages/fs/tool-str-replace-editor': { category: 'optional', reason: 'Explicit alternative to the default filesystem editing tools.' },
|
||||
'packages/sdk/client': { category: 'sdk', reason: 'Published TypeScript embedding client runs in the consumer process.' },
|
||||
'packages/typert/generator': { category: 'build', reason: 'Build-time generation of type metadata.' },
|
||||
'packages/test-support/agent-loop-testkit': { category: 'test', reason: 'Published agent-loop test harness.' },
|
||||
'packages/test-support/client-runtime': { category: 'test', reason: 'Published browser plugin test harness.' },
|
||||
'packages/test-support/llm-mock-server': { category: 'test', reason: 'Published model-provider test server.' },
|
||||
'packages/test-support/llm-replay': { category: 'test', reason: 'Published recorded-model replay harness.' },
|
||||
'packages/test-support/loader-smoke': { category: 'test', reason: 'Published plugin Loader smoke-test harness.' },
|
||||
'packages/test-support/remote-mock': { category: 'test', reason: 'Published typed Remote test proxies.' },
|
||||
'packages/test-support/session-snapshot': { category: 'test', reason: 'Published Session snapshot replay harness.' },
|
||||
'packages/client/web': { category: 'web-distribution', reason: 'Browser boot kernel bundled into the Web distribution.' },
|
||||
'packages/client/ui-dockkit': { category: 'web-distribution', reason: 'Layout library bundled into the Web distribution.' },
|
||||
'packages/util/package-manifest': { category: 'declarations', reason: 'Shared package metadata declarations have no runtime mount.' },
|
||||
}
|
||||
@@ -450,7 +450,7 @@ describe('docsPages locale routes', () => {
|
||||
const translated = rootPages.filter(page => page.contentLocale === 'zh-CN')
|
||||
const fallbacks = rootPages.filter(page => page.contentLocale === 'en-US')
|
||||
|
||||
expect(translated).toHaveLength(48)
|
||||
expect(translated).toHaveLength(47)
|
||||
expect(translated.every(page => page.source.endsWith('.zh.md'))).toBe(true)
|
||||
expect(fallbacks).toEqual([])
|
||||
})
|
||||
|
||||
@@ -561,7 +561,6 @@ class BaselinePackager {
|
||||
)
|
||||
this.runner.run('pnpm', ['run', 'build'], worktree.path)
|
||||
this.runner.run('pnpm', ['run', 'publint'], worktree.path)
|
||||
this.runner.run('pnpm', ['run', 'verify-built-package-invariants'], worktree.path)
|
||||
this.runner.run('pnpm', [
|
||||
'--filter', './vendor/**',
|
||||
'--filter', './packages/**',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getEventListeners } from 'node:events'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { agentEvents, type Agent, type PreStepDecision } from '@deepseek-ai/dsh-agent'
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import { createToolResultMessage, createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { WorkflowRunId } from '@deepseek-ai/dsh-workflow'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
@@ -9,7 +9,8 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
|
||||
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
|
||||
import * as spawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
|
||||
import { MockAdapter, textResponse } from '../packages/core/agent-loop/tests/mock-adapter.ts'
|
||||
import type {} from '@deepseek-ai/dsh-tool-workflow'
|
||||
import type {} from '@deepseek-ai/dsh-tool-workflow/types'
|
||||
import type {} from '@deepseek-ai/dsh-session-log-deepseek'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
// @ts-expect-error Scenario plugins are runtime JavaScript without declaration artifacts.
|
||||
import * as fixtureModule from './fixtures/python-snapshot-workflow-order.mjs'
|
||||
@@ -17,17 +18,17 @@ import * as fixtureModule from './fixtures/python-snapshot-workflow-order.mjs'
|
||||
const config = { parentSessionId: 'advanced-parent', prompt: 'workflow child prompt' }
|
||||
const fixture = fixtureModule as unknown as {
|
||||
name: string
|
||||
apply(ctx: Context, config: { parentSessionId: string; prompt: string }): void
|
||||
apply(ctx: Context, config: { parentSessionId: string; prompt: string; direct?: { prompt: string; callId: string } }): void
|
||||
}
|
||||
const cleanups: (() => Promise<unknown>)[] = []
|
||||
afterEach(async () => { for (const cleanup of cleanups.splice(0).reverse()) await cleanup() })
|
||||
|
||||
async function harness() {
|
||||
async function harness(direct = false) {
|
||||
const ctx = new Context()
|
||||
const store = ctx.plugin(SessionStore)
|
||||
await store
|
||||
cleanups.push(() => store.dispose())
|
||||
const fiber = ctx.plugin(fixture, config)
|
||||
const fiber = ctx.plugin(fixture, { ...config, ...direct ? { direct: { prompt: config.prompt, callId: 'direct-child' } } : {} })
|
||||
await fiber
|
||||
cleanups.push(() => fiber.dispose())
|
||||
const parent = ctx.sessions.create(SessionId(config.parentSessionId))
|
||||
@@ -49,7 +50,7 @@ async function harness() {
|
||||
}
|
||||
|
||||
describe('advanced Python snapshot workflow ordering', () => {
|
||||
it('blocks a real spawned child before its descriptor and first model request', async () => {
|
||||
it('blocks a real activated child before its first model request', async () => {
|
||||
const ctx = new Context()
|
||||
const entered = Promise.withResolvers<Agent>()
|
||||
const order: string[] = []
|
||||
@@ -57,7 +58,7 @@ describe('advanced Python snapshot workflow ordering', () => {
|
||||
const assembly = ctx.plugin({
|
||||
name: 'workflow-order-driver-test',
|
||||
async apply(inner: Context) {
|
||||
await mountAgentLoopTestDependencies(inner)
|
||||
await mountAgentLoopTestDependencies(inner, { workingDirectory: true })
|
||||
await inner.plugin(AgentLoop, { agents: [] })
|
||||
await inner.plugin(SubagentRuntime)
|
||||
await inner.plugin(spawn, { providerName: 'spawn' })
|
||||
@@ -66,7 +67,7 @@ describe('advanced Python snapshot workflow ordering', () => {
|
||||
return next()
|
||||
})
|
||||
inner.on('session/event', (_session, event) => {
|
||||
if (event.type === 'tool-workflow/agent-start' || event.type === 'subagent/descriptor') order.push(event.type)
|
||||
if (event.type === 'subagent/catalog' || event.type === 'tool-workflow/agent-start') order.push(event.type)
|
||||
})
|
||||
await inner.plugin(fixture, config)
|
||||
},
|
||||
@@ -75,20 +76,21 @@ describe('advanced Python snapshot workflow ordering', () => {
|
||||
await assembly
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const parent = await ctx.agentLoop.create(SessionId(config.parentSessionId), { provider: 'mock', model: 'mock' })
|
||||
const run = await ctx.subagents.start('spawn', {
|
||||
parent, prompt: [{ type: 'text', text: config.prompt }], signal: new AbortController().signal,
|
||||
const run = await ctx.subagents.startActivation({
|
||||
provider: 'spawn', label: 'workflow-child', delivery: 'caller',
|
||||
request: { parent, prompt: [{ type: 'text', text: config.prompt }] }, signal: new AbortController().signal,
|
||||
})
|
||||
cleanups.push(() => run.dispose())
|
||||
const child = await entered.promise
|
||||
expect(child.id).toBe(run.id)
|
||||
expect(child.id).toBe(run.childId)
|
||||
expect(adapter.requests).toHaveLength(0)
|
||||
expect(child.session.snapshotEvents().some(event => event.type === 'subagent/descriptor')).toBe(false)
|
||||
expect(child.session.snapshotEvents().some(event => event.type === 'subagent/descriptor')).toBe(true)
|
||||
parent.session.append('tool-workflow/agent-start', {
|
||||
runId: WorkflowRunId('run'), seq: 1, label: 'workflow-child', childId: child.id,
|
||||
})
|
||||
expect((await run.result).output).toEqual([{ type: 'text', text: 'child complete' }])
|
||||
expect(adapter.requests).toHaveLength(1)
|
||||
expect(order).toEqual(['tool-workflow/agent-start', 'subagent/descriptor'])
|
||||
expect(order).toEqual(['subagent/catalog', 'tool-workflow/agent-start'])
|
||||
})
|
||||
|
||||
it('holds the child until the exact parent records the exact member', async () => {
|
||||
@@ -107,6 +109,27 @@ describe('advanced Python snapshot workflow ordering', () => {
|
||||
expect(getEventListeners(h.controller.signal, 'abort')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('waits for the parent to consume the direct start and acknowledge its request log', async () => {
|
||||
const h = await harness(true)
|
||||
const pending = h.step()
|
||||
h.parent.append('turn/start', { turn: 1 })
|
||||
h.parent.append('step/start', { turn: 1, step: 1 })
|
||||
const callId = ToolCallId('direct-child')
|
||||
h.parent.append('tool/call', { turn: 1, step: 1, callId, name: 'subagent', arguments: '{}' })
|
||||
const result = h.parent.append('tool/result', {
|
||||
turn: 1, step: 1,
|
||||
message: createToolResultMessage({ callId, content: [{ type: 'text', text: 'started subagent child' }], isError: false }),
|
||||
}, { surfaceOp: 'append' })
|
||||
h.parent.append('step/start', { turn: 1, step: 2 })
|
||||
await Promise.resolve()
|
||||
expect(h.next).not.toHaveBeenCalled()
|
||||
h.parent.append('session-log-deepseek/delivery-accepted', {
|
||||
sessionId: h.parent.id, sessionFormatVersion: h.parent.header.version, throughSeq: result.seq,
|
||||
})
|
||||
expect(await pending).toBe(h.decision)
|
||||
expect(h.next).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('retains a start recorded before the child reaches its first step', async () => {
|
||||
const h = await harness()
|
||||
h.start()
|
||||
|
||||
@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { officialClientBuildEnvironment, writeClientBuildRecord } from '../client-build-environment.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../../packages/boot/app-boot/src/official-bundles.ts'
|
||||
import { releaseFamily, type ReleaseMember } from './families.ts'
|
||||
import { compareVersions, nextVendorVersion, planShared, reachesPayload } from './bump.ts'
|
||||
|
||||
@@ -52,22 +53,46 @@ describe('release families', () => {
|
||||
'@deepseek-ai/dsh-experimental-agent-team',
|
||||
'@deepseek-ai/dsh-experimental-api-speech-to-text',
|
||||
'@deepseek-ai/dsh-experimental-auto-review',
|
||||
'@deepseek-ai/dsh-experimental-badge-skill-bundle',
|
||||
'@deepseek-ai/dsh-experimental-browser-use-chrome-devtools-mcp',
|
||||
'@deepseek-ai/dsh-experimental-browser-use-playwright-mcp',
|
||||
'@deepseek-ai/dsh-experimental-browser-use-runtime',
|
||||
'@deepseek-ai/dsh-experimental-browser-use-stagehand-native',
|
||||
'@deepseek-ai/dsh-experimental-claude-code-mods',
|
||||
'@deepseek-ai/dsh-experimental-client-ui-agent-team',
|
||||
'@deepseek-ai/dsh-experimental-client-ui-claude-code-mods',
|
||||
'@deepseek-ai/dsh-experimental-client-ui-cot-translation',
|
||||
'@deepseek-ai/dsh-experimental-client-ui-voice-input',
|
||||
'@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp',
|
||||
'@deepseek-ai/dsh-experimental-computer-use-cua-driver-native',
|
||||
'@deepseek-ai/dsh-experimental-cot-translation-bundle',
|
||||
'@deepseek-ai/dsh-hook-protocol',
|
||||
'@deepseek-ai/dsh-hooks-claude-code',
|
||||
'@deepseek-ai/dsh-hooks-codex',
|
||||
'@deepseek-ai/dsh-experimental-inspector-profile',
|
||||
'@deepseek-ai/dsh-experimental-inspector',
|
||||
'@deepseek-ai/dsh-experimental-ptc-runtime-python',
|
||||
'@deepseek-ai/dsh-experimental-ralph-bundle',
|
||||
'@deepseek-ai/dsh-experimental-session-inspector',
|
||||
'@deepseek-ai/dsh-experimental-session-search',
|
||||
'@deepseek-ai/dsh-session-title-all-prompts-llm',
|
||||
'@deepseek-ai/dsh-experimental-session-titles-bundle',
|
||||
'@deepseek-ai/dsh-skill-badge',
|
||||
'@deepseek-ai/dsh-experimental-speech-to-text-sensevoice',
|
||||
'@deepseek-ai/dsh-experimental-speech-to-text',
|
||||
'@deepseek-ai/dsh-experimental-terminal-bundle',
|
||||
'@deepseek-ai/dsh-experimental-tool-agent-team',
|
||||
'@deepseek-ai/dsh-tool-ralph',
|
||||
'@deepseek-ai/dsh-tool-session-query',
|
||||
'@deepseek-ai/dsh-tool-terminal',
|
||||
'@deepseek-ai/dsh-experimental-tool-worktree',
|
||||
'@deepseek-ai/dsh-experimental-translator',
|
||||
'@deepseek-ai/dsh-experimental-voice-input-bundle',
|
||||
'@deepseek-ai/dsh-webhook-github',
|
||||
'@deepseek-ai/dsh-webhook',
|
||||
'@deepseek-ai/dsh-experimental-webworker-packer',
|
||||
'@deepseek-ai/dsh-experimental-webworker-runtime',
|
||||
'@deepseek-ai/dsh-experimental-worktree',
|
||||
])
|
||||
})
|
||||
|
||||
@@ -214,6 +239,52 @@ describe('release families', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('publishes on-demand catalog members before dsh without adding installed dependencies', () => {
|
||||
const dsh = member('apps/cli', '@deepseek-ai/dsh')
|
||||
const providers = ON_DEMAND_BUNDLES.map(name => member(`packages/subagent/${name}`, name))
|
||||
const plan = releaseFamily('dsh').publishOrder([dsh, ...providers])
|
||||
expect(plan.order.map(entry => entry.name)).toEqual([...ON_DEMAND_BUNDLES, dsh.name])
|
||||
expect(dsh.manifest).toEqual({})
|
||||
expect(plan.droppedPeerEdges).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects an advertised provider missing from the public release family', () => {
|
||||
expect(() => releaseFamily('dsh').publishOrder([member('apps/cli', '@deepseek-ai/dsh')]))
|
||||
.toThrow('Official catalog prerequisite')
|
||||
})
|
||||
|
||||
it('keeps catalog prerequisites ahead of a dsh peer declaration', () => {
|
||||
const dsh = member('apps/cli', '@deepseek-ai/dsh')
|
||||
const providers = ON_DEMAND_BUNDLES.map(name => member(`packages/subagent/${name}`, name, {
|
||||
peerDependencies: { '@deepseek-ai/dsh': 'workspace:*' },
|
||||
}))
|
||||
const plan = releaseFamily('dsh').publishOrder([dsh, ...providers])
|
||||
expect(plan.order.map(entry => entry.name)).toEqual([...ON_DEMAND_BUNDLES, dsh.name])
|
||||
expect(plan.droppedPeerEdges).toEqual(ON_DEMAND_BUNDLES.map(name => ({ consumer: name, peer: dsh.name })))
|
||||
})
|
||||
|
||||
it.each(['dependencies', 'optionalDependencies', 'peerDependencies'])(
|
||||
'requires exact packed DSH %s while preserving external ranges', (section) => {
|
||||
const pkg = member('packages/example/provider', '@deepseek-ai/dsh-provider')
|
||||
const family = releaseFamily('dsh')
|
||||
const manifest = { name: pkg.name, version: pkg.version, [section]: { '@deepseek-ai/dsh-tools': pkg.version, external: '^4.0.0' } }
|
||||
expect(() => { family.validatePackedManifest(pkg, manifest) }).not.toThrow()
|
||||
for (const range of ['workspace:*', '^0.0.1', '~0.0.1', '0.0.2', '*']) {
|
||||
expect(() => { family.validatePackedManifest(pkg, { ...manifest, [section]: { '@deepseek-ai/dsh-tools': range } }) })
|
||||
.toThrow(`packed ${section}.@deepseek-ai/dsh-tools must equal`)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
it('checks packed identities and preserves independent vendor versions', () => {
|
||||
const pkg = member('vendor/example', '@deepseek-ai/example')
|
||||
const family = releaseFamily('vendor')
|
||||
expect(() => { family.validatePackedManifest(pkg, { name: pkg.name, version: pkg.version, dependencies: { '@deepseek-ai/dsh-tools': '^4.0.0' } }) })
|
||||
.not.toThrow()
|
||||
expect(() => { family.validatePackedManifest(pkg, { name: '@deepseek-ai/other', version: pkg.version }) }).toThrow('packed identity')
|
||||
expect(() => { family.validatePackedManifest(pkg, { name: pkg.name, version: '0.0.2' }) }).toThrow('packed identity')
|
||||
})
|
||||
|
||||
it('reports a runtime dependency cycle instead of emitting an arbitrary order', () => {
|
||||
const dsh = releaseFamily('dsh')
|
||||
const members = [
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
readClientBuildRecord,
|
||||
} from '../client-build-environment.ts'
|
||||
import { validateTarballPayload } from '../publication-payload.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../../packages/boot/app-boot/src/official-bundle-packages.ts'
|
||||
|
||||
/**
|
||||
* Dependency sections a consumer must publish after, because npm resolves them
|
||||
@@ -248,6 +249,13 @@ export abstract class ReleaseFamily {
|
||||
return { order: ordered, droppedPeerEdges }
|
||||
}
|
||||
|
||||
/**
|
||||
* Packages that must already be published before this member can advertise them.
|
||||
* @param _member - Member being published.
|
||||
* @returns Release prerequisites that do not become installed dependencies.
|
||||
*/
|
||||
protected publicationPrerequisites(_member: ReleaseMember): readonly string[] { return [] }
|
||||
|
||||
/**
|
||||
* The family members one member declares in the given sections.
|
||||
* @param member - the dependent member.
|
||||
@@ -269,6 +277,13 @@ export abstract class ReleaseFamily {
|
||||
if (dependency !== undefined && dependency.name !== member.name) edges.push(dependency)
|
||||
}
|
||||
}
|
||||
if (sections.includes('dependencies')) {
|
||||
for (const name of this.publicationPrerequisites(member)) {
|
||||
const prerequisite = byName.get(name)
|
||||
if (prerequisite === undefined) throw new Error(`${member.name}: Official catalog prerequisite ${name} is missing from the public release family`)
|
||||
if (!edges.some(edge => edge.name === name)) edges.push(prerequisite)
|
||||
}
|
||||
}
|
||||
return edges.sort((left, right) => left.name.localeCompare(right.name))
|
||||
}
|
||||
|
||||
@@ -311,6 +326,17 @@ export abstract class ReleaseFamily {
|
||||
*/
|
||||
abstract validatePayload(member: ReleaseMember, files: readonly string[]): void
|
||||
|
||||
/**
|
||||
* Check the identity in the packed manifest before publishing its bytes.
|
||||
* @param member - Source member selected for this release.
|
||||
* @param manifest - Manifest read from its packed tarball.
|
||||
*/
|
||||
validatePackedManifest(member: ReleaseMember, manifest: Readonly<Record<string, unknown>>): void {
|
||||
if (manifest.name !== member.name || manifest.version !== member.version) {
|
||||
throw new Error(`${member.name}: packed identity must match ${member.name}@${member.version}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The executable that proves this family's artifacts install and run, or
|
||||
* `undefined` for a family that publishes no executable.
|
||||
@@ -369,6 +395,26 @@ class DshFamily extends ReleaseFamily {
|
||||
validateTarballPayload(files, member.name)
|
||||
}
|
||||
|
||||
/** Publish every advertised on-demand package before the installation that offers it. */
|
||||
protected override publicationPrerequisites(member: ReleaseMember): readonly string[] {
|
||||
return member.name === '@deepseek-ai/dsh' ? ON_DEMAND_BUNDLES : []
|
||||
}
|
||||
|
||||
/** Require the packed DSH runtime graph to stay on the shared exact release version. */
|
||||
override validatePackedManifest(member: ReleaseMember, manifest: Readonly<Record<string, unknown>>): void {
|
||||
super.validatePackedManifest(member, manifest)
|
||||
for (const section of ['dependencies', 'optionalDependencies', 'peerDependencies']) {
|
||||
const dependencies = manifest[section]
|
||||
if (dependencies === null || typeof dependencies !== 'object' || Array.isArray(dependencies)) continue
|
||||
for (const [name, range] of Object.entries(dependencies)) {
|
||||
if (name !== '@deepseek-ai/dsh' && !name.startsWith('@deepseek-ai/dsh-')) continue
|
||||
if (range !== member.version) {
|
||||
throw new Error(`${member.name}: packed ${section}.${name} must equal ${member.version}, got ${String(range)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly installedEntry = { packageName: '@deepseek-ai/dsh', binPath: 'lib/bin.js' }
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS } from '../experimental-package-policy.ts'
|
||||
import { verifyInstalledProductIsolation } from './installed-product-isolation.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
@@ -26,7 +27,7 @@ afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('installed default-product isolation', () => {
|
||||
describe.each([experimental, ...Object.values(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS)])('installed default-product isolation: %s', (experimental) => {
|
||||
it('ignores development dependencies and unrelated installed experimental packages', () => {
|
||||
const root = fixture()
|
||||
const entry = writePackage(root, '@deepseek-ai/dsh', {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { join } from 'node:path'
|
||||
import { hasExperimentalPackageReference } from '../experimental-package-policy.ts'
|
||||
import { OPTIONAL_BUNDLES } from '../../packages/boot/app-boot/src/profile.ts'
|
||||
|
||||
interface InstalledManifest {
|
||||
@@ -56,7 +57,7 @@ export function verifyInstalledProductIsolation(directory: string, optionalBundl
|
||||
}
|
||||
|
||||
function rejectExperimental(name: string, chain: readonly string[]): void {
|
||||
if (name.startsWith('@deepseek-ai/dsh-experimental-')) {
|
||||
if (hasExperimentalPackageReference(name)) {
|
||||
throw new Error(`default product includes an experimental package: ${chain.join(' -> ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
/** Real-pnpm regression coverage for release packing, selection, hooks, and payload checks. */
|
||||
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
||||
import { createServer, type ServerResponse } from 'node:http'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { officialClientBuildEnvironment, writeClientBuildRecord } from '../client-build-environment.ts'
|
||||
import { runGate, type Gate } from '../run-gates.ts'
|
||||
import { removeFixtureSafely } from '../test-fixture-cleanup.ts'
|
||||
import { releaseFamily, tarballName } from './families.ts'
|
||||
import { packedIdentity, PUBLISH_ORDER_FILE, readPublishOrder } from './tarball.ts'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const packScript = resolve(import.meta.dirname, 'pack.ts')
|
||||
const tsxHook = pathToFileURL(require.resolve('tsx/esm')).href
|
||||
const pnpmEntry = join(dirname(require.resolve('pnpm')), 'bin/pnpm.cjs')
|
||||
const roots: string[] = []
|
||||
const commit = 'abcdef0'
|
||||
|
||||
function write(path: string, content: string): void {
|
||||
mkdirSync(dirname(path), { recursive: true })
|
||||
writeFileSync(path, content)
|
||||
}
|
||||
|
||||
function fixture(): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-release-pack-'))
|
||||
roots.push(root)
|
||||
write(join(root, 'package.json'), JSON.stringify({ name: '@deepseek-ai/pack-root', private: true, version: '1.2.3' }))
|
||||
write(join(root, 'pnpm-workspace.yaml'), 'packages:\n - vendor/*\n - native/*\n - packages/*/*\n - apps/*\n')
|
||||
return root
|
||||
}
|
||||
|
||||
function packageFixture(root: string, directory: string, name: string, additions: Record<string, unknown> = {}): void {
|
||||
write(join(root, directory, 'package.json'), JSON.stringify({
|
||||
name, version: '1.2.3', files: ['lib/index.js', 'hook.cjs'],
|
||||
scripts: {
|
||||
prepack: 'node hook.cjs prepack',
|
||||
prepare: 'node hook.cjs prepare',
|
||||
postpack: 'node hook.cjs postpack',
|
||||
},
|
||||
...additions,
|
||||
}))
|
||||
write(join(root, directory, 'lib/index.js'), 'export const value = 1;\n')
|
||||
write(join(root, directory, 'hook.cjs'), [
|
||||
"const fs = require('node:fs');",
|
||||
"const path = require('node:path');",
|
||||
"const manifest = require('./package.json');",
|
||||
"fs.appendFileSync(path.join(process.env.DSH_PACK_FIXTURE, 'hooks.log'), `${manifest.name}:${process.argv[2]}\\n`);",
|
||||
'',
|
||||
].join('\n'))
|
||||
}
|
||||
|
||||
function packGate(root: string, args: string[] = []): Gate {
|
||||
return {
|
||||
id: 'release-pack-fixture',
|
||||
label: 'release pack fixture',
|
||||
displayCommand: 'pnpm exec node release pack fixture',
|
||||
command: process.execPath,
|
||||
args: [pnpmEntry, '--dir', root, 'exec', process.execPath, '--import', tsxHook, packScript, '--family', 'vendor', '--out', 'packed', ...args],
|
||||
env: { npm_execpath: pnpmEntry, DSH_PACK_FIXTURE: root, DSH_CLIENT_COMMIT_HASH: commit },
|
||||
}
|
||||
}
|
||||
|
||||
async function runPack(root: string, args: string[] = []) {
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => { controller.abort() }, 90_000)
|
||||
try {
|
||||
const result = await runGate(packGate(root, args), controller.signal)
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.aborted).toBe(false)
|
||||
expect(result.signalCode).toBeNull()
|
||||
return {
|
||||
status: result.exitCode,
|
||||
stderr: result.output.filter(chunk => chunk.stream === 'stderr').map(chunk => chunk.text).join(''),
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
function stopped(pid: number): boolean {
|
||||
if (process.platform === 'linux') {
|
||||
try {
|
||||
return /\)\s+Z\s/.test(readFileSync(`/proc/${pid}/stat`, 'utf8'))
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return true
|
||||
throw error
|
||||
}
|
||||
}
|
||||
try {
|
||||
process.kill(pid, 0)
|
||||
return false
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ESRCH') return true
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
function readyHookPids(root: string): number[] {
|
||||
const ready: unknown = JSON.parse(readFileSync(join(root, 'hook-ready.json'), 'utf8'))
|
||||
if (ready === null || typeof ready !== 'object' || !('hook' in ready) || !('child' in ready)
|
||||
|| typeof ready.hook !== 'number' || !Number.isSafeInteger(ready.hook) || ready.hook < 1
|
||||
|| typeof ready.child !== 'number' || !Number.isSafeInteger(ready.child) || ready.child < 1) {
|
||||
throw new Error('pack fixture hook readiness requires positive process ids')
|
||||
}
|
||||
return [ready.hook, ready.child]
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) removeFixtureSafely(root)
|
||||
})
|
||||
|
||||
describe('release pack CLI', () => {
|
||||
it.each([undefined, '1', '2'])('packs only public family members and records their publish order (concurrency=%s)', async (concurrency) => {
|
||||
const root = fixture()
|
||||
// Publication places zebra before alpha; pnpm's workspace dev edge points in the opposite direction.
|
||||
packageFixture(root, 'vendor/alpha', '@deepseek-ai/alpha', { peerDependencies: { '@deepseek-ai/zebra': '*' } })
|
||||
packageFixture(root, 'vendor/zebra', '@deepseek-ai/zebra', { devDependencies: { '@deepseek-ai/alpha': 'workspace:*' } })
|
||||
write(join(root, 'vendor/zebra/node_modules/@deepseek-ai/alpha/package.json'), JSON.stringify({ name: '@deepseek-ai/alpha', version: '1.2.3' }))
|
||||
packageFixture(root, 'vendor/private', '@deepseek-ai/private', { private: true })
|
||||
packageFixture(root, 'native/unselected', '@deepseek-ai/unselected')
|
||||
const family = releaseFamily('vendor')
|
||||
const members = family.publishOrder(family.members(root)).order
|
||||
const expectedOrder = members.map(tarballName)
|
||||
|
||||
const result = await runPack(root, concurrency === undefined ? [] : ['--concurrency', concurrency])
|
||||
|
||||
expect(result.status, result.stderr).toBe(0)
|
||||
expect(readPublishOrder(join(root, 'packed'))).toEqual(expectedOrder)
|
||||
expect(readdirSync(join(root, 'packed')).sort()).toEqual([...expectedOrder, PUBLISH_ORDER_FILE].sort())
|
||||
for (const member of members) {
|
||||
expect(packedIdentity(join(root, 'packed', tarballName(member))))
|
||||
.toEqual({ name: member.name, version: member.version })
|
||||
}
|
||||
const hooks = readFileSync(join(root, 'hooks.log'), 'utf8').trim().split('\n')
|
||||
const expectedHooks = members.flatMap(member => ['prepack', 'prepare', 'postpack'].map(phase => `${member.name}:${phase}`))
|
||||
expect([...hooks].sort()).toEqual([...expectedHooks].sort())
|
||||
if (concurrency !== '2') expect(hooks).toEqual(expectedHooks)
|
||||
}, 120_000)
|
||||
|
||||
it('overlaps two independent packs and keeps later packages within the concurrency limit', async () => {
|
||||
const root = fixture()
|
||||
const names = ['alpha', 'bravo', 'charlie'].map(name => `@deepseek-ai/${name}`)
|
||||
for (const name of names) {
|
||||
const directory = `vendor/${name.slice('@deepseek-ai/'.length)}`
|
||||
packageFixture(root, directory, name, {
|
||||
scripts: { prepack: 'node hook.cjs start', postpack: 'node hook.cjs finish' },
|
||||
})
|
||||
write(join(root, directory, 'hook.cjs'), [
|
||||
"const { get } = require('node:http');",
|
||||
"const { name } = require('./package.json');",
|
||||
'const url = `${process.env.DSH_PACK_COORDINATOR}/${process.argv[2]}?name=${encodeURIComponent(name)}&pid=${process.pid}`;',
|
||||
'const request = get(url, { agent: false }, response => {',
|
||||
' response.resume();',
|
||||
" response.on('end', () => { if (response.statusCode !== 200) process.exitCode = 1; });",
|
||||
'});',
|
||||
"request.on('error', error => { console.error(error); process.exitCode = 1; });",
|
||||
'',
|
||||
].join('\n'))
|
||||
}
|
||||
|
||||
const active = new Set<string>()
|
||||
const waiting = new Map<string, ServerResponse>()
|
||||
const started: string[] = []
|
||||
const completed: string[] = []
|
||||
const pids = new Set<number>()
|
||||
const errors: string[] = []
|
||||
let maximumActive = 0
|
||||
const server = createServer((request, response) => {
|
||||
const url = new URL(request.url ?? '/', 'http://localhost')
|
||||
const name = url.searchParams.get('name') ?? ''
|
||||
const pid = Number(url.searchParams.get('pid'))
|
||||
if (!names.includes(name) || !Number.isSafeInteger(pid) || pid < 1
|
||||
|| (url.pathname !== '/start' && url.pathname !== '/finish')) {
|
||||
errors.push(`unexpected hook request ${request.url ?? ''}`)
|
||||
response.writeHead(400).end()
|
||||
return
|
||||
}
|
||||
pids.add(pid)
|
||||
if (url.pathname === '/start') {
|
||||
if (active.has(name)) errors.push(`${name} started twice`)
|
||||
started.push(name)
|
||||
active.add(name)
|
||||
maximumActive = Math.max(maximumActive, active.size)
|
||||
waiting.set(name, response)
|
||||
} else {
|
||||
if (!active.delete(name)) errors.push(`${name} finished without starting`)
|
||||
completed.push(name)
|
||||
response.end()
|
||||
}
|
||||
})
|
||||
const release = (name: string | undefined): void => {
|
||||
const response = name === undefined ? undefined : waiting.get(name)
|
||||
if (name === undefined || response === undefined) throw new Error('pack hook has not reached its barrier')
|
||||
waiting.delete(name)
|
||||
response.end()
|
||||
}
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => { controller.abort() }, 90_000)
|
||||
let pending: ReturnType<typeof runGate> | undefined
|
||||
try {
|
||||
await new Promise<void>((resolveListen, rejectListen) => {
|
||||
server.once('error', rejectListen)
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
server.off('error', rejectListen)
|
||||
resolveListen()
|
||||
})
|
||||
})
|
||||
const address = server.address()
|
||||
if (address === null || typeof address === 'string') throw new Error('pack coordinator has no TCP address')
|
||||
const gate = packGate(root, ['--concurrency', '2'])
|
||||
gate.env = { ...gate.env, DSH_PACK_COORDINATOR: `http://127.0.0.1:${address.port}` }
|
||||
pending = runGate(gate, controller.signal)
|
||||
|
||||
await vi.waitFor(() => { expect(started).toHaveLength(2) }, { timeout: 30_000, interval: 25 })
|
||||
expect(active.size).toBe(2)
|
||||
expect([...pids].every(pid => !stopped(pid))).toBe(true)
|
||||
expect(completed).toEqual([])
|
||||
release(started[0])
|
||||
|
||||
await vi.waitFor(() => { expect(started).toHaveLength(3) }, { timeout: 30_000, interval: 25 })
|
||||
expect(completed).toEqual([started[0]])
|
||||
expect(active.size).toBe(2)
|
||||
expect(maximumActive).toBe(2)
|
||||
release(started[1])
|
||||
release(started[2])
|
||||
|
||||
const result = await pending
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.aborted).toBe(false)
|
||||
expect(result.signalCode).toBeNull()
|
||||
expect(result.exitCode, result.output.map(chunk => chunk.text).join('')).toBe(0)
|
||||
expect(errors).toEqual([])
|
||||
expect([...started].sort()).toEqual(names)
|
||||
expect([...completed].sort()).toEqual(names)
|
||||
expect(active.size).toBe(0)
|
||||
expect(maximumActive).toBe(2)
|
||||
await vi.waitFor(() => { expect([...pids].every(stopped)).toBe(true) }, { timeout: 10_000, interval: 25 })
|
||||
expect(readPublishOrder(join(root, 'packed'))).toHaveLength(3)
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
controller.abort()
|
||||
try {
|
||||
await pending
|
||||
} finally {
|
||||
if (server.listening) {
|
||||
const closed = new Promise<void>((resolveClose, rejectClose) => {
|
||||
server.close((error) => {
|
||||
if (error === undefined) resolveClose()
|
||||
else rejectClose(error)
|
||||
})
|
||||
})
|
||||
server.closeAllConnections()
|
||||
await closed
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 120_000)
|
||||
|
||||
it('does not broaden an empty parallel family to the whole workspace', async () => {
|
||||
const root = fixture()
|
||||
packageFixture(root, 'vendor/private', '@deepseek-ai/private', { private: true })
|
||||
packageFixture(root, 'native/unselected', '@deepseek-ai/unselected')
|
||||
|
||||
const result = await runPack(root, ['--concurrency', '8'])
|
||||
|
||||
expect(result.status, result.stderr).toBe(0)
|
||||
expect(readFileSync(join(root, 'packed', PUBLISH_ORDER_FILE), 'utf8')).toBe('\n')
|
||||
expect(readdirSync(join(root, 'packed'))).toEqual([PUBLISH_ORDER_FILE])
|
||||
expect(existsSync(join(root, 'hooks.log'))).toBe(false)
|
||||
})
|
||||
|
||||
it('leaves no publish order after a pack hook fails', async () => {
|
||||
const root = fixture()
|
||||
packageFixture(root, 'vendor/failing', '@deepseek-ai/failing', { scripts: { prepack: 'node failing.cjs' } })
|
||||
write(join(root, 'vendor/failing/failing.cjs'), "throw new Error('pack fixture failure')\n")
|
||||
|
||||
const result = await runPack(root, ['--concurrency', '8'])
|
||||
|
||||
expect(result.status).not.toBe(0)
|
||||
expect(result.stderr).toContain('pack fixture failure')
|
||||
expect(existsSync(join(root, 'packed', PUBLISH_ORDER_FILE))).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects a successful command that did not produce the expected member tarball', async () => {
|
||||
const root = fixture()
|
||||
packageFixture(root, 'vendor/expected', '@deepseek-ai/expected', { scripts: { prepack: 'node rename.cjs' } })
|
||||
write(join(root, 'vendor/expected/rename.cjs'), [
|
||||
"const fs = require('node:fs');",
|
||||
"const manifest = require('./package.json');",
|
||||
"manifest.name = '@deepseek-ai/renamed';",
|
||||
"fs.writeFileSync('package.json', JSON.stringify(manifest));",
|
||||
'',
|
||||
].join('\n'))
|
||||
|
||||
const result = await runPack(root, ['--concurrency', '8'])
|
||||
|
||||
expect(result.status).not.toBe(0)
|
||||
expect(result.stderr).toContain('@deepseek-ai/expected produced no tarball')
|
||||
expect(existsSync(join(root, 'packed', 'deepseek-ai-renamed-1.2.3.tgz'))).toBe(true)
|
||||
expect(existsSync(join(root, 'packed', PUBLISH_ORDER_FILE))).toBe(false)
|
||||
})
|
||||
|
||||
it('validates every parallel tarball before recording the publish order', async () => {
|
||||
const root = fixture()
|
||||
packageFixture(root, 'packages/util/alpha', '@deepseek-ai/alpha')
|
||||
packageFixture(root, 'packages/util/zebra', '@deepseek-ai/zebra', { files: ['lib/index.js', 'src/index.ts'] })
|
||||
write(join(root, 'packages/util/zebra/src/index.ts'), 'export const source = 1\n')
|
||||
write(join(root, 'apps/web/dist/index.html'), '<main></main>')
|
||||
writeClientBuildRecord(root, officialClientBuildEnvironment(root, { DSH_CLIENT_COMMIT_HASH: commit }))
|
||||
|
||||
const result = await runPack(root, ['--family', 'dsh', '--concurrency', '8'])
|
||||
|
||||
expect(result.status).not.toBe(0)
|
||||
expect(result.stderr).toContain('@deepseek-ai/zebra publishes source file package/src/index.ts')
|
||||
expect(readdirSync(join(root, 'packed')).filter(name => name.endsWith('.tgz'))).toHaveLength(2)
|
||||
expect(existsSync(join(root, 'packed', PUBLISH_ORDER_FILE))).toBe(false)
|
||||
})
|
||||
|
||||
it.each(['dependencies', 'optionalDependencies', 'peerDependencies'])(
|
||||
'rejects nonexact DSH %s in the produced tarball before recording publication', async (section) => {
|
||||
const root = fixture()
|
||||
packageFixture(root, 'packages/util/probe', '@deepseek-ai/dsh-probe', { [section]: { '@deepseek-ai/dsh-library': '^1.2.3' } })
|
||||
write(join(root, 'apps/web/dist/index.html'), '<main></main>')
|
||||
writeClientBuildRecord(root, officialClientBuildEnvironment(root, { DSH_CLIENT_COMMIT_HASH: commit }))
|
||||
const result = await runPack(root, ['--family', 'dsh'])
|
||||
expect(result.status).not.toBe(0)
|
||||
expect(result.stderr).toContain(`packed ${section}.@deepseek-ai/dsh-library must equal 1.2.3`)
|
||||
expect(existsSync(join(root, 'packed', PUBLISH_ORDER_FILE))).toBe(false)
|
||||
},
|
||||
)
|
||||
|
||||
it('stops the ready pack hook and its stalled descendant before fixture cleanup', async () => {
|
||||
const root = fixture()
|
||||
packageFixture(root, 'vendor/stalled', '@deepseek-ai/stalled', { scripts: { prepack: 'node stalled.cjs' } })
|
||||
write(join(root, 'vendor/stalled/stalled.cjs'), [
|
||||
"const { spawn } = require('node:child_process');",
|
||||
"spawn(process.execPath, ['descendant.cjs'], { stdio: 'inherit' });",
|
||||
'',
|
||||
].join('\n'))
|
||||
write(join(root, 'vendor/stalled/descendant.cjs'), [
|
||||
"const fs = require('node:fs');",
|
||||
"const path = require('node:path');",
|
||||
"process.on('SIGTERM', () => {});",
|
||||
'setInterval(() => {}, 1000);',
|
||||
"fs.writeFileSync(path.join(process.env.DSH_PACK_FIXTURE, 'hook-ready.json'), JSON.stringify({ hook: process.ppid, child: process.pid }));",
|
||||
'',
|
||||
].join('\n'))
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => { controller.abort() }, 90_000)
|
||||
const pending = runGate(packGate(root, ['--concurrency', '8']), controller.signal)
|
||||
try {
|
||||
let pids: number[] = []
|
||||
await vi.waitFor(() => {
|
||||
pids = readyHookPids(root)
|
||||
expect(pids.every(pid => !stopped(pid))).toBe(true)
|
||||
}, { timeout: 30_000, interval: 25 })
|
||||
|
||||
controller.abort()
|
||||
const result = await pending
|
||||
|
||||
expect(result.aborted).toBe(true)
|
||||
expect(result.status).toBe('failed')
|
||||
await vi.waitFor(() => { expect(pids.every(stopped)).toBe(true) }, { timeout: 10_000, interval: 25 })
|
||||
expect(existsSync(join(root, 'packed', PUBLISH_ORDER_FILE))).toBe(false)
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
controller.abort()
|
||||
await pending
|
||||
}
|
||||
}, 120_000)
|
||||
})
|
||||
+40
-22
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Pack one release family's whole publish set into a single directory, in
|
||||
* publish order, and record that order for the publish step.
|
||||
* Pack one release family's whole publish set into a single directory and
|
||||
* record its publish order for the publish step. Default serial packs execute
|
||||
* in that order; explicitly parallel packs use pnpm's bounded scheduler.
|
||||
*
|
||||
* The pack step is the release boundary: it runs without credentials, produces
|
||||
* every tarball from one commit, and hands the publish step exactly those bytes
|
||||
@@ -13,29 +14,59 @@ import { parseArgs } from 'node:util'
|
||||
import { pnpmInvocation } from '../pnpm-invocation.ts'
|
||||
import { releaseFamily, tarballName, type ReleaseFamily, type ReleaseMember } from './families.ts'
|
||||
import { isEntry, runConcurrent } from './process.ts'
|
||||
import { PUBLISH_ORDER_FILE, tarballFiles } from './tarball.ts'
|
||||
import { PUBLISH_ORDER_FILE, packedManifest, tarballFiles } from './tarball.ts'
|
||||
|
||||
/** Where pack output lands when `--out` is omitted. */
|
||||
const DEFAULT_OUTPUT = 'dist/npm'
|
||||
|
||||
/**
|
||||
* Pack one member and check what its tarball carries.
|
||||
* Check one member's expected tarball and its published files.
|
||||
* @param family - the release family being packed.
|
||||
* @param member - the member to pack.
|
||||
* @param destination - absolute output directory.
|
||||
* @returns The tarball filename.
|
||||
*/
|
||||
async function packMember(family: ReleaseFamily, member: ReleaseMember, destination: string): Promise<string> {
|
||||
const invocation = pnpmInvocation(['--dir', member.directory, 'pack', '--pack-destination', destination])
|
||||
await runConcurrent(invocation.command, invocation.args)
|
||||
|
||||
function validatePackedMember(family: ReleaseFamily, member: ReleaseMember, destination: string): string {
|
||||
const filename = tarballName(member)
|
||||
const tarball = join(destination, filename)
|
||||
if (!existsSync(tarball)) throw new Error(`${member.name} produced no tarball at ${tarball}`)
|
||||
family.validatePayload(member, tarballFiles(tarball))
|
||||
family.validatePackedManifest(member, packedManifest(tarball))
|
||||
return filename
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack serial members in publish order, or batch an explicitly parallel family.
|
||||
* @param family - Release family whose payload checks apply to every tarball.
|
||||
* @param members - Exact public members in the recorded publish order.
|
||||
* @param destination - Absolute output directory.
|
||||
* @param concurrency - Maximum active package packs; one preserves serial publication order.
|
||||
* @returns Validated tarball filenames in publish order.
|
||||
*/
|
||||
async function packMembers(
|
||||
family: ReleaseFamily, members: readonly ReleaseMember[], destination: string, concurrency: number,
|
||||
): Promise<string[]> {
|
||||
if (concurrency === 1) {
|
||||
const order: string[] = []
|
||||
for (const member of members) {
|
||||
const invocation = pnpmInvocation(['--dir', member.directory, 'pack', '--pack-destination', destination])
|
||||
await runConcurrent(invocation.command, invocation.args)
|
||||
order.push(validatePackedMember(family, member, destination))
|
||||
}
|
||||
return order
|
||||
}
|
||||
if (members.length === 0) return []
|
||||
const invocation = pnpmInvocation([
|
||||
'pack',
|
||||
'--recursive',
|
||||
`--workspace-concurrency=${String(concurrency)}`,
|
||||
...members.map(member => `--filter=${member.name}`),
|
||||
'--pack-destination', destination,
|
||||
])
|
||||
await runConcurrent(invocation.command, invocation.args)
|
||||
return members.map(member => validatePackedMember(family, member, destination))
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The validated `--concurrency` value; 1 (the default) packs the
|
||||
* members one at a time, exactly as the credentialed publish workflows run it.
|
||||
@@ -68,20 +99,7 @@ async function main(): Promise<void> {
|
||||
rmSync(destination, { recursive: true, force: true })
|
||||
mkdirSync(destination, { recursive: true })
|
||||
|
||||
// Members pack in a bounded pool; the recorded publish order stays the
|
||||
// members' order regardless of completion order, because each worker writes
|
||||
// its result at the member's own position.
|
||||
const order = new Array<string>(members.length)
|
||||
let cursor = 0
|
||||
await Promise.all(Array.from({ length: Math.min(concurrency, members.length) }, async () => {
|
||||
while (cursor < members.length) {
|
||||
const index = cursor
|
||||
cursor += 1
|
||||
const member = members[index]
|
||||
if (member === undefined) break
|
||||
order[index] = await packMember(family, member, destination)
|
||||
}
|
||||
}))
|
||||
const order = await packMembers(family, members, destination, concurrency)
|
||||
writeFileSync(join(destination, PUBLISH_ORDER_FILE), `${order.join('\n')}\n`)
|
||||
|
||||
console.log(`release pack: family ${family.id}, ${String(order.length)} tarball(s) in ${values.out ?? DEFAULT_OUTPUT}`)
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
/** npm channel selection, collision prevention, and immutable-artifact retries. */
|
||||
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { setTimeout as sleep } from 'node:timers/promises'
|
||||
import { afterEach, beforeEach, describe, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { attempt, attemptEchoed, type CommandResult } from './process.ts'
|
||||
import { publishRelease } from './publish.ts'
|
||||
import { packedIdentity, readPublishOrder } from './tarball.ts'
|
||||
|
||||
vi.mock('./process.ts', async importOriginal => ({
|
||||
...await importOriginal<typeof import('./process.ts')>(),
|
||||
attempt: vi.fn(),
|
||||
attemptEchoed: vi.fn(),
|
||||
isEntry: () => false,
|
||||
}))
|
||||
|
||||
vi.mock('./tarball.ts', async importOriginal => ({
|
||||
...await importOriginal<typeof import('./tarball.ts')>(),
|
||||
packedIdentity: vi.fn(),
|
||||
readPublishOrder: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('node:timers/promises', async importOriginal => ({
|
||||
...await importOriginal<typeof import('node:timers/promises')>(),
|
||||
setTimeout: vi.fn(),
|
||||
}))
|
||||
|
||||
const CHANNEL = 'dsh-0-2-1-alpha-1'
|
||||
const ABSENT: CommandResult = { status: 1, stdout: '', stderr: 'npm error code E404' }
|
||||
const SUCCESS: CommandResult = { status: 0, stdout: '', stderr: '' }
|
||||
const CONFLICT: CommandResult = { status: 1, stdout: '', stderr: 'npm error code E409 Failed to save packument' }
|
||||
|
||||
interface PackedFixture {
|
||||
readonly name: string
|
||||
readonly version: string
|
||||
readonly path: string
|
||||
readonly integrity: string
|
||||
}
|
||||
|
||||
function packedRelease(versions: readonly string[]): { directory: string; packages: PackedFixture[] } {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'dsh-publish-channel-'))
|
||||
onTestFinished(() => { rmSync(directory, { recursive: true, force: true }) })
|
||||
const packages = versions.map((version, index) => {
|
||||
const name = `@deepseek-ai/release-fixture-${String(index)}`
|
||||
const path = join(directory, `${String(index)}.tgz`)
|
||||
const bytes = Buffer.from(`${name}@${version}\n`)
|
||||
writeFileSync(path, bytes)
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
path,
|
||||
integrity: `sha512-${createHash('sha512').update(bytes).digest('base64')}`,
|
||||
}
|
||||
})
|
||||
vi.mocked(readPublishOrder).mockReturnValue(packages.map((_, index) => `${String(index)}.tgz`))
|
||||
vi.mocked(packedIdentity).mockImplementation((path) => {
|
||||
const entry = packages.find(entry => entry.path === path)
|
||||
if (entry === undefined) throw new Error(`Unexpected tarball ${path}`)
|
||||
return { name: entry.name, version: entry.version }
|
||||
})
|
||||
return { directory, packages }
|
||||
}
|
||||
|
||||
function registry(
|
||||
tags: ReadonlyMap<string, unknown> = new Map(),
|
||||
integrities: ReadonlyMap<string, string> = new Map(),
|
||||
): void {
|
||||
vi.mocked(attempt).mockImplementation((command, args) => {
|
||||
expect(command).toBe('npm')
|
||||
if (args[0] === 'dist-tag') {
|
||||
expect(args).toHaveLength(3)
|
||||
expect(args[1]).toBe('ls')
|
||||
const name = args[2]
|
||||
if (name === undefined) throw new Error('Missing npm dist-tag package')
|
||||
if (!tags.has(name)) return ABSENT
|
||||
const payload = tags.get(name)
|
||||
const stdout = payload !== null && typeof payload === 'object' && !Array.isArray(payload)
|
||||
? Object.entries(payload).map(([tag, version]) => `${tag}: ${String(version)}`).join('\n')
|
||||
: JSON.stringify(payload) ?? 'undefined'
|
||||
return { ...SUCCESS, stdout }
|
||||
}
|
||||
expect(args).toHaveLength(4)
|
||||
expect(args[0]).toBe('view')
|
||||
expect(args[3]).toBe('--json')
|
||||
const selector = args[1]
|
||||
if (selector === undefined) throw new Error('Missing npm view selector')
|
||||
if (args[2] === 'dist.integrity') {
|
||||
const integrity = integrities.get(selector)
|
||||
return integrity === undefined ? ABSENT : { ...SUCCESS, stdout: JSON.stringify(integrity) }
|
||||
}
|
||||
throw new Error(`Unexpected npm view field ${String(args[2])}`)
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
vi.mocked(attemptEchoed).mockReturnValue(SUCCESS)
|
||||
vi.mocked(sleep).mockResolvedValue(undefined)
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
describe('release publication channels', () => {
|
||||
it.each([
|
||||
['vendor', '1.0.6', undefined],
|
||||
['vendor', '1.0.6-alpha.1', 'next'],
|
||||
['dsh', '0.2.1', undefined],
|
||||
['dsh', '0.2.1-alpha.1', 'alpha'],
|
||||
['dsh', '0.2.1-canary.1', 'canary'],
|
||||
['dsh', '0.2.1-rc.1', 'next'],
|
||||
] as const)('uses the default %s channel for %s when no override is supplied', async (family, version, tag) => {
|
||||
const fixture = packedRelease([version])
|
||||
const [entry] = fixture.packages
|
||||
expect(entry).toBeDefined()
|
||||
registry()
|
||||
|
||||
await publishRelease(family, fixture.directory)
|
||||
|
||||
expect(attempt).toHaveBeenCalledExactlyOnceWith('npm', ['view', `${entry!.name}@${version}`, 'dist.integrity', '--json'])
|
||||
expect(attemptEchoed).toHaveBeenCalledExactlyOnceWith('npm', [
|
||||
'publish', entry!.path, ...tag === undefined ? [] : ['--tag', tag],
|
||||
])
|
||||
})
|
||||
|
||||
it('checks every custom channel before publishing in the recorded order', async () => {
|
||||
const fixture = packedRelease(['1.0.6-alpha.1', '4.0.5-alpha.1'])
|
||||
const [first, second] = fixture.packages
|
||||
expect(first).toBeDefined()
|
||||
expect(second).toBeDefined()
|
||||
registry(new Map([[first!.name, { latest: '1.0.5', next: '1.0.5-rc.1' }]]))
|
||||
vi.mocked(attemptEchoed).mockImplementation(() => {
|
||||
expect(vi.mocked(attempt).mock.calls.filter(([, args]) => args[0] === 'dist-tag')).toEqual([
|
||||
['npm', ['dist-tag', 'ls', first!.name]],
|
||||
['npm', ['dist-tag', 'ls', second!.name]],
|
||||
])
|
||||
return SUCCESS
|
||||
})
|
||||
|
||||
await publishRelease('vendor', fixture.directory, CHANNEL)
|
||||
|
||||
expect(vi.mocked(attemptEchoed).mock.calls).toEqual(fixture.packages.map(entry => [
|
||||
'npm', ['publish', entry.path, '--tag', CHANNEL],
|
||||
]))
|
||||
})
|
||||
|
||||
it('uses an explicit channel instead of the dsh prerelease default', async () => {
|
||||
const fixture = packedRelease(['0.2.1-alpha.1'])
|
||||
registry()
|
||||
|
||||
await publishRelease('dsh', fixture.directory, CHANNEL)
|
||||
|
||||
expect(attemptEchoed).toHaveBeenCalledExactlyOnceWith('npm', ['publish', fixture.packages[0]!.path, '--tag', CHANNEL])
|
||||
})
|
||||
|
||||
it.each(['', ' ', ' next', '-release-preview', 'release/channel', '1.2.3', '^1.2', 'v1', 'x'])(
|
||||
'rejects invalid channel %j before reading artifacts or querying npm',
|
||||
async (tag) => {
|
||||
await expect(publishRelease('vendor', '/unused-release-artifacts', tag)).rejects.toThrow()
|
||||
|
||||
expect(readPublishOrder).not.toHaveBeenCalled()
|
||||
expect(packedIdentity).not.toHaveBeenCalled()
|
||||
expect(attempt).not.toHaveBeenCalled()
|
||||
expect(attemptEchoed).not.toHaveBeenCalled()
|
||||
},
|
||||
)
|
||||
|
||||
it('stops before publishing when a later package already assigns the custom channel elsewhere', async () => {
|
||||
const fixture = packedRelease(['1.0.6-alpha.1', '4.0.5-alpha.1'])
|
||||
const [first, second] = fixture.packages
|
||||
registry(new Map([
|
||||
[first!.name, { latest: '1.0.5' }],
|
||||
[second!.name, { [CHANNEL]: '4.0.4' }],
|
||||
]))
|
||||
|
||||
await expect(publishRelease('vendor', fixture.directory, CHANNEL)).rejects.toThrow()
|
||||
|
||||
expect(vi.mocked(attempt).mock.calls.filter(([, args]) => args[0] === 'dist-tag')).toEqual([
|
||||
['npm', ['dist-tag', 'ls', first!.name]],
|
||||
['npm', ['dist-tag', 'ls', second!.name]],
|
||||
])
|
||||
expect(attemptEchoed).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([null, [], {}, 'not an object', { [CHANNEL]: 42 }])('rejects malformed registry channels %j before publishing', async (payload) => {
|
||||
const fixture = packedRelease(['1.0.6-alpha.1'])
|
||||
registry(new Map([[fixture.packages[0]!.name, payload]]))
|
||||
|
||||
await expect(publishRelease('vendor', fixture.directory, CHANNEL)).rejects.toThrow()
|
||||
|
||||
expect(attemptEchoed).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('stops on a registry authorization failure before publishing', async () => {
|
||||
const fixture = packedRelease(['1.0.6-alpha.1'])
|
||||
vi.mocked(attempt).mockReturnValue({ status: 1, stdout: '', stderr: 'npm error code E403' })
|
||||
|
||||
await expect(publishRelease('vendor', fixture.directory, CHANNEL)).rejects.toThrow(/E403/u)
|
||||
|
||||
expect(attemptEchoed).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects repeated channel bindings before publishing', async () => {
|
||||
const fixture = packedRelease(['1.0.6-alpha.1'])
|
||||
vi.mocked(attempt).mockReturnValue({
|
||||
...SUCCESS,
|
||||
stdout: `${CHANNEL}: 1.0.6-alpha.1\n${CHANNEL}: 1.0.6-alpha.1\n`,
|
||||
})
|
||||
|
||||
await expect(publishRelease('vendor', fixture.directory, CHANNEL)).rejects.toThrow('invalid dist-tags')
|
||||
|
||||
expect(attemptEchoed).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('skips an identical artifact with its custom channel and no latest tag', async () => {
|
||||
const fixture = packedRelease(['1.0.6-alpha.1'])
|
||||
const entry = fixture.packages[0]!
|
||||
registry(
|
||||
new Map([[entry.name, { [CHANNEL]: entry.version }]]),
|
||||
new Map([[`${entry.name}@${entry.version}`, entry.integrity]]),
|
||||
)
|
||||
|
||||
await publishRelease('vendor', fixture.directory, CHANNEL)
|
||||
|
||||
expect(attempt).toHaveBeenCalledWith('npm', ['dist-tag', 'ls', entry.name])
|
||||
expect(attemptEchoed).not.toHaveBeenCalled()
|
||||
expect(sleep).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects an already published version whose custom channel is absent', async () => {
|
||||
const fixture = packedRelease(['1.0.6-alpha.1'])
|
||||
const entry = fixture.packages[0]!
|
||||
registry(new Map([[entry.name, { latest: '1.0.5' }]]), new Map([[`${entry.name}@${entry.version}`, entry.integrity]]))
|
||||
|
||||
await expect(publishRelease('vendor', fixture.directory, CHANNEL)).rejects.toThrow()
|
||||
|
||||
expect(attemptEchoed).not.toHaveBeenCalled()
|
||||
expect(attempt).toHaveBeenCalledWith('npm', ['view', `${entry.name}@${entry.version}`, 'dist.integrity', '--json'])
|
||||
})
|
||||
|
||||
it('stops before the first upload when a later published version has no custom channel', async () => {
|
||||
const fixture = packedRelease(['1.0.6-alpha.1', '4.0.5-alpha.1'])
|
||||
const [first, second] = fixture.packages
|
||||
registry(
|
||||
new Map([[first!.name, { latest: '1.0.5' }], [second!.name, { latest: '4.0.4' }]]),
|
||||
new Map([[`${second!.name}@${second!.version}`, second!.integrity]]),
|
||||
)
|
||||
|
||||
await expect(publishRelease('vendor', fixture.directory, CHANNEL)).rejects.toThrow()
|
||||
|
||||
expect(attempt).toHaveBeenCalledWith('npm', ['view', `${second!.name}@${second!.version}`, 'dist.integrity', '--json'])
|
||||
expect(attemptEchoed).not.toHaveBeenCalled()
|
||||
expect(sleep).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('preserves the explicit channel when retrying an E409 publication', async () => {
|
||||
const fixture = packedRelease(['1.0.6-alpha.1'])
|
||||
const entry = fixture.packages[0]!
|
||||
registry(new Map([[entry.name, { latest: '1.0.5', next: '1.0.5-rc.1' }]]))
|
||||
vi.mocked(attemptEchoed).mockReturnValueOnce(CONFLICT).mockReturnValueOnce(SUCCESS)
|
||||
|
||||
await publishRelease('vendor', fixture.directory, CHANNEL)
|
||||
|
||||
expect(vi.mocked(attemptEchoed).mock.calls).toEqual([
|
||||
['npm', ['publish', entry.path, '--tag', CHANNEL]],
|
||||
['npm', ['publish', entry.path, '--tag', CHANNEL]],
|
||||
])
|
||||
expect(sleep).toHaveBeenCalledExactlyOnceWith(2_000)
|
||||
})
|
||||
|
||||
it('keeps the explicit channel when an E409 publication already landed', async () => {
|
||||
const fixture = packedRelease(['1.0.6-alpha.1'])
|
||||
const entry = fixture.packages[0]!
|
||||
const tags = new Map<string, Record<string, string>>([[entry.name, { latest: '1.0.5', next: '1.0.5-rc.1' }]])
|
||||
const integrities = new Map<string, string>()
|
||||
registry(tags, integrities)
|
||||
vi.mocked(attemptEchoed).mockImplementationOnce(() => {
|
||||
integrities.set(`${entry.name}@${entry.version}`, entry.integrity)
|
||||
tags.set(entry.name, { ...tags.get(entry.name)!, [CHANNEL]: entry.version })
|
||||
return CONFLICT
|
||||
})
|
||||
|
||||
await publishRelease('vendor', fixture.directory, CHANNEL)
|
||||
|
||||
expect(attemptEchoed).toHaveBeenCalledExactlyOnceWith('npm', ['publish', entry.path, '--tag', CHANNEL])
|
||||
const lookupIndex = vi.mocked(attempt).mock.calls.findLastIndex(([, args]) => args[2] === 'dist.integrity')
|
||||
expect(lookupIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(vi.mocked(attempt).mock.invocationCallOrder[lookupIndex]).toBeGreaterThan(
|
||||
vi.mocked(attemptEchoed).mock.invocationCallOrder[0]!,
|
||||
)
|
||||
expect(sleep).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects different bytes already published at the requested version', async () => {
|
||||
const fixture = packedRelease(['1.0.6-alpha.1'])
|
||||
const entry = fixture.packages[0]!
|
||||
registry(
|
||||
new Map([[entry.name, { [CHANNEL]: entry.version }]]),
|
||||
new Map([[`${entry.name}@${entry.version}`, 'sha512-different']]),
|
||||
)
|
||||
|
||||
await expect(publishRelease('vendor', fixture.directory, CHANNEL)).rejects.toThrow('already published with different content')
|
||||
|
||||
expect(attemptEchoed).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
+71
-18
@@ -17,9 +17,10 @@ import { readFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { setTimeout as sleep } from 'node:timers/promises'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { valid, validRange } from 'semver'
|
||||
import { releaseFamily } from './families.ts'
|
||||
import { attempt, attemptEchoed, isEntry } from './process.ts'
|
||||
import { packedIdentity, readPublishOrder } from './tarball.ts'
|
||||
import { packedIdentity, readPublishOrder, type PackedIdentity } from './tarball.ts'
|
||||
|
||||
/**
|
||||
* Registry codes that answer a write which did not settle, rather than a
|
||||
@@ -84,6 +85,44 @@ function registryState(name: string, version: string): RegistryState {
|
||||
return { kind: 'present', integrity: parsed }
|
||||
}
|
||||
|
||||
/** Reject empty tags, npm option prefixes, characters requiring URI encoding, and version ranges. */
|
||||
function validateDistTag(distTag: string): void {
|
||||
if (distTag === '' || distTag.startsWith('-') || encodeURIComponent(distTag) !== distTag || validRange(distTag) !== null) {
|
||||
throw new Error(`Invalid npm dist-tag ${JSON.stringify(distTag)}: use a non-empty tag without a leading hyphen, characters requiring URI encoding, or a version range.`)
|
||||
}
|
||||
}
|
||||
|
||||
/** An explicit channel may be new or already name the same version after a partial publication. */
|
||||
function verifyDistTag(member: PackedIdentity, distTag: string): void {
|
||||
const result = attempt('npm', ['dist-tag', 'ls', member.name])
|
||||
if (result.status !== 0) {
|
||||
const output = `${result.stdout}${result.stderr}`
|
||||
if (output.includes('E404') || output.includes('404 Not Found')) return
|
||||
throw new Error(`npm dist-tag ls ${member.name} failed:\n${output}`)
|
||||
}
|
||||
const tags = new Map<string, string>()
|
||||
for (const line of result.stdout.split(/\r?\n/u).filter(line => line !== '')) {
|
||||
const match = /^(\S+): (\S+)$/u.exec(line)
|
||||
const tag = match?.[1]
|
||||
const version = match?.[2]
|
||||
if (tag === undefined || version === undefined || valid(version) === null || tags.has(tag)) {
|
||||
throw new Error(`npm reported invalid dist-tags for ${member.name}: ${JSON.stringify(line)}`)
|
||||
}
|
||||
tags.set(tag, version)
|
||||
}
|
||||
if (tags.size === 0) throw new Error(`npm reported no dist-tags for ${member.name}`)
|
||||
const version = tags.get(distTag)
|
||||
if (version === undefined) {
|
||||
if (registryState(member.name, member.version).kind === 'present') {
|
||||
throw new Error(`${member.name}@${member.version} is already published without dist-tag ${distTag}; this run cannot bind an existing version to a new channel.`)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (version !== member.version) {
|
||||
throw new Error(`${member.name}@${distTag} already points to ${version}; choose an unused dist-tag instead of replacing it with ${member.version}.`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish one tarball, retrying a registry write that did not settle.
|
||||
*
|
||||
@@ -127,30 +166,32 @@ async function publishTarball(
|
||||
}
|
||||
}
|
||||
|
||||
/** Publish the family named by `--family` from the directory named by `--from`. */
|
||||
async function main(): Promise<void> {
|
||||
const { values } = parseArgs({
|
||||
options: { family: { type: 'string' }, from: { type: 'string' } },
|
||||
allowPositionals: false,
|
||||
})
|
||||
if (values.family === undefined || values.from === undefined) {
|
||||
throw new Error('usage: publish.ts --family <dsh|vendor> --from <packed directory>')
|
||||
}
|
||||
|
||||
const family = releaseFamily(values.family)
|
||||
const directory = resolve(process.cwd(), values.from)
|
||||
/**
|
||||
* Publish packed artifacts, preserving occupied explicit channels and existing version integrity.
|
||||
* @param familyId - release family whose default channels apply when no override is supplied.
|
||||
* @param directory - packed artifact directory containing the publication order.
|
||||
* @param distTag - optional explicit npm channel, checked across the whole batch before uploading.
|
||||
* @returns Resolves after every artifact has been published or verified as already present.
|
||||
*/
|
||||
export async function publishRelease(familyId: string, directory: string, distTag?: string): Promise<void> {
|
||||
if (distTag !== undefined) validateDistTag(distTag)
|
||||
const family = releaseFamily(familyId)
|
||||
|
||||
// Every entry in the order settles as either published or already present, so
|
||||
// one counter answers "how far along is this run" for whoever is watching a
|
||||
// release that takes minutes per family.
|
||||
const order = readPublishOrder(directory)
|
||||
const order = readPublishOrder(directory).map((filename) => {
|
||||
const tarball = join(directory, filename)
|
||||
return { tarball, ...packedIdentity(tarball) }
|
||||
})
|
||||
if (distTag !== undefined) {
|
||||
for (const member of order) verifyDistTag(member, distTag)
|
||||
}
|
||||
const total = String(order.length)
|
||||
let published = 0
|
||||
let skipped = 0
|
||||
for (const [index, filename] of order.entries()) {
|
||||
for (const [index, { tarball, name, version }] of order.entries()) {
|
||||
const progress = `[${String(index + 1)}/${total}]`
|
||||
const tarball = join(directory, filename)
|
||||
const { name, version } = packedIdentity(tarball)
|
||||
const state = registryState(name, version)
|
||||
if (state.kind === 'present') {
|
||||
const local = integrityOf(tarball)
|
||||
@@ -168,7 +209,7 @@ async function main(): Promise<void> {
|
||||
// Space out the writes: the gap belongs between publishes, so a run that
|
||||
// only skips does not wait at all.
|
||||
if (published > 0) await sleep(PUBLISH_SPACING_MS)
|
||||
await publishTarball(tarball, name, version, family.distTagForVersion(version))
|
||||
await publishTarball(tarball, name, version, distTag ?? family.distTagForVersion(version))
|
||||
console.log(`release publish: ${progress} ${name}@${version} published`)
|
||||
published += 1
|
||||
}
|
||||
@@ -179,4 +220,16 @@ async function main(): Promise<void> {
|
||||
)
|
||||
}
|
||||
|
||||
/** Publish the family named by `--family` from the directory named by `--from`. */
|
||||
async function main(): Promise<void> {
|
||||
const { values } = parseArgs({
|
||||
options: { family: { type: 'string' }, from: { type: 'string' }, 'dist-tag': { type: 'string' } },
|
||||
allowPositionals: false,
|
||||
})
|
||||
if (values.family === undefined || values.from === undefined) {
|
||||
throw new Error('usage: publish.ts --family <dsh|vendor> --from <packed directory> [--dist-tag <tag>]')
|
||||
}
|
||||
await publishRelease(values.family, resolve(process.cwd(), values.from), values['dist-tag'])
|
||||
}
|
||||
|
||||
if (isEntry(import.meta.url)) await main()
|
||||
@@ -32,14 +32,23 @@ export function tarballFiles(tarball: string): string[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a packed tarball's own manifest.
|
||||
* @param tarball - absolute tarball path.
|
||||
* Read the manifest contained in a packed tarball, without consulting the checkout.
|
||||
* @param tarball - Absolute tarball path.
|
||||
* @returns The packed package.json object.
|
||||
*/
|
||||
export function packedManifest(tarball: string): Record<string, unknown> {
|
||||
const manifest: unknown = JSON.parse(capture('tar', ['-xOzf', basename(tarball), 'package/package.json'], { cwd: dirname(tarball) }))
|
||||
if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) throw new Error(`${tarball} has no manifest`)
|
||||
return manifest as Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a packed tarball's own identity.
|
||||
* @param tarball - Absolute tarball path.
|
||||
* @returns The name and version the tarball declares.
|
||||
*/
|
||||
export function packedIdentity(tarball: string): PackedIdentity {
|
||||
const manifest: unknown = JSON.parse(capture('tar', ['-xOzf', basename(tarball), 'package/package.json'], { cwd: dirname(tarball) }))
|
||||
if (manifest === null || typeof manifest !== 'object') throw new Error(`${tarball} has no manifest`)
|
||||
const { name, version } = manifest as Record<string, unknown>
|
||||
const { name, version } = packedManifest(tarball)
|
||||
if (typeof name !== 'string' || typeof version !== 'string') throw new Error(`${tarball} manifest lacks name/version`)
|
||||
return { name, version }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/** Packed-install npm cache ownership through the real offline verifier entry. */
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
const root = resolve(import.meta.dirname, '../..')
|
||||
const roots: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const directory of roots.splice(0)) rmSync(directory, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
// The release verifier invokes POSIX npm directly, not the Windows npm.cmd shim.
|
||||
describe.skipIf(process.platform === 'win32')('packed-install npm cache', () => {
|
||||
// The verifier starts npm, lifecycle scripts, and the installed entry serially.
|
||||
it.each(['success', 'install failure', 'entry failure'] as const)(
|
||||
'owns cache writes and removes them after %s',
|
||||
(outcome) => {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'dsh-packed-cache-test-'))
|
||||
roots.push(directory)
|
||||
const source = join(directory, 'package')
|
||||
const packed = join(directory, 'packed')
|
||||
const ambient = join(directory, 'ambient-cache')
|
||||
const configured = join(directory, 'configured-cache')
|
||||
const temporary = join(directory, 'temporary')
|
||||
for (const path of [join(source, 'lib'), packed, ambient, configured, temporary]) mkdirSync(path, { recursive: true })
|
||||
writeFileSync(join(ambient, 'sentinel'), 'ambient cache must survive\n')
|
||||
writeFileSync(join(configured, 'sentinel'), 'configured cache must survive\n')
|
||||
const npmrc = join(directory, 'npmrc')
|
||||
writeFileSync(npmrc, `cache=${configured}\n`)
|
||||
const evidence = join(directory, 'evidence.json')
|
||||
const probe = `
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const consumer = process.env.INIT_CWD || process.cwd()
|
||||
const cache = path.join(consumer, '.npm-cache', '_cacache')
|
||||
const files = fs.existsSync(cache) ? fs.readdirSync(cache, { recursive: true }).filter(name => fs.statSync(path.join(cache, name)).isFile()) : []
|
||||
fs.writeFileSync(process.env.CACHE_EVIDENCE, JSON.stringify({ consumer, files }))
|
||||
`
|
||||
writeFileSync(join(source, 'probe.cjs'), probe + (outcome === 'install failure' ? '\nprocess.exit(37)\n' : ''))
|
||||
writeFileSync(join(source, 'lib/bin.js'), outcome === 'entry failure' ? 'process.exit(37)\n' : 'console.log("0.0.1")\n')
|
||||
writeFileSync(join(source, 'package.json'), JSON.stringify({
|
||||
name: '@deepseek-ai/dsh',
|
||||
version: '0.0.1',
|
||||
scripts: { postinstall: 'node probe.cjs' },
|
||||
}))
|
||||
const archive = spawnSync('tar', ['-czf', join(packed, 'dsh.tgz'), '-C', directory, 'package'], { encoding: 'utf8', timeout: 30_000 })
|
||||
expect(archive.error).toBeUndefined()
|
||||
expect(archive.signal).toBeNull()
|
||||
expect(archive.status, archive.stderr).toBe(0)
|
||||
const environment = Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^npm_config_/i.test(key)))
|
||||
Object.assign(environment, {
|
||||
npm_config_cache: ambient,
|
||||
npm_config_userconfig: npmrc,
|
||||
npm_config_offline: 'true',
|
||||
npm_config_ignore_scripts: 'false',
|
||||
CACHE_EVIDENCE: evidence,
|
||||
TMPDIR: temporary,
|
||||
TMP: temporary,
|
||||
TEMP: temporary,
|
||||
})
|
||||
const result = spawnSync(process.execPath, [
|
||||
'--import', 'tsx', 'scripts/release/verify-packed-install.ts', '--family', 'dsh', '--from', packed,
|
||||
], { cwd: root, env: environment, encoding: 'utf8', timeout: 120_000 })
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.signal).toBeNull()
|
||||
expect(result.status, result.stdout + result.stderr).toBe(outcome === 'success' ? 0 : 1)
|
||||
const observation = JSON.parse(readFileSync(evidence, 'utf8')) as { consumer: string; files: string[] }
|
||||
expect(observation.files.length, 'npm must write tarball content into the consumer-private cache').toBeGreaterThan(0)
|
||||
expect(observation.consumer).toContain('dsh-packed-dsh-')
|
||||
expect(existsSync(observation.consumer), 'the verifier finally must remove its consumer and cache').toBe(false)
|
||||
expect(readdirSync(ambient)).toEqual(['sentinel'])
|
||||
expect(readFileSync(join(ambient, 'sentinel'), 'utf8')).toBe('ambient cache must survive\n')
|
||||
expect(readdirSync(configured)).toEqual(['sentinel'])
|
||||
expect(readFileSync(join(configured, 'sentinel'), 'utf8')).toBe('configured cache must survive\n')
|
||||
},
|
||||
180_000,
|
||||
)
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Install packed tarballs into a throwaway consumer outside the repository and
|
||||
* drive the installed executable with plain Node.
|
||||
* drive the installed executable with plain Node. Its npm cache belongs to that
|
||||
* consumer and is removed with it on normal completion or failure.
|
||||
*
|
||||
* Every tarball the installed tree needs comes from `--from`, so the only
|
||||
* registry traffic is for external dependencies. That matters beyond hermetic
|
||||
@@ -105,7 +106,7 @@ function main(): void {
|
||||
// that cannot install them must still start — which is what optional means
|
||||
// here. Their entry package is a plain dependency of dsh-sandbox-local, so
|
||||
// its tarball is supplied through --from.
|
||||
capture('npm', ['install', '--no-audit', '--no-fund', '--package-lock=false', '--omit=optional', '--loglevel=http'],
|
||||
capture('npm', ['install', '--cache', join(consumerRoot, '.npm-cache'), '--no-audit', '--no-fund', '--package-lock=false', '--omit=optional', '--loglevel=http'],
|
||||
{ cwd: consumerRoot, env: environment })
|
||||
|
||||
const installedEntry = join(consumerRoot, 'node_modules', entry.packageName)
|
||||
|
||||
@@ -152,7 +152,6 @@ const POSTCONDITIONS: readonly PostCondition[] = [
|
||||
{ file: 'vendor/hmr/package.json', text: '"name": "@deepseek-ai/cordis-plugin-hmr"', count: 1 },
|
||||
{ file: 'scripts/cordis-walk.ts', text: '@deepseek-ai\\/cordis', count: 1 },
|
||||
{ file: 'scripts/cordis-walk.ts', text: '!== \'@deepseek-ai/cordis\'', count: 1 },
|
||||
{ file: 'scripts/gen-scoped-events.ts', text: '=== \'@deepseek-ai/cordis\'', count: 1 },
|
||||
{ file: 'packages/typert/generator/src/analyzer.ts', text: '!== \'@deepseek-ai/cordis\'', count: 2 },
|
||||
{ file: 'scripts/check-workspace-constraints.ts', text: '?.[\'@deepseek-ai/cordis\']', count: 2 },
|
||||
{ file: 'packages/boot/app-boot/tsdown.config.ts', text: '[\'@deepseek-ai/cordis-plugin-include\']', count: 1 },
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/** Build benchmark artifacts with TypeScript diagnostics owned by required full builds. */
|
||||
|
||||
import { resolve } from 'node:path'
|
||||
import { buildLibraryArtifacts, runPnpmCommand } from './compile-referenced-projects.ts'
|
||||
|
||||
/**
|
||||
* Build native support, libraries, benchmark workers, and Web assets that benchmark files consume.
|
||||
* @param root - Repository root containing all build configurations.
|
||||
* @throws If any build fails.
|
||||
*/
|
||||
export function buildCiBenchArtifacts(root: string): void {
|
||||
runPnpmCommand(root, ['run', 'build:native-system'])
|
||||
buildLibraryArtifacts(root, false, 'benchmark-emit')
|
||||
runPnpmCommand(root, ['exec', 'tsdown', '--config-loader', 'native', '--config', 'benchmarks/tsdown.config.ts'])
|
||||
runPnpmCommand(root, ['run', 'build:web'])
|
||||
}
|
||||
|
||||
/**
|
||||
* Build benchmark artifacts, then run the existing benchmark suite.
|
||||
* @param root - Repository root containing all build configurations.
|
||||
* @throws If any build or the benchmark suite fails.
|
||||
*/
|
||||
export function runCiBench(root: string): void {
|
||||
buildCiBenchArtifacts(root)
|
||||
runPnpmCommand(root, ['run', 'test:bench:built'])
|
||||
}
|
||||
|
||||
if (import.meta.main) runCiBench(resolve(import.meta.dirname, '..'))
|
||||
+137
-23
@@ -149,6 +149,20 @@ function withEnv<T>(name: string, value: string | undefined, action: () => T): T
|
||||
}
|
||||
}
|
||||
|
||||
// Mutates worker-global state: only use for synchronous, non-concurrent graph inspection.
|
||||
function withPlatform<T>(platform: NodeJS.Platform, action: () => T): T {
|
||||
const original = Object.getOwnPropertyDescriptor(process, 'platform')!
|
||||
Object.defineProperty(process, 'platform', { ...original, value: platform })
|
||||
try { return action() } finally { Object.defineProperty(process, 'platform', original) }
|
||||
}
|
||||
|
||||
// Mutates worker-global state: only use for synchronous, non-concurrent graph inspection.
|
||||
function withNodeVersion<T>(version: string, action: () => T): T {
|
||||
const original = Object.getOwnPropertyDescriptor(process.versions, 'node')!
|
||||
Object.defineProperty(process.versions, 'node', { ...original, value: version })
|
||||
try { return action() } finally { Object.defineProperty(process.versions, 'node', original) }
|
||||
}
|
||||
|
||||
describe('CI worker allocation', () => {
|
||||
it.each([1, 2, 4, 8, 16, 64])('shares a %i CPU coverage budget without multiplying pools', (cpus) => {
|
||||
const env = ciWorkerEnvironment('ci-coverage', {}, cpus)
|
||||
@@ -236,7 +250,7 @@ describe('gate graph validation', () => {
|
||||
await expect(runGates(subject, subject.length, execute)).resolves.toHaveLength(subject.length)
|
||||
})
|
||||
|
||||
it('builds the native addon before benchmarks through the ci-bench script chain', () => {
|
||||
it('uses the internal CI benchmark runner without changing the public benchmark scripts', () => {
|
||||
const subject = withPnpmEntrypoint(() => gatesForMode('ci-bench'))
|
||||
const { scripts } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as {
|
||||
scripts: Record<string, string>
|
||||
@@ -246,12 +260,12 @@ describe('gate graph validation', () => {
|
||||
expect(subject).toHaveLength(1)
|
||||
expect(subject[0]).toMatchObject({
|
||||
id: 'bench',
|
||||
displayCommand: 'pnpm run test:bench',
|
||||
args: ['/private/pnpm.cjs', 'run', 'test:bench'],
|
||||
displayCommand: 'pnpm exec tsx scripts/run-ci-bench.ts',
|
||||
args: ['/private/pnpm.cjs', 'exec', 'tsx', 'scripts/run-ci-bench.ts'],
|
||||
})
|
||||
expect(scripts['test:bench']).toBe('npm run build:bench && npm run build:web && npm run test:bench:built')
|
||||
expect(scripts['build:bench']).toBe(
|
||||
'npm run build:native-system && npm run build:lib && tsdown --config benchmarks/tsdown.config.ts',
|
||||
'npm run build:native-system && npm run build:lib && tsdown --config-loader native --config benchmarks/tsdown.config.ts',
|
||||
)
|
||||
expect(scripts['build:native-system']).toBe('tsx native/system/scripts/build.ts --host-addon-only')
|
||||
expect(scripts['test:bench:built']).toBe('vitest run --config vitest.bench.config.ts')
|
||||
@@ -277,6 +291,13 @@ describe('gate graph validation', () => {
|
||||
expect(ids).toContain('public-repository-links')
|
||||
})
|
||||
|
||||
it('checks the complete CLI reference locally and in the static CI lane', () => {
|
||||
for (const mode of ['doc-sync', 'ci-static'] as const) {
|
||||
const gates = withPnpmEntrypoint(() => gatesForMode(mode))
|
||||
expect(gates.find(gate => gate.id === 'cli-help')).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the concrete terminology policy in the documentation gate', () => {
|
||||
const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id))
|
||||
|
||||
@@ -333,10 +354,10 @@ describe('gate graph validation', () => {
|
||||
const ids = withPnpmEntrypoint(() => gatesForMode('hygiene').map(subject => subject.id))
|
||||
|
||||
expect(ids).toEqual([
|
||||
'rescope-vendor', 'publint', 'constraints', 'default-product-isolation', 'package-dependencies', 'application-entrypoints',
|
||||
'dsh-package-licenses', 'package-invariants', 'built-package-invariants', 'node-next-types',
|
||||
'rescope-vendor', 'publint', 'constraints', 'default-product-isolation', 'product-use', 'official-bundle-catalog', 'package-dependencies', 'application-entrypoints',
|
||||
'dsh-package-licenses', 'node-next-types',
|
||||
'optional-dependency-imports', 'client-packages', 'client-ui-i18n', 'client-route-resolution', 'no-bare-dispatcher',
|
||||
'no-unknown-casts',
|
||||
'plugin-record-callers', 'no-unknown-casts',
|
||||
'cordis-config', 'runtime-closure',
|
||||
])
|
||||
})
|
||||
@@ -354,8 +375,8 @@ describe('gate graph validation', () => {
|
||||
const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id))
|
||||
|
||||
expect(ids.slice(0, 11)).toEqual([
|
||||
'doc-typecheck', 'docs-site-build', 'doc-graphs', 'markdown-links', 'type-equivalence',
|
||||
'cordis-catalog', 'cordis-inspect-catalog', 'workflow-guest', 'mermaid', 'scoped-events', 'translation-pairing',
|
||||
'doc-typecheck', 'docs-site-build', 'doc-graphs', 'cli-help', 'markdown-links', 'type-equivalence',
|
||||
'cordis-catalog', 'cordis-inspect-catalog', 'workflow-guest', 'mermaid', 'translation-pairing',
|
||||
])
|
||||
})
|
||||
|
||||
@@ -456,7 +477,7 @@ describe('gate graph validation', () => {
|
||||
it('keeps native Windows coverage blocking and behind the complete build', () => {
|
||||
const complete = withPnpmEntrypoint(() => gatesForMode('ci-windows-complete'))
|
||||
const observational = withPnpmEntrypoint(() => gatesForMode('ci-windows-observational-ready'))
|
||||
.filter(gate => gate.id !== 'docs-site-build')
|
||||
.filter(gate => gate.id !== 'docs-site-build' && gate.id !== 'electron-install')
|
||||
const byId = new Map(complete.map(subject => [subject.id, subject]))
|
||||
|
||||
expect(byId.get('coverage')?.allowFailure).not.toBe(true)
|
||||
@@ -535,6 +556,22 @@ describe('gate graph validation', () => {
|
||||
expect(results.some(result => result.status === 'skipped')).toBe(false)
|
||||
})
|
||||
|
||||
it.each([undefined, '3'])('provisions Electron before Windows coverage with partition count %s', async (partitions) => {
|
||||
const gates = withEnv('DSH_COVERAGE_PARTITIONS', partitions, () =>
|
||||
withPnpmEntrypoint(() => gatesForMode('ci-windows-complete')))
|
||||
const installers = gates.filter(gate => gate.id === 'electron-install')
|
||||
expect(installers).toHaveLength(1)
|
||||
expect(installers[0]?.allowFailure).not.toBe(true)
|
||||
expect(installers[0]?.needs ?? []).not.toContain('build')
|
||||
expect(gates.find(gate => gate.id === 'coverage')?.needs).toContain('electron-install')
|
||||
let installed = false
|
||||
await runGates(gates, 8, async (subject) => {
|
||||
if (subject.id === 'electron-install') installed = true
|
||||
if (subject.id === 'coverage') expect(installed).toBe(true)
|
||||
return resultFor(subject, 'passed')
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['ci-windows-complete', 'ci-windows-observational-ready'] as const)(
|
||||
'provisions the locked Electron binary before built smoke in %s', (mode) => {
|
||||
const gates = withPnpmEntrypoint(() => gatesForMode(mode))
|
||||
@@ -562,17 +599,40 @@ describe('gate graph validation', () => {
|
||||
expect(results.find(result => result.gate.id === 'built-bin-smoke')?.status).toBe('skipped')
|
||||
expect(results.find(result => result.gate.id === 'doc-graphs')?.status).toBe('passed')
|
||||
if (mode === 'ci-windows-complete') {
|
||||
expect(results.filter(result => result.status === 'failed' && !result.gate.allowFailure)).toEqual([])
|
||||
expect(results.filter(result => result.status === 'failed' && !result.gate.allowFailure).map(result => result.gate.id))
|
||||
.toEqual(['electron-install'])
|
||||
expect(attempted).not.toContain('coverage')
|
||||
expect(results.find(result => result.gate.id === 'coverage')?.status).toBe('skipped')
|
||||
expect(results.find(result => result.gate.id === 'coverage-exempt-heavy')?.status).toBe('passed')
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
it.each(['ci-primary', 'ci-linux-primary', 'ci-artifacts', 'ci-windows-blocking'] as const)(
|
||||
'does not provision the observational Electron dependency in %s', (mode) => {
|
||||
expect(withPnpmEntrypoint(() => gatesForMode(mode)).some(gate => gate.id === 'electron-install')).toBe(false)
|
||||
it.each(['ci-coverage', 'ci-artifacts', 'ci-windows-blocking'] as const)(
|
||||
'only provisions Electron for native Windows coverage in %s', (mode) => {
|
||||
const original = Object.getOwnPropertyDescriptor(process, 'platform')
|
||||
for (const platform of ['linux', 'darwin', 'win32'] as const) {
|
||||
const gates = withPlatform(platform, () => withPnpmEntrypoint(() => gatesForMode(mode)))
|
||||
const needed = platform === 'win32' && mode === 'ci-coverage'
|
||||
expect(gates.filter(gate => gate.id === 'electron-install')).toHaveLength(needed ? 1 : 0)
|
||||
if (needed) {
|
||||
expect(gates.find(gate => gate.id === 'coverage')?.needs).toContain('electron-install')
|
||||
expect(gates.find(gate => gate.id === 'electron-install')?.env).toEqual({ ELECTRON_GET_USE_PROXY: '1' })
|
||||
expect(gates.filter(gate => gate.id !== 'electron-install').every(gate => gate.env?.ELECTRON_GET_USE_PROXY === undefined)).toBe(true)
|
||||
}
|
||||
}
|
||||
expect(Object.getOwnPropertyDescriptor(process, 'platform')).toEqual(original)
|
||||
},
|
||||
)
|
||||
|
||||
it('restores the platform descriptor when coverage configuration is rejected', () => {
|
||||
const original = Object.getOwnPropertyDescriptor(process, 'platform')
|
||||
expect(() => withEnv('DSH_COVERAGE_PARTITIONS', '1', () =>
|
||||
withPlatform('win32', () => withPnpmEntrypoint(() => gatesForMode('ci-coverage')))))
|
||||
.toThrow('DSH_COVERAGE_PARTITIONS must be an integer greater than 1')
|
||||
expect(Object.getOwnPropertyDescriptor(process, 'platform')).toEqual(original)
|
||||
})
|
||||
|
||||
it('leaves the lane test budget to the inherited environment on both coverage gates', () => {
|
||||
// vitest.config.ts reads DSH_COVERAGE_TEST_TIMEOUT_MS per inline project
|
||||
// (coverageTestTimeoutOptions); a gate argument would add a second owner.
|
||||
@@ -768,20 +828,76 @@ describe('Typert contract preparation', () => {
|
||||
})
|
||||
|
||||
describe('Node compatibility graph', () => {
|
||||
it('runs the jsdom environment smoke on every advertised Node line', () => {
|
||||
const subject = withPnpmEntrypoint(() => gatesForMode('node-compat'))
|
||||
it.each(['22.19.0', '24.9.0', '26.0.0'])('runs all source compatibility smokes serially on Node %s', (version) => {
|
||||
const subject = withNodeVersion(version, () =>
|
||||
withPnpmEntrypoint(() => gatesForMode('node-compat')))
|
||||
|
||||
expect(subject.find(item => item.id === 'vitest-jsdom-smoke')).toMatchObject({
|
||||
label: 'Vitest jsdom smoke',
|
||||
expect(subject.filter(item => item.id === 'source-compat-smokes')).toMatchObject([{
|
||||
label: 'source compatibility smokes',
|
||||
args: [
|
||||
'/private/pnpm.cjs',
|
||||
'exec',
|
||||
'vitest',
|
||||
'run',
|
||||
'packages/workflow/workflow-ptc/tests/source-runtime.compat.spec.ts',
|
||||
'packages/session/session-persistence-jsonl/tests/zstd.compat.spec.ts',
|
||||
'apps/cli/tests/source-launch.compat.spec.ts',
|
||||
'scripts/vitest-environment.compat.spec.ts',
|
||||
'packages/boot/app-boot/tests/profile-resolution.spec.ts',
|
||||
'packages/boot/app-boot/tests/profile-resolution-service.spec.ts',
|
||||
'packages/boot/app-boot/tests/profile-resolution-worker-bootstrap.spec.ts',
|
||||
'--no-file-parallelism',
|
||||
'--maxWorkers=1',
|
||||
],
|
||||
env: { VITEST_MAX_WORKERS: '1' },
|
||||
}])
|
||||
})
|
||||
|
||||
it.each(['1', '8'])('keeps source compatibility smokes serial with inherited VITEST_MAX_WORKERS=%s', (workers) => {
|
||||
const subject = withEnv('VITEST_MAX_WORKERS', workers, () =>
|
||||
withPnpmEntrypoint(() => gatesForMode('node-compat')))
|
||||
|
||||
expect(subject.find(item => item.id === 'source-compat-smokes')?.env).toEqual({ VITEST_MAX_WORKERS: '1' })
|
||||
})
|
||||
|
||||
it.each([undefined, '1'])('builds the complete Node 22 artifacts once with skip-typecheck=%s', (skipTypecheck) => {
|
||||
const subject = withNodeVersion('22.19.0', () =>
|
||||
withEnv('DSH_NODE_COMPAT_SKIP_TYPECHECK', skipTypecheck, () =>
|
||||
withPnpmEntrypoint(() => gatesForMode('node-compat'))))
|
||||
|
||||
expect(subject.filter(item => item.displayCommand === 'pnpm run build --artifacts-only')).toHaveLength(1)
|
||||
expect(subject.find(item => item.id === 'build')?.args).toEqual([
|
||||
'/private/pnpm.cjs', 'run', 'build', '--artifacts-only',
|
||||
])
|
||||
expect(subject.map(item => item.id)).not.toContain('build:web')
|
||||
expect(subject.find(item => item.id === 'cli-lazy-search-startup-smoke')).toMatchObject({
|
||||
needs: ['build'],
|
||||
env: { DSH_REQUIRE_BUILT_CLI_SMOKE: '1' },
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['passed', 'failed'] as const)('runs the Node 22 lazy-search smoke only after a passed build (%s)', async (buildStatus) => {
|
||||
const subject = withNodeVersion('22.19.0', () =>
|
||||
withEnv('DSH_NODE_COMPAT_SKIP_TYPECHECK', '1', () =>
|
||||
withPnpmEntrypoint(() => gatesForMode('node-compat'))))
|
||||
let buildCompleted = false
|
||||
let smokeStarted = false
|
||||
const results = await runGates(subject, 2, async (item) => {
|
||||
if (item.id === 'build') {
|
||||
buildCompleted = true
|
||||
return resultFor(item, buildStatus)
|
||||
}
|
||||
if (item.id === 'cli-lazy-search-startup-smoke') {
|
||||
expect(buildCompleted).toBe(true)
|
||||
smokeStarted = true
|
||||
}
|
||||
return resultFor(item)
|
||||
})
|
||||
|
||||
expect(smokeStarted).toBe(buildStatus === 'passed')
|
||||
expect(results.find(item => item.gate.id === 'cli-lazy-search-startup-smoke')?.status)
|
||||
.toBe(buildStatus === 'passed' ? 'passed' : 'skipped')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Node 24 lane ownership', () => {
|
||||
@@ -803,7 +919,6 @@ describe('Node 24 lane ownership', () => {
|
||||
'build',
|
||||
'node-compat',
|
||||
'publint',
|
||||
'built-package-invariants',
|
||||
'lint-and-duplication',
|
||||
'snapshot',
|
||||
'expected-output',
|
||||
@@ -819,8 +934,7 @@ describe('Node 24 lane ownership', () => {
|
||||
expect(subject.find(item => item.id === 'node-compat')?.env).toEqual({
|
||||
DSH_BUILD_CLIENT_PROFILE: 'official',
|
||||
})
|
||||
expect(subject.find(item => item.id === 'built-package-invariants')?.needs).toEqual(['build'])
|
||||
expect(subject.find(item => item.id === 'lint-and-duplication')?.needs).toEqual(['built-package-invariants'])
|
||||
expect(subject.find(item => item.id === 'lint-and-duplication')?.needs).toEqual(['build'])
|
||||
for (const id of [
|
||||
'snapshot',
|
||||
'expected-output',
|
||||
@@ -829,7 +943,7 @@ describe('Node 24 lane ownership', () => {
|
||||
'node-next-types',
|
||||
'built-bin-smoke',
|
||||
]) {
|
||||
expect(subject.find(item => item.id === id)?.needs).toEqual(['built-package-invariants'])
|
||||
expect(subject.find(item => item.id === id)?.needs).toEqual(['build'])
|
||||
}
|
||||
expect(subject.find(item => item.id === 'snapshot')?.env).toEqual({ DSH_EXAMPLE_MODE: 'lib' })
|
||||
expect(subject.find(item => item.id === 'expected-output')?.env).toEqual({ DSH_EXAMPLE_MODE: 'lib' })
|
||||
@@ -871,7 +985,7 @@ describe('Linux primary graph', () => {
|
||||
expect(web).toMatchObject({
|
||||
displayCommand: 'DSH_SNAPSHOT=replay pnpm run test:web:built',
|
||||
env: { DSH_SNAPSHOT: 'replay' },
|
||||
needs: ['built-package-invariants'],
|
||||
needs: ['build'],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+45
-64
@@ -3,7 +3,6 @@
|
||||
*
|
||||
* Package scripts own public aggregate names; this runner owns their validated
|
||||
* dependency graphs, scheduler environment, and process diagnostics.
|
||||
* @see ../.agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.md
|
||||
*/
|
||||
import { spawn, spawnSync } from 'node:child_process'
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
@@ -273,7 +272,7 @@ export function gatesForMode(selected: Mode): Gate[] {
|
||||
case 'ci-primary':
|
||||
return ciPrimaryGates()
|
||||
case 'ci-linux-primary':
|
||||
return [...ciPrimaryGates(), webSnapshotGate(['built-package-invariants'])]
|
||||
return [...ciPrimaryGates(), webSnapshotGate(['build'])]
|
||||
case 'ci-static':
|
||||
return ciStaticGates({ ownsBuild: false })
|
||||
case 'ci-lint-contracts-ready':
|
||||
@@ -286,7 +285,7 @@ export function gatesForMode(selected: Mode): Gate[] {
|
||||
case 'ci-unit':
|
||||
return ciUnitGates()
|
||||
case 'ci-bench':
|
||||
return [pnpmScript('bench', 'test:bench', { label: 'performance benchmarks' })]
|
||||
return [pnpmExec('bench', ['tsx', 'scripts/run-ci-bench.ts'], { label: 'performance benchmarks' })]
|
||||
case 'ci-snapshot':
|
||||
return [ciBuildGate(), snapshotGate()]
|
||||
case 'ci-artifacts':
|
||||
@@ -346,11 +345,12 @@ function ciSharedStaticGates(): Gate[] {
|
||||
return [
|
||||
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
|
||||
pnpmScript('default-product-isolation', 'verify-default-product-isolation', { label: 'default product isolation' }),
|
||||
pnpmScript('product-use', 'verify-product-use', { label: 'product package classification' }),
|
||||
pnpmScript('official-bundle-catalog', 'verify-official-bundle-catalog', { label: 'Official bundle catalog' }),
|
||||
pnpmScript('application-entrypoints', 'verify-application-entrypoints', { label: 'application entrypoints' }),
|
||||
pnpmScript('constraints', 'constraints'),
|
||||
pnpmScript('package-dependencies', 'verify-package-dependencies', { label: 'package dependencies' }),
|
||||
pnpmScript('dsh-package-licenses', 'verify-dsh-package-licenses', { label: 'DSH package licenses' }),
|
||||
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
|
||||
pnpmScript('package-meta', 'verify-package-meta', { label: 'package metadata' }),
|
||||
pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }),
|
||||
...sharedHygieneGates(),
|
||||
@@ -368,6 +368,7 @@ function sharedHygieneGates(): Gate[] {
|
||||
pnpmScript('client-ui-i18n', 'verify-client-ui-i18n', { label: 'client UI i18n' }),
|
||||
pnpmScript('client-route-resolution', 'verify-client-route-resolution', { label: 'client route resolution' }),
|
||||
pnpmScript('no-bare-dispatcher', 'verify-no-bare-dispatcher', { label: 'proxy-aware dispatchers' }),
|
||||
pnpmScript('plugin-record-callers', 'verify-plugin-record-callers', { label: 'plugin record callers' }),
|
||||
pnpmScript('no-unknown-casts', 'verify-no-unknown-casts', { label: 'no new unknown casts' }),
|
||||
]
|
||||
}
|
||||
@@ -396,7 +397,6 @@ function ciPrimaryGates(): Gate[] {
|
||||
label: 'node-next types',
|
||||
needs: ['build'],
|
||||
}),
|
||||
builtPackageInvariantsGate(['build']),
|
||||
builtBinSmokeGate(),
|
||||
]
|
||||
}
|
||||
@@ -411,45 +411,31 @@ function nodeCompatGates(): Gate[] {
|
||||
return [
|
||||
...typecheck,
|
||||
pnpmScript('build', 'build', {
|
||||
...pnpmInvocation(['run', 'build', '--artifacts-only']),
|
||||
displayCommand: 'pnpm run build --artifacts-only',
|
||||
...typecheck.length === 0 ? {} : { needs: ['typecheck'] },
|
||||
}),
|
||||
pnpmScript('build:web', 'build:web', {
|
||||
label: 'Web frontend build',
|
||||
needs: ['build'],
|
||||
}),
|
||||
...nodeCompatSmokeGates({ cliSmoke: true }),
|
||||
]
|
||||
}
|
||||
|
||||
function nodeCompatSmokeGates(options: { cliSmoke?: boolean } = {}): Gate[] {
|
||||
const gates: Gate[] = [
|
||||
pnpmExec('source-worker-smoke', [
|
||||
// Serial files keep CLI and PTC children from overlapping. Vitest's
|
||||
// environment worker override takes precedence over its CLI flags.
|
||||
pnpmExec('source-compat-smokes', [
|
||||
'vitest',
|
||||
'run',
|
||||
'packages/workflow/workflow-ptc/tests/source-runtime.compat.spec.ts',
|
||||
], { label: 'source worker smoke' }),
|
||||
pnpmExec('jsonl-zstd-smoke', [
|
||||
'vitest',
|
||||
'run',
|
||||
'packages/session/session-persistence-jsonl/tests/zstd.compat.spec.ts',
|
||||
], { label: 'JSONL Zstandard smoke' }),
|
||||
pnpmExec('dsh-source-launch-smoke', [
|
||||
'vitest',
|
||||
'run',
|
||||
'apps/cli/tests/source-launch.compat.spec.ts',
|
||||
], { label: 'dsh source-launch smoke' }),
|
||||
pnpmExec('vitest-jsdom-smoke', [
|
||||
'vitest',
|
||||
'run',
|
||||
'scripts/vitest-environment.compat.spec.ts',
|
||||
], { label: 'Vitest jsdom smoke' }),
|
||||
pnpmExec('profile-resolution-smoke', [
|
||||
'vitest',
|
||||
'run',
|
||||
'packages/boot/app-boot/tests/profile-resolution.spec.ts',
|
||||
'packages/boot/app-boot/tests/profile-resolution-service.spec.ts',
|
||||
'packages/boot/app-boot/tests/profile-resolution-worker-bootstrap.spec.ts',
|
||||
], { label: 'profile resolution smoke' }),
|
||||
'--no-file-parallelism',
|
||||
'--maxWorkers=1',
|
||||
], { label: 'source compatibility smokes', env: { VITEST_MAX_WORKERS: '1' } }),
|
||||
]
|
||||
if (options.cliSmoke) {
|
||||
gates.push(
|
||||
@@ -460,7 +446,7 @@ function nodeCompatSmokeGates(options: { cliSmoke?: boolean } = {}): Gate[] {
|
||||
], {
|
||||
label: 'CLI lazy-search startup smoke',
|
||||
env: { DSH_REQUIRE_BUILT_CLI_SMOKE: '1' },
|
||||
needs: ['build:web'],
|
||||
needs: ['build'],
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -503,14 +489,12 @@ function ciArtifactGates(): Gate[] {
|
||||
label: 'node-next types',
|
||||
needs: ['build'],
|
||||
}),
|
||||
builtPackageInvariantsGate(['build']),
|
||||
builtBinSmokeGate(),
|
||||
]
|
||||
}
|
||||
|
||||
function ciConsumerGates(): Gate[] {
|
||||
const builtTree = ['build']
|
||||
const validatedBuild = ['built-package-invariants']
|
||||
// The HMR web test starts `dev:web`, which rewrites the shared `lib/` and
|
||||
// `apps/web/dist/` trees. Let every build-artifact reader settle before that
|
||||
// writer starts; `after` preserves the web diagnostic even if a reader fails.
|
||||
@@ -530,23 +514,22 @@ function ciConsumerGates(): Gate[] {
|
||||
env: { [CLIENT_BUILD_PROFILE_SELECTOR]: 'official' },
|
||||
}),
|
||||
pnpmScript('publint', 'publint', { needs: builtTree }),
|
||||
builtPackageInvariantsGate(builtTree),
|
||||
pnpmScript('lint-and-duplication', 'check:ci:lint:contracts-ready', {
|
||||
label: 'lint and duplication',
|
||||
needs: validatedBuild,
|
||||
needs: builtTree,
|
||||
}),
|
||||
snapshotGate(validatedBuild),
|
||||
expectedOutputGate(validatedBuild),
|
||||
webSnapshotGate(validatedBuild, buildArtifactReaders),
|
||||
snapshotGate(builtTree),
|
||||
expectedOutputGate(builtTree),
|
||||
webSnapshotGate(builtTree, buildArtifactReaders),
|
||||
pnpmScript('doc-typecheck', 'doc-typecheck:contracts-ready', {
|
||||
needs: validatedBuild,
|
||||
needs: builtTree,
|
||||
env: { DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1' },
|
||||
}),
|
||||
pnpmScript('node-next-types', 'verify-node-next-types', {
|
||||
label: 'node-next types',
|
||||
needs: validatedBuild,
|
||||
needs: builtTree,
|
||||
}),
|
||||
builtBinSmokeGate(validatedBuild),
|
||||
builtBinSmokeGate(builtTree),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -582,15 +565,15 @@ function ciWindowsBlockingGates(): Gate[] {
|
||||
}
|
||||
|
||||
function ciWindowsCompleteGates(): Gate[] {
|
||||
const coverage = coverageGates().map(gate => ({
|
||||
const coverage = coverageGates('win32').map(gate => gate.id === 'electron-install' ? gate : {
|
||||
...gate,
|
||||
needs: [...new Set(['build', ...(gate.needs ?? [])])],
|
||||
}))
|
||||
})
|
||||
const coverageAfter = coverage.map(gate => gate.id)
|
||||
const observational = ciWindowsObservationalGates()
|
||||
// The required production site replaces the observational MPA build; both
|
||||
// VitePress modes write the same output directory and cannot overlap.
|
||||
.filter(gate => gate.id !== 'build' && gate.id !== 'docs-site-build')
|
||||
// Coverage owns Electron preparation. The required production site replaces
|
||||
// the MPA build, which writes to the same output directory.
|
||||
.filter(gate => gate.id !== 'build' && gate.id !== 'docs-site-build' && gate.id !== 'electron-install')
|
||||
.map(gate => ({
|
||||
...gate,
|
||||
allowFailure: true,
|
||||
@@ -607,17 +590,21 @@ function ciWindowsCompleteGates(): Gate[] {
|
||||
]
|
||||
}
|
||||
|
||||
// Native Electron fixtures need the locked binary before Vitest removes ambient proxies.
|
||||
function electronInstallGate(): Gate {
|
||||
return {
|
||||
id: 'electron-install',
|
||||
label: 'Electron binary',
|
||||
displayCommand: 'pnpm --filter @deepseek-ai/dsh-desktop exec install-electron',
|
||||
...pnpmInvocation(['--filter', '@deepseek-ai/dsh-desktop', 'exec', 'install-electron']),
|
||||
env: { ELECTRON_GET_USE_PROXY: '1' },
|
||||
}
|
||||
}
|
||||
|
||||
function ciWindowsObservationalGates(): Gate[] {
|
||||
const predecessors = [
|
||||
...ciStaticGates({ ownsBuild: true }),
|
||||
// Electron's lazy download must finish before Vitest removes ambient proxies.
|
||||
{
|
||||
id: 'electron-install',
|
||||
label: 'Electron binary',
|
||||
displayCommand: 'pnpm --filter @deepseek-ai/dsh-desktop exec install-electron',
|
||||
...pnpmInvocation(['--filter', '@deepseek-ai/dsh-desktop', 'exec', 'install-electron']),
|
||||
env: { ELECTRON_GET_USE_PROXY: '1' },
|
||||
},
|
||||
electronInstallGate(),
|
||||
// Linux owns required lint and snapshots; Windows omits those duplicates.
|
||||
pnpmScript('duplication', 'duplication'),
|
||||
pnpmScript('publint', 'publint', { needs: ['build'] }),
|
||||
@@ -625,7 +612,6 @@ function ciWindowsObservationalGates(): Gate[] {
|
||||
label: 'node-next types',
|
||||
needs: ['build'],
|
||||
}),
|
||||
builtPackageInvariantsGate(['build']),
|
||||
]
|
||||
return [
|
||||
...predecessors,
|
||||
@@ -679,7 +665,8 @@ function coverageWorkerArgs(): { instrumented: string[]; exempt: string[] } {
|
||||
}
|
||||
}
|
||||
|
||||
function coverageGates(): Gate[] {
|
||||
function coverageGates(platform: NodeJS.Platform = process.platform): Gate[] {
|
||||
const electron = platform === 'win32' ? [electronInstallGate()] : []
|
||||
const workers = coverageWorkerArgs()
|
||||
const partitions = parseCoveragePartitionCount(process.env[COVERAGE_PARTITIONS_ENV])
|
||||
const instrumented = partitions === undefined
|
||||
@@ -700,7 +687,8 @@ function coverageGates(): Gate[] {
|
||||
})
|
||||
return [
|
||||
pnpmScript('native-system', 'build:native-system'),
|
||||
{ ...instrumented, needs: ['native-system'] },
|
||||
...electron,
|
||||
{ ...instrumented, needs: ['native-system', ...electron.map(gate => gate.id)] },
|
||||
pnpmExec('coverage-exempt-heavy', [
|
||||
'vitest',
|
||||
'run',
|
||||
@@ -748,13 +736,6 @@ function expectedOutputGate(needs: string[] = ['build']): Gate {
|
||||
})
|
||||
}
|
||||
|
||||
function builtPackageInvariantsGate(needs?: string[]): Gate {
|
||||
return pnpmScript('built-package-invariants', 'verify-built-package-invariants', {
|
||||
label: 'built package invariants',
|
||||
...needs === undefined ? {} : { needs },
|
||||
})
|
||||
}
|
||||
|
||||
function positiveIntArg(envName: string, flag: string): string[] {
|
||||
const raw = process.env[envName]
|
||||
if (raw === undefined || raw === '') return []
|
||||
@@ -779,11 +760,11 @@ function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] {
|
||||
pnpmScript('publint', 'publint', artifactOptions),
|
||||
pnpmScript('constraints', 'constraints'),
|
||||
pnpmScript('default-product-isolation', 'verify-default-product-isolation', { label: 'default product isolation' }),
|
||||
pnpmScript('product-use', 'verify-product-use', { label: 'product package classification' }),
|
||||
pnpmScript('official-bundle-catalog', 'verify-official-bundle-catalog', { label: 'Official bundle catalog' }),
|
||||
pnpmScript('package-dependencies', 'verify-package-dependencies', { label: 'package dependencies' }),
|
||||
pnpmScript('application-entrypoints', 'verify-application-entrypoints', { label: 'application entrypoints' }),
|
||||
pnpmScript('dsh-package-licenses', 'verify-dsh-package-licenses', { label: 'DSH package licenses' }),
|
||||
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
|
||||
builtPackageInvariantsGate(options.artifactNeeds),
|
||||
pnpmScript('node-next-types', 'verify-node-next-types', {
|
||||
label: 'node-next types',
|
||||
...artifactOptions,
|
||||
@@ -809,13 +790,13 @@ function docSyncLeafGates(options: {
|
||||
: [pnpmScript('doc-typecheck', options.docTypecheckScript ?? 'doc-typecheck', docTypecheckOptions)],
|
||||
pnpmScript('docs-site-build', options.docsBuildScript ?? 'docs:build', { label: 'documentation build' }),
|
||||
pnpmScript('doc-graphs', 'verify-doc-graphs', { label: 'doc graphs' }),
|
||||
pnpmScript('cli-help', 'verify-cli-help', { label: 'CLI help reference' }),
|
||||
pnpmScript('markdown-links', 'verify-md-links', { label: 'markdown links', quick: true }),
|
||||
pnpmScript('type-equivalence', 'verify-type-equiv', { label: 'type equivalence', quick: true }),
|
||||
pnpmScript('cordis-catalog', 'verify-cordis-catalog', { label: 'cordis catalog' }),
|
||||
pnpmScript('cordis-inspect-catalog', 'verify-cordis-inspect-catalog', { label: 'Cordis inspect catalog' }),
|
||||
pnpmScript('workflow-guest', 'verify-workflow-guest', { label: 'workflow guest source' }),
|
||||
pnpmScript('mermaid', 'verify-mermaid'),
|
||||
pnpmScript('scoped-events', 'verify-scoped-events', { label: 'scoped events' }),
|
||||
pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing', quick: true }),
|
||||
pnpmScript('markdown-wrap', 'verify-md-wrap', { label: 'markdown wrap', quick: true }),
|
||||
pnpmScript('client-catalog', 'verify-client-catalog', { label: 'client catalog' }),
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
MEMORY_PRESSURE_FILES,
|
||||
memoryLimitViolation,
|
||||
memoryPressureMachine,
|
||||
memoryScopeCommand,
|
||||
} from './run-memory-pressure-bench.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
describe('memory-pressure benchmark launcher', () => {
|
||||
it('selects existing benchmark files', () => {
|
||||
for (const file of MEMORY_PRESSURE_FILES) expect(existsSync(resolve(root, file)), file).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts only the modeled machines', () => {
|
||||
expect(memoryPressureMachine('4g')).toBe('4g')
|
||||
expect(memoryPressureMachine('8g')).toBe('8g')
|
||||
expect(() => memoryPressureMachine('16g')).toThrow('usage')
|
||||
expect(() => memoryPressureMachine(undefined)).toThrow('usage')
|
||||
})
|
||||
|
||||
it('confines the command to the machine memory without swap', () => {
|
||||
expect(memoryScopeCommand('4g', ['node', 'x.js'])).toEqual([
|
||||
'systemd-run', '--user', '--scope', '--quiet', '-p', 'MemoryMax=4G', '-p', 'MemorySwapMax=0', '--', 'node', 'x.js',
|
||||
])
|
||||
})
|
||||
|
||||
it('accepts only a cgroup capped at the machine size without swap', () => {
|
||||
const capped = { max: '4294967296', swapMax: '0', constrained: 4 * 1024 ** 3 }
|
||||
expect(memoryLimitViolation('4g', capped)).toBeUndefined()
|
||||
for (const limits of [
|
||||
{ ...capped, max: 'max' },
|
||||
{ ...capped, max: '8589934592' },
|
||||
{ ...capped, swapMax: 'max' },
|
||||
{ ...capped, swapMax: '4294967296' },
|
||||
{ ...capped, constrained: 2 * 1024 ** 3 },
|
||||
]) {
|
||||
expect(memoryLimitViolation('4g', limits)).toContain('does not cap 4g without swap')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,147 @@
|
||||
/** Run the memory-sensitive benchmark files inside a Linux cgroup that models a 4 GB or 8 GB machine. */
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { getHeapStatistics } from 'node:v8'
|
||||
import { pnpmCommand } from './release/process.ts'
|
||||
import { buildCiBenchArtifacts } from './run-ci-bench.ts'
|
||||
|
||||
/** Modeled machines: memory available to the whole benchmark process tree, without swap. */
|
||||
export const MEMORY_PRESSURE_MACHINES = { '4g': 4, '8g': 8 } as const
|
||||
|
||||
/** Modeled machine name. */
|
||||
export type MemoryPressureMachine = keyof typeof MEMORY_PRESSURE_MACHINES
|
||||
|
||||
/**
|
||||
* Benchmark files that run under the limit. They cover the Host operations with the largest measured
|
||||
* resident memory, the longest Session open, and the Chromium long-history workflow with its Host.
|
||||
*/
|
||||
export const MEMORY_PRESSURE_FILES = [
|
||||
'benchmarks/session-corpus/session-corpus.bench.ts',
|
||||
'benchmarks/session-corpus/projection-list.bench.ts',
|
||||
'benchmarks/session-open/session-open.bench.ts',
|
||||
'benchmarks/long-session-browser/long-session.bench.ts',
|
||||
] as const
|
||||
|
||||
const USAGE = `usage: run-memory-pressure-bench.ts <${Object.keys(MEMORY_PRESSURE_MACHINES).join('|')}>`
|
||||
|
||||
/**
|
||||
* Parse a modeled machine name.
|
||||
* @param value - Command-line machine name.
|
||||
* @returns The machine name.
|
||||
* @throws If the name is not a modeled machine.
|
||||
*/
|
||||
export function memoryPressureMachine(value: string | undefined): MemoryPressureMachine {
|
||||
if (value !== undefined && Object.hasOwn(MEMORY_PRESSURE_MACHINES, value)) return value as MemoryPressureMachine
|
||||
throw new Error(USAGE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the systemd transient-scope command that confines `command` to the machine's memory.
|
||||
* @param machine - Modeled machine.
|
||||
* @param command - Command and arguments to run inside the scope.
|
||||
* @returns The `systemd-run` argument vector.
|
||||
*/
|
||||
export function memoryScopeCommand(machine: MemoryPressureMachine, command: readonly string[]): string[] {
|
||||
return [
|
||||
'systemd-run', '--user', '--scope', '--quiet',
|
||||
'-p', `MemoryMax=${String(MEMORY_PRESSURE_MACHINES[machine])}G`,
|
||||
'-p', 'MemorySwapMax=0',
|
||||
'--', ...command,
|
||||
]
|
||||
}
|
||||
|
||||
function run(root: string, command: readonly string[]): number {
|
||||
const [file, ...args] = command
|
||||
if (file === undefined) throw new Error('run-memory-pressure-bench: empty command')
|
||||
const result = spawnSync(file, args, { cwd: root, stdio: 'inherit' })
|
||||
if (result.error !== undefined) throw result.error
|
||||
return result.status ?? 1
|
||||
}
|
||||
|
||||
function cgroupFile(name: string): string {
|
||||
const line = readFileSync('/proc/self/cgroup', 'utf8').split('\n').find(entry => entry.startsWith('0::'))
|
||||
if (line === undefined) throw new Error('run-memory-pressure-bench: the process is not in a cgroup v2 hierarchy')
|
||||
return readFileSync(join('/sys/fs/cgroup', line.slice(3), name), 'utf8').trim()
|
||||
}
|
||||
|
||||
/** Memory controls of the cgroup that contains the benchmark process tree. */
|
||||
export interface CgroupMemoryLimits {
|
||||
/** `memory.max` contents. */
|
||||
readonly max: string
|
||||
/** `memory.swap.max` contents. */
|
||||
readonly swapMax: string
|
||||
/** `process.constrainedMemory()` in bytes, which also reflects ancestor cgroups. */
|
||||
readonly constrained: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe why a cgroup does not model the machine.
|
||||
* @param machine - Modeled machine.
|
||||
* @param limits - Memory controls of the current cgroup.
|
||||
* @returns The violation, or `undefined` when memory is capped at the machine's size without swap.
|
||||
*/
|
||||
export function memoryLimitViolation(machine: MemoryPressureMachine, limits: CgroupMemoryLimits): string | undefined {
|
||||
const limitBytes = String(MEMORY_PRESSURE_MACHINES[machine] * 1024 ** 3)
|
||||
if (limits.max === limitBytes && limits.swapMax === '0' && String(limits.constrained) === limitBytes) return undefined
|
||||
return `the cgroup does not cap ${machine} without swap: memory.max=${limits.max}, `
|
||||
+ `memory.swap.max=${limits.swapMax}, constrainedMemory=${String(limits.constrained)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the selected benchmark files inside the current cgroup and report its memory facts.
|
||||
* `memory.peak` and `oom_kill` cover the cgroup's lifetime: a fresh systemd scope contains only this run, while a
|
||||
* container cgroup also contains earlier commands in that container. `memory.peak` needs Linux 5.19 or later.
|
||||
* @param root - Repository root with built benchmark artifacts.
|
||||
* @param machine - Modeled machine that the current cgroup must enforce.
|
||||
* @returns The benchmark exit status.
|
||||
* @throws If the current cgroup does not cap memory at the machine's size without swap.
|
||||
*/
|
||||
export function runScopedMemoryPressureBench(root: string, machine: MemoryPressureMachine): number {
|
||||
const violation = memoryLimitViolation(machine, {
|
||||
max: cgroupFile('memory.max'),
|
||||
swapMax: cgroupFile('memory.swap.max'),
|
||||
constrained: process.constrainedMemory(),
|
||||
})
|
||||
if (violation !== undefined) throw new Error(`run-memory-pressure-bench: ${violation}`)
|
||||
const status = run(root, [...pnpmCommand(), 'exec', 'vitest', 'run', '--config', 'vitest.bench.config.ts', ...MEMORY_PRESSURE_FILES])
|
||||
const oomKills = /^oom_kill (\d+)$/m.exec(cgroupFile('memory.events'))?.[1]
|
||||
if (oomKills === undefined) throw new Error('run-memory-pressure-bench: memory.events has no oom_kill counter')
|
||||
console.log(JSON.stringify({
|
||||
benchmark: `memory-pressure/${machine}`,
|
||||
limitMb: MEMORY_PRESSURE_MACHINES[machine] * 1024,
|
||||
// Heap limit Node derives from the cgroup for a process without an explicit `--max-old-space-size`.
|
||||
defaultNodeHeapLimitMb: Math.round(getHeapStatistics().heap_size_limit / 1024 ** 2),
|
||||
cgroupPeakMb: Math.round(Number(cgroupFile('memory.peak')) / 1024 ** 2),
|
||||
oomKills: Number(oomKills),
|
||||
files: MEMORY_PRESSURE_FILES,
|
||||
status,
|
||||
}))
|
||||
return status
|
||||
}
|
||||
|
||||
/**
|
||||
* Build benchmark artifacts unconfined, then rerun this script inside a transient memory-limited scope.
|
||||
* @param root - Repository root.
|
||||
* @param machine - Modeled machine.
|
||||
* @returns The scoped benchmark exit status.
|
||||
* @throws If the host is not Linux or a build fails.
|
||||
*/
|
||||
export function runMemoryPressureBench(root: string, machine: MemoryPressureMachine): number {
|
||||
if (process.platform !== 'linux') {
|
||||
throw new Error('run-memory-pressure-bench: memory limits need Linux cgroup v2 and a systemd user manager')
|
||||
}
|
||||
buildCiBenchArtifacts(root)
|
||||
return run(root, memoryScopeCommand(machine, [
|
||||
...pnpmCommand(), 'exec', 'tsx', 'scripts/run-memory-pressure-bench.ts', '--scoped', machine,
|
||||
]))
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const args = process.argv.slice(2)
|
||||
process.exitCode = args[0] === '--scoped'
|
||||
? runScopedMemoryPressureBench(root, memoryPressureMachine(args[1]))
|
||||
: runMemoryPressureBench(root, memoryPressureMachine(args[0]))
|
||||
}
|
||||
@@ -39,6 +39,18 @@ describe('recorded-session corpus policy', () => {
|
||||
.toEqual({ currentRoles: 8, baselineRoles: 8, retainedRoles: 3, retainedScenarios: 3 })
|
||||
})
|
||||
|
||||
it.each([
|
||||
[SESSION_FORMAT_VERSION, 3],
|
||||
[3, SESSION_FORMAT_VERSION],
|
||||
])('counts baseline and current roles independently for parent v%i and child v%i', (parent, child) => {
|
||||
expect(assertSnapshotCorpusPolicy([
|
||||
current,
|
||||
{ key: 'sdk/mixed', selectedVersions: [parent, child] },
|
||||
completeV0,
|
||||
...adjacent,
|
||||
])).toEqual({ currentRoles: 9, baselineRoles: 1, retainedRoles: 3, retainedScenarios: 3 })
|
||||
})
|
||||
|
||||
it('rejects refreshing away every direct V3 migration input', () => {
|
||||
expect(() => assertSnapshotCorpusPolicy([current, completeV0, ...adjacent]))
|
||||
.toThrow('Session corpus lacks v3 coverage: adjacent-migration')
|
||||
@@ -85,15 +97,18 @@ describe('recorded-session corpus policy', () => {
|
||||
])).toThrow('session/v0: v0 retained coverage must be multi-hop, packed-row, retry-failure, shipped-profile')
|
||||
})
|
||||
|
||||
it('rejects absent roles, undeclared history, and mixed selected generations', () => {
|
||||
it('rejects absent roles, undeclared history, and mixed explicitly retained generations', () => {
|
||||
expect(() => assertSnapshotCorpusPolicy([{ key: 'session/empty', selectedVersions: [] }]))
|
||||
.toThrow('session/empty: scenario owns no selected Session role')
|
||||
expect(() => assertSnapshotCorpusPolicy([{ key: 'session/old', selectedVersions: [1] }]))
|
||||
.toThrow(`session/old: selected Session generation v1 must be retained baseline v3 or current v${SESSION_FORMAT_VERSION}`)
|
||||
expect(() => assertSnapshotCorpusPolicy([{ ...completeV0, selectedVersions: [0, SESSION_FORMAT_VERSION] }]))
|
||||
.toThrow(`session/v0: selected Session generation v${SESSION_FORMAT_VERSION} does not match expected v0`)
|
||||
expect(() => assertSnapshotCorpusPolicy([{ ...baseline, selectedVersions: [3, SESSION_FORMAT_VERSION] }]))
|
||||
.toThrow(`session/v3: selected Session generation v${SESSION_FORMAT_VERSION} does not match expected v3`)
|
||||
})
|
||||
|
||||
it.each([2, SESSION_FORMAT_VERSION + 1])('rejects unsupported child generation %s', (version) => {
|
||||
expect(() => assertSnapshotCorpusPolicy([{ key: 'sdk/invalid-child', selectedVersions: [3, version] }]))
|
||||
.toThrow(`sdk/invalid-child: selected Session generation v${version} must be retained baseline v3 or current v${SESSION_FORMAT_VERSION}`)
|
||||
})
|
||||
|
||||
it('rejects future unpinned generations', () => {
|
||||
|
||||
@@ -32,7 +32,7 @@ const REQUIRED_V0_COVERAGE = new Set([
|
||||
const REQUIRED_ADJACENT_COVERAGE = new Set(['adjacent-migration'])
|
||||
|
||||
/**
|
||||
* Require a baseline/current majority and direct coverage of each historical migration source.
|
||||
* Count baseline/current generations per role, require their majority, and preserve direct historical migration coverage.
|
||||
*
|
||||
* @param scenarios - Every owning top-level recorded-session scenario.
|
||||
* @returns Accepted current, baseline, and explicitly retained role counts.
|
||||
@@ -47,29 +47,30 @@ export function assertSnapshotCorpusPolicy(
|
||||
const coverageByVersion = new Map<number, Set<string>>()
|
||||
|
||||
for (const scenario of scenarios) {
|
||||
const selectedVersion = scenario.selectedVersions[0]
|
||||
if (selectedVersion === undefined) {
|
||||
if (scenario.selectedVersions.length === 0) {
|
||||
throw new Error(`${scenario.key}: scenario owns no selected Session role`)
|
||||
}
|
||||
const expectedVersion = scenario.retained?.version ?? selectedVersion
|
||||
if (scenario.retained === undefined) {
|
||||
for (const version of scenario.selectedVersions) {
|
||||
if (version === RETAINED_BASELINE_VERSION) {
|
||||
baselineRoles += 1
|
||||
} else if (version === SESSION_FORMAT_VERSION) {
|
||||
currentRoles += 1
|
||||
} else {
|
||||
throw new Error(
|
||||
`${scenario.key}: selected Session generation v${version} must be retained baseline v${RETAINED_BASELINE_VERSION} or current v${SESSION_FORMAT_VERSION}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
const expectedVersion = scenario.retained.version
|
||||
const mismatched = scenario.selectedVersions.find(version => version !== expectedVersion)
|
||||
if (mismatched !== undefined) {
|
||||
throw new Error(
|
||||
`${scenario.key}: selected Session generation v${mismatched} does not match expected v${expectedVersion}`,
|
||||
)
|
||||
}
|
||||
if (scenario.retained === undefined) {
|
||||
if (selectedVersion === RETAINED_BASELINE_VERSION) {
|
||||
baselineRoles += scenario.selectedVersions.length
|
||||
} else if (selectedVersion === SESSION_FORMAT_VERSION) {
|
||||
currentRoles += scenario.selectedVersions.length
|
||||
} else {
|
||||
throw new Error(
|
||||
`${scenario.key}: selected Session generation v${selectedVersion} must be retained baseline v${RETAINED_BASELINE_VERSION} or current v${SESSION_FORMAT_VERSION}`,
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
const retiredTools = scenario.retained.coverage.length === 1 && scenario.retained.coverage[0] === 'retired-tools'
|
||||
if (!Number.isSafeInteger(scenario.retained.version)
|
||||
|| scenario.retained.version < 0 || scenario.retained.version > SESSION_FORMAT_VERSION
|
||||
|
||||
+148
-86
@@ -22,10 +22,10 @@ import time
|
||||
import zipfile
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from typing import TYPE_CHECKING, Callable, Literal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from deepseek_harness import RunResult
|
||||
from deepseek_harness import Notification, RunResult
|
||||
|
||||
|
||||
EXPECTED_TEXT = "runtime smoke ok"
|
||||
@@ -85,7 +85,6 @@ LEGACY_CUSTOM_DISABLED_ROWS = (
|
||||
"tool-fs",
|
||||
"tool-fs-search",
|
||||
"tool-goal",
|
||||
"tool-ralph",
|
||||
"tool-skill",
|
||||
"tool-str-replace-editor",
|
||||
"tool-subagent-control",
|
||||
@@ -115,11 +114,10 @@ SNAPSHOT_WORKFLOW_SCRIPT = (
|
||||
f"const reply = await agent('{SNAPSHOT_WORKFLOW_CHILD_PROMPT}', {{ label: 'workflow-child' }})\n"
|
||||
"return { reply }"
|
||||
)
|
||||
ADVANCED_SNAPSHOT_DIRECTORY = (
|
||||
Path(__file__).resolve().parent / "snapshots" / "python-sdk-single-exe" / "advanced"
|
||||
)
|
||||
ADVANCED_SNAPSHOT_ROOT = Path(__file__).resolve().parent / "snapshots" / "python-sdk-single-exe"
|
||||
ADVANCED_SNAPSHOT_DIRECTORY = ADVANCED_SNAPSHOT_ROOT / "advanced"
|
||||
ADVANCED_SNAPSHOT_FILENAMES = (
|
||||
"result.json", "session.v3.jsonl", "session.1.v3.jsonl", "session.2.v3.jsonl",
|
||||
"result.json", "session.v4.jsonl", "session.1.v4.jsonl", "session.2.v4.jsonl",
|
||||
)
|
||||
MINIMAL_SNAPSHOT_DIRECTORY = (
|
||||
Path(__file__).resolve().parent / "snapshots" / "python-sdk-single-exe" / "minimal"
|
||||
@@ -139,7 +137,7 @@ RESTART_SNAPSHOT_DIRECTORY = (
|
||||
Path(__file__).resolve().parent / "snapshots" / "python-sdk-single-exe" / "restart"
|
||||
)
|
||||
RESTART_SNAPSHOT_FILENAMES = (
|
||||
"result.json", "requests.json", "session.1.v3.jsonl", "session.2.v3.jsonl",
|
||||
"result.json", "requests.json", "session.1.v4.jsonl", "session.2.v4.jsonl",
|
||||
)
|
||||
RECOVERY_SNAPSHOT_DIRECTORY = (
|
||||
Path(__file__).resolve().parent / "snapshots" / "python-sdk-single-exe" / "scheduler-recovery"
|
||||
@@ -242,10 +240,12 @@ def write_profile_patch(
|
||||
return path
|
||||
|
||||
|
||||
def write_advanced_profile_patch(root: Path, name: str, sessions: Path) -> Path:
|
||||
def write_advanced_profile_patch(
|
||||
root: Path, name: str, sessions: Path, mode: Literal["native", "ptc"] = "native",
|
||||
) -> Path:
|
||||
"""Write the shared custom, snapshot, and restart profile patch."""
|
||||
return write_profile_patch(root, name, sessions, [
|
||||
{"id": "tools", "config": {"mode": "both"}},
|
||||
{"id": "tools", "config": {"mode": mode}},
|
||||
{
|
||||
"id": "system-prompt",
|
||||
"config": {
|
||||
@@ -261,7 +261,6 @@ def write_advanced_profile_patch(root: Path, name: str, sessions: Path) -> Path:
|
||||
"config": {
|
||||
"provider": "spawn",
|
||||
"toolName": "subagent",
|
||||
"backgroundMode": "one-shot",
|
||||
},
|
||||
},
|
||||
{"insert": [
|
||||
@@ -295,6 +294,7 @@ class MockModelHandler(BaseHTTPRequestHandler):
|
||||
"""Return deterministic text, worker, and orchestration completions."""
|
||||
|
||||
requests: list[dict[str, object]] = []
|
||||
direct_child_finished = threading.Event()
|
||||
|
||||
def do_POST(self) -> None:
|
||||
if self.path != "/v1/messages":
|
||||
@@ -306,8 +306,13 @@ class MockModelHandler(BaseHTTPRequestHandler):
|
||||
self.send_response(200)
|
||||
self.send_header("content-type", "text/event-stream")
|
||||
self.end_headers()
|
||||
opening = message_start()
|
||||
self.wfile.write(f"event: {opening['type']}\ndata: {json.dumps(opening)}\n\n".encode())
|
||||
self.wfile.flush()
|
||||
chunks = completion_chunks(body)
|
||||
for chunk in chunks:
|
||||
if chunks[0] != opening:
|
||||
raise AssertionError("model fixture response must begin with message_start")
|
||||
for chunk in chunks[1:]:
|
||||
self.wfile.write(f"event: {chunk['type']}\ndata: {json.dumps(chunk)}\n\n".encode())
|
||||
self.wfile.flush()
|
||||
|
||||
@@ -440,13 +445,16 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
|
||||
if prompt == SNAPSHOT_WORKFLOW_CHILD_PROMPT:
|
||||
return text_chunks("WORKFLOW_CHILD_OK")
|
||||
if prompt == SNAPSHOT_PROMPT:
|
||||
if "run_code" in advertised_tool_names(body):
|
||||
if advertised_tool_names(body) != {"run_code"}:
|
||||
raise AssertionError("PTC mode must advertise only run_code")
|
||||
return tool_call_chunks(
|
||||
"advanced-code", "run_code",
|
||||
{"code": "return await tools.snapshot_double({ value: 21 })",
|
||||
"description": "Call the configured Plugin tool"},
|
||||
)
|
||||
assert_advertised_tool(body, "snapshot_double")
|
||||
assert_advertised_tool(body, "run_code")
|
||||
return tool_call_chunks(
|
||||
"advanced-code", "run_code",
|
||||
{"code": "return await tools.snapshot_double({ value: 21 })",
|
||||
"description": "Call the configured Plugin tool"},
|
||||
)
|
||||
return tool_call_chunks("advanced-native", "snapshot_double", {"value": 21})
|
||||
if prompt == RESTART_FIRST_PROMPT:
|
||||
return text_chunks(RESTART_FIRST_TEXT)
|
||||
if prompt == DYNAMIC_TOOLS_PROMPT:
|
||||
@@ -615,15 +623,13 @@ def advanced_tool_followup(
|
||||
"""Advance the executable snapshot's deterministic parent tool chain."""
|
||||
if not call_id.startswith("advanced-"):
|
||||
return None
|
||||
if call_id == "advanced-native" and tool_name == "snapshot_double":
|
||||
if tool_text != "42":
|
||||
raise AssertionError(f"native call returned no configured-tool value: {tool_text}")
|
||||
return tool_call_chunks("advanced-denied-native", "snapshot_double", {"value": -1})
|
||||
if call_id == "advanced-code" and tool_name == "run_code":
|
||||
if "42" not in tool_text:
|
||||
raise AssertionError(f"run_code returned no configured-tool value: {tool_text}")
|
||||
return tool_call_chunks("advanced-denied-native", "snapshot_double", {"value": -1})
|
||||
if call_id == "advanced-denied-native" and tool_name == "snapshot_double":
|
||||
if 'Auto review rejected tool "snapshot_double"; its body was not executed' not in tool_text:
|
||||
raise AssertionError(f"native denial did not preserve the model result: {tool_text}")
|
||||
if "transport raw" in tool_text:
|
||||
raise AssertionError("native denial leaked its user-facing reason to the model")
|
||||
return tool_call_chunks("advanced-denied-ptc", "run_code", {
|
||||
"code": "try { await tools.snapshot_double({ value: -1 }) } catch (error) { return error.message }",
|
||||
"description": "Catch a structured inner tool denial",
|
||||
@@ -633,6 +639,12 @@ def advanced_tool_followup(
|
||||
raise AssertionError(f"PTC denial did not preserve the model result: {tool_text}")
|
||||
if "transport raw" in tool_text:
|
||||
raise AssertionError("PTC denial leaked its user-facing reason to the model")
|
||||
return text_chunks(SNAPSHOT_FINAL_TEXT)
|
||||
if call_id == "advanced-denied-native" and tool_name == "snapshot_double":
|
||||
if 'Auto review rejected tool "snapshot_double"; its body was not executed' not in tool_text:
|
||||
raise AssertionError(f"native denial did not preserve the model result: {tool_text}")
|
||||
if "transport raw" in tool_text:
|
||||
raise AssertionError("native denial leaked its user-facing reason to the model")
|
||||
assert_advertised_tool(body, "subagent")
|
||||
return tool_call_chunks(
|
||||
"advanced-direct-child",
|
||||
@@ -643,8 +655,10 @@ def advanced_tool_followup(
|
||||
},
|
||||
)
|
||||
if call_id == "advanced-direct-child" and tool_name == "subagent":
|
||||
if "DIRECT_CHILD_OK" not in tool_text:
|
||||
raise AssertionError(f"subagent returned no expected child value: {tool_text}")
|
||||
if not tool_text.startswith("started subagent "):
|
||||
raise AssertionError(f"subagent returned no child identity: {tool_text}")
|
||||
if not MockModelHandler.direct_child_finished.wait(timeout=30):
|
||||
raise AssertionError("direct child did not emit its terminal SDK notification")
|
||||
assert_advertised_tool(body, "workflow")
|
||||
return tool_call_chunks(
|
||||
"advanced-workflow",
|
||||
@@ -1121,7 +1135,7 @@ def smoke_sdk_live() -> None:
|
||||
|
||||
|
||||
def assert_live_turn(label: str, result: RunResult) -> None:
|
||||
"""Require completed model tool use and the smoke sentinel on the final answer line."""
|
||||
"""Require completed model tool use and a standalone smoke acknowledgement."""
|
||||
if result.finish_reason != "completed":
|
||||
event_types = [event.get("type") for event in result.events]
|
||||
turn_end_data = next(
|
||||
@@ -1138,8 +1152,7 @@ def assert_live_turn(label: str, result: RunResult) -> None:
|
||||
f"{label} turn made no model-requested tool call; "
|
||||
f"final={result.final_response!r}"
|
||||
)
|
||||
answer_lines = result.final_response.strip().splitlines()
|
||||
if not answer_lines or answer_lines[-1].strip() != LIVE_API_SENTINEL:
|
||||
if not any(line.strip() == LIVE_API_SENTINEL for line in result.final_response.splitlines()):
|
||||
raise AssertionError(f"{label} turn returned {result.final_response!r}")
|
||||
|
||||
def safe_turn_end(value: object) -> object:
|
||||
@@ -1198,33 +1211,35 @@ def smoke_sdk_default(base_url: str) -> None:
|
||||
def smoke_sdk_custom(base_url: str, executable: Path) -> None:
|
||||
from deepseek_harness import DeepSeekHarness
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="dsh-sdk-custom-") as temporary:
|
||||
root = Path(temporary).resolve()
|
||||
dsh_home = root / "home"
|
||||
sessions = dsh_home / "sessions"
|
||||
patch = write_advanced_profile_patch(root, "custom.patch.yml", sessions)
|
||||
with DeepSeekHarness(
|
||||
provider="deepseek-official",
|
||||
model="smoke-model",
|
||||
cwd=str(root),
|
||||
dsh_bin=str(executable),
|
||||
dsh_home=str(dsh_home),
|
||||
patches=(str(patch),),
|
||||
env={
|
||||
"DSH_PERMISSION_MODE": "danger-full-access",
|
||||
"DSH_TELEMETRY_DISABLED": "1",
|
||||
},
|
||||
api_key="sk-keyless-smoke",
|
||||
base_url=base_url,
|
||||
request_timeout_seconds=60,
|
||||
) as harness:
|
||||
text_result = harness.run("reply with the smoke text", session_id="custom-smoke")
|
||||
code_result = harness.run(CODE_PROMPT, session_id="custom-smoke")
|
||||
workflow_result = harness.run(WORKFLOW_PROMPT, session_id="custom-smoke")
|
||||
assert text_result.final_response == EXPECTED_TEXT, text_result.final_response
|
||||
assert code_result.final_response == CODE_WORKER_TEXT, code_result.final_response
|
||||
assert workflow_result.final_response == WORKFLOW_WORKER_TEXT, workflow_result.final_response
|
||||
assert_session_log(sessions, root, EXPECTED_TEXT, CODE_WORKER_TEXT, WORKFLOW_WORKER_TEXT)
|
||||
cases: tuple[tuple[Literal["native", "ptc"], tuple[tuple[str, str], ...]], ...] = (
|
||||
("native", (("reply with the smoke text", EXPECTED_TEXT), (WORKFLOW_PROMPT, WORKFLOW_WORKER_TEXT))),
|
||||
("ptc", ((CODE_PROMPT, CODE_WORKER_TEXT),)),
|
||||
)
|
||||
for mode, turns in cases:
|
||||
with tempfile.TemporaryDirectory(prefix=f"dsh-sdk-custom-{mode}-") as temporary:
|
||||
root = Path(temporary).resolve()
|
||||
dsh_home = root / "home"
|
||||
sessions = dsh_home / "sessions"
|
||||
patch = write_advanced_profile_patch(root, "custom.patch.yml", sessions, mode)
|
||||
with DeepSeekHarness(
|
||||
provider="deepseek-official",
|
||||
model="smoke-model",
|
||||
cwd=str(root),
|
||||
dsh_bin=str(executable),
|
||||
dsh_home=str(dsh_home),
|
||||
patches=(str(patch),),
|
||||
env={
|
||||
"DSH_PERMISSION_MODE": "danger-full-access",
|
||||
"DSH_TELEMETRY_DISABLED": "1",
|
||||
},
|
||||
api_key="sk-keyless-smoke",
|
||||
base_url=base_url,
|
||||
request_timeout_seconds=60,
|
||||
) as harness:
|
||||
for prompt, expected in turns:
|
||||
result = harness.run(prompt, session_id=f"custom-{mode}")
|
||||
assert result.final_response == expected, result.final_response
|
||||
assert_session_log(sessions, root, *(expected for _, expected in turns))
|
||||
|
||||
|
||||
def smoke_sdk_minimal(
|
||||
@@ -1293,7 +1308,11 @@ def smoke_sdk_dynamic_tools(base_url: str, executable: Path, update_snapshots: b
|
||||
with tempfile.TemporaryDirectory(prefix="dsh-sdk-dynamic-tools-") as temporary:
|
||||
root = Path(temporary).resolve()
|
||||
dsh_home = root / "home"
|
||||
user_home = root / "user-home"
|
||||
user_home.mkdir()
|
||||
sessions = dsh_home / "sessions"
|
||||
# Own the baseline instruction message instead of inheriting host instructions.
|
||||
(root / "AGENTS.md").write_text("Use the tools requested by the user.\n", encoding="utf-8")
|
||||
task = root / "dynamic-tool-task.txt"
|
||||
task.write_text("Call snapshot_ping once.\n", encoding="utf-8")
|
||||
patch = write_profile_patch(root, "dynamic-tools.patch.yml", sessions, [
|
||||
@@ -1303,6 +1322,8 @@ def smoke_sdk_dynamic_tools(base_url: str, executable: Path, update_snapshots: b
|
||||
{"id": "tool-bash", "disabled": True},
|
||||
{"id": "tool-pwsh", "disabled": True},
|
||||
{"id": "session-title-llm", "disabled": True},
|
||||
# Export acknowledgements can change the request header sequence numbers.
|
||||
{"id": "session-log-deepseek", "config": {"enabled": False}},
|
||||
{"insert": [{
|
||||
"id": "dynamic-tools",
|
||||
"name": (Path(__file__).resolve().parent / "fixtures/python-sdk-dynamic-tools.mjs").as_uri(),
|
||||
@@ -1311,7 +1332,11 @@ def smoke_sdk_dynamic_tools(base_url: str, executable: Path, update_snapshots: b
|
||||
with DeepSeekHarness(
|
||||
provider="deepseek-official", model="smoke-model", cwd=str(root),
|
||||
dsh_bin=str(executable), dsh_home=str(dsh_home), patches=(str(patch),),
|
||||
env={"DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1"},
|
||||
env={
|
||||
"DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1",
|
||||
"HOME": str(user_home), "USERPROFILE": str(user_home),
|
||||
"DSH_AGENTS_HOME": str(user_home / ".agents"),
|
||||
},
|
||||
api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60,
|
||||
) as harness:
|
||||
result = harness.run(DYNAMIC_TOOLS_PROMPT, session_id="dynamic-tools-smoke")
|
||||
@@ -1514,14 +1539,22 @@ def smoke_sdk_profile_plugin(base_url: str) -> None:
|
||||
|
||||
|
||||
def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool) -> None:
|
||||
"""Drive and compare the advanced SDK/executable behavioral snapshot."""
|
||||
"""Exercise native and PTC calls in independent packaged-runtime compositions."""
|
||||
for mode in ("native", "ptc"):
|
||||
smoke_sdk_snapshot_mode(base_url, executable, update_snapshots, mode)
|
||||
|
||||
|
||||
def smoke_sdk_snapshot_mode(
|
||||
base_url: str, executable: Path, update_snapshots: bool, mode: Literal["native", "ptc"],
|
||||
) -> None:
|
||||
"""Drive one supported presentation through the SDK and persistent Session log."""
|
||||
from deepseek_harness import DeepSeekHarness
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="dsh-sdk-snapshot-") as temporary:
|
||||
root = Path(temporary).resolve()
|
||||
dsh_home = root / "home"
|
||||
sessions = dsh_home / "sessions"
|
||||
patch = write_advanced_profile_patch(root, "snapshot.patch.yml", sessions)
|
||||
patch = write_advanced_profile_patch(root, "snapshot.patch.yml", sessions, mode)
|
||||
feedback_patch = write_profile_patch(root, "feedback.patch.yml", sessions, [{"insert": [
|
||||
{"id": "snapshot-tool", "name": (
|
||||
Path(__file__).resolve().parent / "fixtures/python-snapshot-tool.mjs"
|
||||
@@ -1533,6 +1566,7 @@ def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool)
|
||||
Path(__file__).resolve().parent / "fixtures/python-snapshot-workflow-order.mjs"
|
||||
).as_uri(), "config": {
|
||||
"parentSessionId": SNAPSHOT_SESSION_ID, "prompt": SNAPSHOT_WORKFLOW_CHILD_PROMPT,
|
||||
"direct": {"prompt": SNAPSHOT_DIRECT_CHILD_PROMPT, "callId": "advanced-direct-child"},
|
||||
}},
|
||||
{"id": "snapshot-message-feedback", "name": "@deepseek-ai/dsh-message-feedback",
|
||||
"config": {"maxNoteBytes": 1024}},
|
||||
@@ -1561,16 +1595,27 @@ def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool)
|
||||
base_url=base_url,
|
||||
request_timeout_seconds=60,
|
||||
) as harness:
|
||||
result = harness.run(SNAPSHOT_PROMPT, session_id=SNAPSHOT_SESSION_ID)
|
||||
MockModelHandler.direct_child_finished.clear()
|
||||
|
||||
assert result.final_response == SNAPSHOT_FINAL_TEXT, result.final_response
|
||||
def observe_child(notification: "Notification") -> None:
|
||||
if (notification.method == "subagent.finished"
|
||||
and message_text(notification.payload.get("lastAssistantMessage")) == "DIRECT_CHILD_OK"):
|
||||
MockModelHandler.direct_child_finished.set()
|
||||
|
||||
result = harness.run(
|
||||
SNAPSHOT_PROMPT, session_id=SNAPSHOT_SESSION_ID, on_notification=observe_child,
|
||||
)
|
||||
|
||||
assert result.final_response == SNAPSHOT_FINAL_TEXT, (result.final_response, harness.client._runtime_diagnostics())
|
||||
jobs = [event for event in result.events if event.get("type") in ("job/registered", "job/settled")]
|
||||
assert [event["type"] for event in jobs] == ["job/registered", "job/settled"], jobs
|
||||
registered, settled = (event["data"] for event in jobs)
|
||||
assert registered["executionId"] == settled["executionId"]
|
||||
assert registered["startedAt"] == settled["startedAt"] <= settled["finishedAt"]
|
||||
assert settled["terminationConfirmed"] is True and settled["status"] == "completed"
|
||||
assert registered["toolCall"] == {"callId": "advanced-code:ptc:1", "rootCallId": "advanced-code"}
|
||||
root_call_id = "advanced-code" if mode == "ptc" else "advanced-native"
|
||||
call_id = root_call_id + ":ptc:1" if mode == "ptc" else root_call_id
|
||||
assert registered["toolCall"] == {"callId": call_id, "rootCallId": root_call_id}
|
||||
subscribed_jobs = [notification.payload["event"] for notification in result.notifications
|
||||
if notification.method == "session.event"
|
||||
and notification.payload.get("event", {}).get("type") in ("job/registered", "job/settled")]
|
||||
@@ -1586,11 +1631,13 @@ def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool)
|
||||
if feedback_types != ["feedback/record", "feedback/record", "feedback/message-put", "feedback/message-put", "feedback/message-delete"]:
|
||||
raise AssertionError(f"advanced snapshot did not exercise all feedback mutations: {feedback_types}")
|
||||
methods = [notification.method for notification in result.notifications]
|
||||
if methods.count("subagent.started") != 2 or methods.count("subagent.finished") != 2:
|
||||
raise AssertionError(f"advanced snapshot emitted unexpected subagent lifecycle: {methods}")
|
||||
expected_children = 2 if mode == "native" else 0
|
||||
if methods.count("subagent.started") != expected_children or methods.count("subagent.finished") != expected_children:
|
||||
raise AssertionError(f"{mode} advanced snapshot emitted unexpected subagent lifecycle: {methods}")
|
||||
ptc_events = [event for event in result.events
|
||||
if event.get("type") in ("tool/ptc-dispatch-start", "tool/ptc-dispatch")]
|
||||
if [event["type"] for event in ptc_events] != ["tool/ptc-dispatch-start", "tool/ptc-dispatch"] * 2:
|
||||
expected_ptc_calls = 2 if mode == "ptc" else 0
|
||||
if [event["type"] for event in ptc_events] != ["tool/ptc-dispatch-start", "tool/ptc-dispatch"] * expected_ptc_calls:
|
||||
raise AssertionError(f"advanced snapshot emitted unexpected PTC dispatch events: {ptc_events}")
|
||||
for index, event in enumerate(ptc_events):
|
||||
data = event["data"]
|
||||
@@ -1604,22 +1651,25 @@ def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool)
|
||||
if event.get("type") in ("tool/result", "tool/ptc-dispatch") and "error" in event["data"]]
|
||||
assert errors == [{
|
||||
"name": "AutoReviewDeniedError", "code": "AUTO_REVIEW_DENIED", "reason": " transport raw\r\nreason ",
|
||||
}] * 2, errors
|
||||
}], errors
|
||||
|
||||
logs = read_session_logs(sessions)
|
||||
child_ids = snapshot_child_ids(result)
|
||||
child_ids = snapshot_child_ids(result, expected_children)
|
||||
expected_ids = {SNAPSHOT_SESSION_ID, *child_ids}
|
||||
if set(logs) != expected_ids:
|
||||
raise AssertionError(f"advanced snapshot expected parent plus two child logs: {sorted(logs)}")
|
||||
if "DIRECT_CHILD_OK" not in render_jsonl(logs[child_ids[0]]):
|
||||
raise AssertionError("first advanced child log has no direct-subagent result")
|
||||
if "WORKFLOW_CHILD_OK" not in render_jsonl(logs[child_ids[1]]):
|
||||
raise AssertionError("second advanced child log has no workflow-subagent result")
|
||||
raise AssertionError(f"advanced snapshot expected parent plus {expected_children} child logs: {sorted(logs)}")
|
||||
if mode == "native":
|
||||
if "DIRECT_CHILD_OK" not in render_jsonl(logs[child_ids[0]]):
|
||||
raise AssertionError("first advanced child log has no direct-subagent result")
|
||||
if "WORKFLOW_CHILD_OK" not in render_jsonl(logs[child_ids[1]]):
|
||||
raise AssertionError("second advanced child log has no workflow-subagent result")
|
||||
|
||||
assert [event for event in logs[SNAPSHOT_SESSION_ID] if event.get("type") in ("job/registered", "job/settled")] == jobs
|
||||
|
||||
files = build_snapshot_files(result, logs, child_ids, root)
|
||||
compare_snapshot_files(
|
||||
files, update_snapshots, ADVANCED_SNAPSHOT_DIRECTORY, ADVANCED_SNAPSHOT_FILENAMES,
|
||||
files, update_snapshots, ADVANCED_SNAPSHOT_DIRECTORY if mode == "native" else ADVANCED_SNAPSHOT_ROOT / "advanced-ptc",
|
||||
ADVANCED_SNAPSHOT_FILENAMES if mode == "native" else ("result.json", "session.v4.jsonl"),
|
||||
native_writer_output=True,
|
||||
)
|
||||
|
||||
@@ -1753,7 +1803,10 @@ def smoke_sdk_scheduler_recovery(base_url: str, executable: Path, update_snapsho
|
||||
raise AssertionError(f"scheduler recovery duplicated or lost tool outcomes: {tool_results}")
|
||||
if sum(record.get("type") == "todo/write" for record in records) != 1:
|
||||
raise AssertionError("scheduler recovery executed an unstarted todo update")
|
||||
replacements = [(str(root), "{{cwd}}"), (RECOVERY_SESSION_ID, "{{parent}}")]
|
||||
replacements = [
|
||||
*snapshot_directory_replacements(root, "{{cwd}}"),
|
||||
(RECOVERY_SESSION_ID, "{{parent}}"),
|
||||
]
|
||||
result_value = [{
|
||||
"session_id": result.session_id,
|
||||
"final_response": result.final_response,
|
||||
@@ -2152,8 +2205,8 @@ def read_session_logs(sessions: Path) -> dict[str, list[dict[str, object]]]:
|
||||
return logs
|
||||
|
||||
|
||||
def snapshot_child_ids(result: "RunResult") -> list[str]:
|
||||
"""Return the two child session ids in their SDK notification order."""
|
||||
def snapshot_child_ids(result: "RunResult", expected_count: int) -> list[str]:
|
||||
"""Return the expected child session ids in their SDK notification order."""
|
||||
child_ids: list[str] = []
|
||||
for notification in result.notifications:
|
||||
if notification.method != "subagent.started":
|
||||
@@ -2164,8 +2217,8 @@ def snapshot_child_ids(result: "RunResult") -> list[str]:
|
||||
child_id = payload.get("childSessionId")
|
||||
if isinstance(child_id, str) and child_id not in child_ids:
|
||||
child_ids.append(child_id)
|
||||
if len(child_ids) != 2:
|
||||
raise AssertionError(f"advanced snapshot expected two child session ids: {child_ids}")
|
||||
if len(child_ids) != expected_count:
|
||||
raise AssertionError(f"advanced snapshot expected {expected_count} child session ids: {child_ids}")
|
||||
return child_ids
|
||||
|
||||
|
||||
@@ -2290,8 +2343,8 @@ def build_minimal_snapshot_files(
|
||||
kept verbatim: they carry what the deployment actually shows the model, so a plugin
|
||||
that contributes an unintended system section or user message cannot pass unnoticed.
|
||||
Assistant and tool payloads keep only their call identity because their text differs
|
||||
across the platforms this expected output must replay on. The shipped profile omits
|
||||
dynamic runtime context, so every message it emits is compared.
|
||||
across the platforms this expected output must replay on. Required working-directory
|
||||
context remains visible even when the profile disables optional runtime context.
|
||||
"""
|
||||
snapshot = []
|
||||
for body in requests:
|
||||
@@ -2341,10 +2394,18 @@ def minimal_snapshot_message(message: object, cwd: Path) -> dict[str, object]:
|
||||
raise AssertionError(f"minimal model request has an unexpected message role: {message}")
|
||||
|
||||
|
||||
def snapshot_directory_replacements(directory: Path, token: str) -> list[tuple[str, str]]:
|
||||
"""Pin the allocated fixture path in plain fields and JSON-quoted context text."""
|
||||
path = str(directory)
|
||||
return [(json.dumps(path, ensure_ascii=False)[1:-1], token), (path, token)]
|
||||
|
||||
|
||||
def minimal_snapshot_text(value: object, cwd: Path) -> object:
|
||||
"""Replace the scenario's temporary working directory everywhere it appears."""
|
||||
if isinstance(value, str):
|
||||
return value.replace(str(cwd), "{{cwd}}")
|
||||
for actual, token in snapshot_directory_replacements(cwd, "{{cwd}}"):
|
||||
value = value.replace(actual, token)
|
||||
return value
|
||||
if isinstance(value, list):
|
||||
return [minimal_snapshot_text(item, cwd) for item in value]
|
||||
if isinstance(value, dict):
|
||||
@@ -2358,9 +2419,10 @@ def build_snapshot_files(
|
||||
child_ids: list[str],
|
||||
cwd: Path,
|
||||
) -> dict[str, str]:
|
||||
"""Render the SDK result and three persisted logs into stable expected outputs."""
|
||||
replacements = [(str(cwd), "{{cwd}}"), (SNAPSHOT_SESSION_ID, "{{parent}}")]
|
||||
replacements.append((snapshot_workflow_run_id(result), "{{workflow-run}}"))
|
||||
"""Render the SDK result and its persisted parent/child logs into stable expected outputs."""
|
||||
replacements = snapshot_directory_replacements(cwd, "{{cwd}}") + [(SNAPSHOT_SESSION_ID, "{{parent}}")]
|
||||
if child_ids:
|
||||
replacements.append((snapshot_workflow_run_id(result), "{{workflow-run}}"))
|
||||
for index, child_id in enumerate(child_ids, start=1):
|
||||
replacements.append((child_id, f"{{{{child-{index}}}}}"))
|
||||
agent_id = snapshot_agent_id(result, child_id)
|
||||
@@ -2431,8 +2493,8 @@ def build_restart_snapshot_files(
|
||||
) -> dict[str, str]:
|
||||
"""Render two SDK processes, isolated model histories, and durable logs."""
|
||||
replacements = [
|
||||
(str(sessions), "{{sessions}}"),
|
||||
(str(cwd), "{{cwd}}"),
|
||||
*snapshot_directory_replacements(sessions, "{{sessions}}"),
|
||||
*snapshot_directory_replacements(cwd, "{{cwd}}"),
|
||||
(RESTART_FIRST_SESSION_ID, "{{session-1}}"),
|
||||
(RESTART_SECOND_SESSION_ID, "{{session-2}}"),
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,50 @@
|
||||
{"type":"session","version":4,"id":"{{parent}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0}
|
||||
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
|
||||
{"type":"sandbox/mode","data":{"mode":"danger-full-access"}}
|
||||
{"type":"approval/policy","data":{"policy":"never"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin:serial-created-fixture"}}]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}}
|
||||
{"type":"turn/start","data":{"turn":1}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{messageId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin:serial-created-fixture"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}}],"source":{"kind":"python-snapshot-image-offload"}},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["run_code"]},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
|
||||
{"type":"session/title","data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[11],"source":{"kind":"fallback"}}}
|
||||
{"type":"assistant/attempt","data":{"turn":1,"step":1,"stream":[{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"code":"IMAGE_OFFLOAD_REQUIRED","message":"authored image budget failure","offloadImages":1}}}}]}}
|
||||
{"type":"image/offload","data":{"targets":[{"seq":13,"imageIndexes":[0]}]}}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["run_code"]},"reason":"series"}}
|
||||
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":4,"throughSeq":19}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Call the configured Plugin tool\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-code","name":"run_code","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-code","args":["{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Call the configured Plugin tool\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Call the configured Plugin tool\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Call the configured Plugin tool\"}"}}
|
||||
{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:ptc:1","name":"snapshot_double","arguments":{"value":21}}}
|
||||
{"type":"job/registered","data":{"executionId":"{{id:1}}","jobId":"snapshot-1","owner":"{{parent}}","kind":"snapshot","label":"snapshot_double","startedAt":0,"toolCall":{"callId":"advanced-code:ptc:1","rootCallId":"advanced-code"}}}
|
||||
{"type":"job/settled","data":{"executionId":"{{id:1}}","jobId":"snapshot-1","owner":"{{parent}}","kind":"snapshot","label":"snapshot_double","startedAt":0,"toolCall":{"callId":"advanced-code:ptc:1","rootCallId":"advanced-code"},"finishedAt":0,"status":"completed","cause":"producer","terminationConfirmed":true}}
|
||||
{"type":"tool/ptc-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:ptc:1","name":"snapshot_double","arguments":{"value":21},"isError":false,"content":[{"type":"text","text":"42"}]}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"advanced-code"},"toolCallId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false,"id":"{{messageId}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","data":{"turn":1,"step":2}}
|
||||
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":4,"throughSeq":29}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-denied-ptc","name":"run_code","arguments":"{\"code\": \"try { await tools.snapshot_double({ value: -1 }) } catch (error) { return error.message }\", \"description\": \"Catch a structured inner tool denial\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-denied-ptc","name":"run_code","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-denied-ptc","args":["{\"code\": \"try { await tools.snapshot_double({ value: -1 }) } catch (error) { return error.message }\", \"description\": \"Catch a structured inner tool denial\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-denied-ptc","name":"run_code","arguments":"{\"code\": \"try { await tools.snapshot_double({ value: -1 }) } catch (error) { return error.message }\", \"description\": \"Catch a structured inner tool denial\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-denied-ptc","name":"run_code","arguments":"{\"code\": \"try { await tools.snapshot_double({ value: -1 }) } catch (error) { return error.message }\", \"description\": \"Catch a structured inner tool denial\"}"}}
|
||||
{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"advanced-denied-ptc","parentCallId":"advanced-denied-ptc","subCallId":"advanced-denied-ptc:ptc:1","name":"snapshot_double","arguments":{"value":-1}}}
|
||||
{"type":"tool/ptc-dispatch","data":{"rootCallId":"advanced-denied-ptc","parentCallId":"advanced-denied-ptc","subCallId":"advanced-denied-ptc:ptc:1","name":"snapshot_double","arguments":{"value":-1},"isError":true,"error":{"name":"AutoReviewDeniedError","code":"AUTO_REVIEW_DENIED","reason":" transport raw\r\nreason "},"content":[{"type":"text","text":"Error: Auto review rejected tool \"snapshot_double\"; its body was not executed"}]}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"advanced-denied-ptc"},"toolCallId":"advanced-denied-ptc","content":[{"type":"text","text":"Auto review rejected tool \"snapshot_double\"; its body was not executed"}],"isError":false,"id":"{{messageId}}"}},"sourceEventSeqs":[32],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":2}}
|
||||
{"type":"step/start","data":{"turn":1,"step":3}}
|
||||
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":4,"throughSeq":37}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"text"}]}},"id":"{{message:1}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["ADVANCED_EXECUTABLE_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"text"}]}}}]},"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":3}}
|
||||
{"type":"command/run","data":{"commandId":"{{command:1}}","name":"feedback","source":{"kind":"user"}}}
|
||||
{"type":"feedback/record","data":{"text":"The session needs a clearer explanation."}}
|
||||
{"type":"command/done","data":{"commandId":"{{command:1}}","kind":"success","text":"Feedback recorded for session {{parent}}\nAnonymous user: {{anonymous-user}}."}}
|
||||
{"type":"feedback/record","data":{"category":"other"}}
|
||||
{"type":"feedback/message-put","data":{"sessionId":"{{parent}}","item":{"messageId":"{{message:1}}","rating":"negative","note":"Explain the result.","category":"task-result","version":"{{feedback-version:1}}","createdAt":0,"updatedAt":0}}}
|
||||
{"type":"feedback/message-put","data":{"sessionId":"{{parent}}","item":{"messageId":"{{message:1}}","rating":"positive","note":"The explanation is clear now.","version":"{{feedback-version:2}}","createdAt":0,"updatedAt":0}}}
|
||||
{"type":"feedback/message-delete","data":{"sessionId":"{{parent}}","messageId":"{{message:1}}"}}
|
||||
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,5 @@
|
||||
{"type":"session","version":4,"id":"{{child-1}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{parent}}","isSeeded":false,"origin":"subagent","delegationDepth":1}
|
||||
{"type":"subagent/descriptor","data":{"version":3,"mode":"continuable","provider":"spawn","label":"Check direct child","agentProvider":"deepseek-official","agentModel":"smoke-model","agentReasoningEffort":"high"}}
|
||||
{"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}}
|
||||
{"type":"approval/policy","data":{"policy":"never","source":"delegation"}}
|
||||
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
|
||||
@@ -7,13 +8,12 @@
|
||||
{"type":"turn/start","data":{"turn":1}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"Check direct child"}}
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{messageId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin:serial-created-fixture"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","snapshot_double","subagent","workflow","working_directory"]},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
|
||||
{"type":"session/title","data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[12],"source":{"kind":"fallback"}}}
|
||||
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-1}}","sessionFormatVersion":4,"throughSeq":16}}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{"type":"session","version":4,"id":"{{child-2}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{parent}}","isSeeded":false,"origin":"subagent","delegationDepth":1}
|
||||
{"type":"subagent/descriptor","data":{"version":3,"mode":"continuable","provider":"spawn","label":"advanced-exe-snapshot child 1","agentProvider":"deepseek-official","agentModel":"smoke-model","agentReasoningEffort":"high"}}
|
||||
{"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}}
|
||||
{"type":"approval/policy","data":{"policy":"never","source":"delegation"}}
|
||||
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
|
||||
@@ -7,13 +8,12 @@
|
||||
{"type":"turn/start","data":{"turn":1}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn"}}
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{messageId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin:serial-created-fixture"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","snapshot_double","subagent","workflow","working_directory"]},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
|
||||
{"type":"session/title","data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[12],"source":{"kind":"fallback"}}}
|
||||
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-2}}","sessionFormatVersion":4,"throughSeq":16}}
|
||||
|
||||
@@ -11,59 +11,52 @@
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{messageId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin:serial-created-fixture"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}}],"source":{"kind":"python-snapshot-image-offload"}},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","snapshot_double","subagent","workflow","working_directory"]},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
|
||||
{"type":"session/title","data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[11],"source":{"kind":"fallback"}}}
|
||||
{"type":"assistant/attempt","data":{"turn":1,"step":1,"stream":[{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"code":"IMAGE_OFFLOAD_REQUIRED","message":"authored image budget failure","offloadImages":1}}}}]}}
|
||||
{"type":"image/offload","data":{"targets":[{"seq":13,"imageIndexes":[0]}]}}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"series"}}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","snapshot_double","subagent","workflow","working_directory"]},"reason":"series"}}
|
||||
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":4,"throughSeq":19}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Call the configured Plugin tool\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-code","name":"run_code","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-code","args":["{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Call the configured Plugin tool\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Call the configured Plugin tool\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Call the configured Plugin tool\"}"}}
|
||||
{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:ptc:1","name":"snapshot_double","arguments":{"value":21}}}
|
||||
{"type":"job/registered","data":{"executionId":"{{id:1}}","jobId":"snapshot-1","owner":"{{parent}}","kind":"snapshot","label":"snapshot_double","startedAt":0,"toolCall":{"callId":"advanced-code:ptc:1","rootCallId":"advanced-code"}}}
|
||||
{"type":"job/settled","data":{"executionId":"{{id:1}}","jobId":"snapshot-1","owner":"{{parent}}","kind":"snapshot","label":"snapshot_double","startedAt":0,"toolCall":{"callId":"advanced-code:ptc:1","rootCallId":"advanced-code"},"finishedAt":0,"status":"completed","cause":"producer","terminationConfirmed":true}}
|
||||
{"type":"tool/ptc-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:ptc:1","name":"snapshot_double","arguments":{"value":21},"isError":false,"content":[{"type":"text","text":"42"}]}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"advanced-code"},"toolCallId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false,"id":"{{messageId}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-native","name":"snapshot_double","arguments":"{\"value\": 21}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-native","name":"snapshot_double","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-native","args":["{\"value\": 21}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-native","name":"snapshot_double","arguments":"{\"value\": 21}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"advanced-native","name":"snapshot_double","arguments":"{\"value\": 21}"}}
|
||||
{"type":"job/registered","data":{"executionId":"{{id:1}}","jobId":"snapshot-1","owner":"{{parent}}","kind":"snapshot","label":"snapshot_double","startedAt":0,"toolCall":{"callId":"advanced-native","rootCallId":"advanced-native"}}}
|
||||
{"type":"job/settled","data":{"executionId":"{{id:1}}","jobId":"snapshot-1","owner":"{{parent}}","kind":"snapshot","label":"snapshot_double","startedAt":0,"toolCall":{"callId":"advanced-native","rootCallId":"advanced-native"},"finishedAt":0,"status":"completed","cause":"producer","terminationConfirmed":true}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"advanced-native"},"toolCallId":"advanced-native","content":[{"type":"text","text":"42"}],"isError":false,"id":"{{messageId}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","data":{"turn":1,"step":2}}
|
||||
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":4,"throughSeq":29}}
|
||||
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":4,"throughSeq":27}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-denied-native","name":"snapshot_double","arguments":"{\"value\": -1}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-denied-native","name":"snapshot_double","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-denied-native","args":["{\"value\": -1}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-denied-native","name":"snapshot_double","arguments":"{\"value\": -1}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-denied-native","name":"snapshot_double","arguments":"{\"value\": -1}"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"advanced-denied-native"},"toolCallId":"advanced-denied-native","content":[{"type":"text","text":"Error: Auto review rejected tool \"snapshot_double\"; its body was not executed"}],"isError":true,"id":"{{messageId}}"},"error":{"name":"AutoReviewDeniedError","code":"AUTO_REVIEW_DENIED","reason":" transport raw\r\nreason "}},"sourceEventSeqs":[32],"surfaceOp":"append"}
|
||||
{"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"advanced-denied-native"},"toolCallId":"advanced-denied-native","content":[{"type":"text","text":"Error: Auto review rejected tool \"snapshot_double\"; its body was not executed"}],"isError":true,"id":"{{messageId}}"},"error":{"name":"AutoReviewDeniedError","code":"AUTO_REVIEW_DENIED","reason":" transport raw\r\nreason "}},"sourceEventSeqs":[30],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":2}}
|
||||
{"type":"step/start","data":{"turn":1,"step":3}}
|
||||
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":4,"throughSeq":35}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-denied-ptc","name":"run_code","arguments":"{\"code\": \"try { await tools.snapshot_double({ value: -1 }) } catch (error) { return error.message }\", \"description\": \"Catch a structured inner tool denial\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-denied-ptc","name":"run_code","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-denied-ptc","args":["{\"code\": \"try { await tools.snapshot_double({ value: -1 }) } catch (error) { return error.message }\", \"description\": \"Catch a structured inner tool denial\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-denied-ptc","name":"run_code","arguments":"{\"code\": \"try { await tools.snapshot_double({ value: -1 }) } catch (error) { return error.message }\", \"description\": \"Catch a structured inner tool denial\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-denied-ptc","name":"run_code","arguments":"{\"code\": \"try { await tools.snapshot_double({ value: -1 }) } catch (error) { return error.message }\", \"description\": \"Catch a structured inner tool denial\"}"}}
|
||||
{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"advanced-denied-ptc","parentCallId":"advanced-denied-ptc","subCallId":"advanced-denied-ptc:ptc:1","name":"snapshot_double","arguments":{"value":-1}}}
|
||||
{"type":"tool/ptc-dispatch","data":{"rootCallId":"advanced-denied-ptc","parentCallId":"advanced-denied-ptc","subCallId":"advanced-denied-ptc:ptc:1","name":"snapshot_double","arguments":{"value":-1},"isError":true,"error":{"name":"AutoReviewDeniedError","code":"AUTO_REVIEW_DENIED","reason":" transport raw\r\nreason "},"content":[{"type":"text","text":"Error: Auto review rejected tool \"snapshot_double\"; its body was not executed"}]}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":3,"message":{"role":"tool","source":{"kind":"tool","callId":"advanced-denied-ptc"},"toolCallId":"advanced-denied-ptc","content":[{"type":"text","text":"Auto review rejected tool \"snapshot_double\"; its body was not executed"}],"isError":false,"id":"{{messageId}}"}},"sourceEventSeqs":[38],"surfaceOp":"append"}
|
||||
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":4,"throughSeq":33}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-direct-child","args":["{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}
|
||||
{"type":"subagent/catalog","data":{"version":0,"childId":"{{child-1}}","childCreatedAt":0,"mode":"continuable","label":"Check direct child"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":3,"message":{"role":"tool","source":{"kind":"tool","callId":"advanced-direct-child"},"toolCallId":"advanced-direct-child","content":[{"type":"text","text":"started subagent {{child-1}}"}],"isError":false,"id":"{{messageId}}"}},"sourceEventSeqs":[36],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":3}}
|
||||
{"type":"step/start","data":{"turn":1,"step":4}}
|
||||
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":4,"throughSeq":43}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-direct-child","args":["{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":4,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}
|
||||
{"type":"subagent/catalog","data":{"version":0,"childId":"{{child-1}}","childCreatedAt":0,"mode":"one-shot","label":"Check direct child"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":4,"message":{"role":"tool","source":{"kind":"tool","callId":"advanced-direct-child"},"toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false,"id":"{{messageId}}"}},"sourceEventSeqs":[46],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":4}}
|
||||
{"type":"step/start","data":{"turn":1,"step":5}}
|
||||
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":4,"throughSeq":50}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-workflow","name":"workflow","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-workflow","args":["{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":5,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}
|
||||
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":4,"throughSeq":40}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{child-1}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{child-1}} finished and will do no further work unless you send it more.","senderSessionId":"{{child-1}}"},"role":"user","id":"{{messageId}}"}]}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-workflow","name":"workflow","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-workflow","args":["{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":4,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}
|
||||
{"type":"tool-workflow/run-start","data":{"runId":"{{workflow-run}}","name":"advanced-exe-snapshot"}}
|
||||
{"type":"subagent/catalog","data":{"version":0,"childId":"{{child-2}}","childCreatedAt":0,"mode":"one-shot"}}
|
||||
{"type":"subagent/catalog","data":{"version":0,"childId":"{{child-2}}","childCreatedAt":0,"mode":"continuable","label":"advanced-exe-snapshot child 1"}}
|
||||
{"type":"tool-workflow/agent-start","data":{"runId":"{{workflow-run}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{child-2}}"}}
|
||||
{"type":"tool-workflow/agent-end","data":{"runId":"{{workflow-run}}","seq":1,"outcome":"completed"}}
|
||||
{"type":"tool-workflow/run-end","data":{"runId":"{{workflow-run}}","stopReason":"completed"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":5,"message":{"role":"tool","source":{"kind":"tool","callId":"advanced-workflow"},"toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false,"id":"{{messageId}}"}},"sourceEventSeqs":[53],"surfaceOp":"append"}
|
||||
{"type":"tool/result","data":{"turn":1,"step":4,"message":{"role":"tool","source":{"kind":"tool","callId":"advanced-workflow"},"toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false,"id":"{{messageId}}"}},"sourceEventSeqs":[44],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":4}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":1,"step":5}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{child-1}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{child-1}} finished and will do no further work unless you send it more.","senderSessionId":"{{child-1}}"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":4,"throughSeq":54}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"text"}]}},"id":"{{message:1}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["ADVANCED_EXECUTABLE_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"text"}]}}}]},"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":5}}
|
||||
{"type":"step/start","data":{"turn":1,"step":6}}
|
||||
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":4,"throughSeq":61}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"text"}]}},"id":"{{message:1}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["ADVANCED_EXECUTABLE_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"text"}]}}}]},"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":6}}
|
||||
{"type":"command/run","data":{"commandId":"{{command:1}}","name":"feedback","source":{"kind":"user"}}}
|
||||
{"type":"feedback/record","data":{"text":"The session needs a clearer explanation."}}
|
||||
{"type":"command/done","data":{"commandId":"{{command:1}}","kind":"success","text":"Feedback recorded for session {{parent}}\nAnonymous user: {{anonymous-user}}."}}
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"web_fetch",
|
||||
"web_search",
|
||||
"workflow",
|
||||
"working_directory",
|
||||
"write"
|
||||
]
|
||||
},
|
||||
@@ -125,6 +126,7 @@
|
||||
"web_fetch",
|
||||
"web_search",
|
||||
"workflow",
|
||||
"working_directory",
|
||||
"write"
|
||||
]
|
||||
},
|
||||
@@ -195,6 +197,7 @@
|
||||
"web_fetch",
|
||||
"web_search",
|
||||
"workflow",
|
||||
"working_directory",
|
||||
"write"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -17,6 +17,19 @@
|
||||
"command"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "working_directory",
|
||||
"description": "Read the current working directory, or change it with cd. Relative paths use the current directory. Existing shells and running processes keep their own directories.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cd": {
|
||||
"type": "string",
|
||||
"description": "Existing directory to enter. Omit to read the current directory."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"messages": [
|
||||
@@ -26,6 +39,10 @@
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Exercise the packaged minimal agent's persistent shell."
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\"."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -49,6 +66,19 @@
|
||||
"command"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "working_directory",
|
||||
"description": "Read the current working directory, or change it with cd. Relative paths use the current directory. Existing shells and running processes keep their own directories.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cd": {
|
||||
"type": "string",
|
||||
"description": "Existing directory to enter. Omit to read the current directory."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"messages": [
|
||||
@@ -58,6 +88,10 @@
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Exercise the packaged minimal agent's persistent shell."
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\"."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -100,6 +134,19 @@
|
||||
"command"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "working_directory",
|
||||
"description": "Read the current working directory, or change it with cd. Relative paths use the current directory. Existing shells and running processes keep their own directories.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cd": {
|
||||
"type": "string",
|
||||
"description": "Existing directory to enter. Omit to read the current directory."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"messages": [
|
||||
@@ -109,6 +156,10 @@
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Exercise the packaged minimal agent's persistent shell."
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\"."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -17,6 +17,19 @@
|
||||
"command"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "working_directory",
|
||||
"description": "Read the current working directory, or change it with cd. Relative paths use the current directory. Existing shells and running processes keep their own directories.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cd": {
|
||||
"type": "string",
|
||||
"description": "Existing directory to enter. Omit to read the current directory."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"messages": [
|
||||
@@ -26,6 +39,10 @@
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Exercise the packaged minimal agent's persistent shell."
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\"."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -49,6 +66,19 @@
|
||||
"command"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "working_directory",
|
||||
"description": "Read the current working directory, or change it with cd. Relative paths use the current directory. Existing shells and running processes keep their own directories.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cd": {
|
||||
"type": "string",
|
||||
"description": "Existing directory to enter. Omit to read the current directory."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"messages": [
|
||||
@@ -58,6 +88,10 @@
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Exercise the packaged minimal agent's persistent shell."
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\"."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -100,6 +134,19 @@
|
||||
"command"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "working_directory",
|
||||
"description": "Read the current working directory, or change it with cd. Relative paths use the current directory. Existing shells and running processes keep their own directories.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cd": {
|
||||
"type": "string",
|
||||
"description": "Existing directory to enter. Omit to read the current directory."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"messages": [
|
||||
@@ -109,6 +156,10 @@
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Exercise the packaged minimal agent's persistent shell."
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\"."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."
|
||||
"text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -23,9 +23,9 @@
|
||||
"job_kill",
|
||||
"job_list",
|
||||
"job_output",
|
||||
"run_code",
|
||||
"subagent",
|
||||
"workflow"
|
||||
"workflow",
|
||||
"working_directory"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -41,7 +41,7 @@
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."
|
||||
"text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -52,9 +52,9 @@
|
||||
"job_kill",
|
||||
"job_list",
|
||||
"job_output",
|
||||
"run_code",
|
||||
"subagent",
|
||||
"workflow"
|
||||
"workflow",
|
||||
"working_directory"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -8,8 +8,8 @@
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{messageId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Complete the first isolated Python SDK process turn."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"initial"}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","subagent","workflow","working_directory"]},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
|
||||
{"type":"session/title","data":{"title":"Complete the first isolated Python","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session-1}}","sessionFormatVersion":4,"throughSeq":12}}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{messageId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Complete the second isolated Python SDK process turn."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"initial"}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","subagent","workflow","working_directory"]},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
|
||||
{"type":"session/title","data":{"title":"Complete the second isolated Python","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session-2}}","sessionFormatVersion":4,"throughSeq":12}}
|
||||
|
||||
Loaded 100 of 5646 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user