Merge branch 'master' into feat/apply-patch

Resolves the headless snapshot runner (keeps both the readonly-patch-directory
and cua-script-guidance workspace setups), regenerates the client slot and tool
catalogs with their pairing record, adds the apply_patch tool title to the five
locales master introduced, and refreshes the apply-patch system-prompt pin for
master's subagent guidance change.
This commit is contained in:
sjw1231 committed 2026-10-26 18:01:46 +08:00
commit d37b2ba52b
3740 files changed
+95972 -10460

No files matched your search

+24 -3
View File
@@ -159,6 +159,19 @@ class Target {
/**
* Validated CLI configuration; construction owns help and parse-error exits.
*/
/** pkg's `--compress` values, whose build cost differs by two orders of magnitude. */
const COMPRESS_TYPES = ['None', 'GZip', 'Brotli', 'Zstd'] as const
type CompressType = typeof COMPRESS_TYPES[number]
/**
* Narrow a raw `--compress` value to a supported pkg compressor.
* @param value - the command-line value.
* @returns whether pkg accepts the value.
*/
function isCompressType(value: string): value is CompressType {
return (COMPRESS_TYPES as readonly string[]).includes(value)
}
class BuildCli {
private constructor(
/** Build targets; defaults to the host platform only. */
@@ -167,6 +180,8 @@ class BuildCli {
readonly skipBuild: boolean,
/** Build through `pnpm run build:artifacts`, which omits the aggregate test and script typechecks. */
readonly buildArtifacts: boolean,
/** pkg archive compression; Brotli is the smallest and by far the slowest. */
readonly compress: CompressType,
/** Print every command and config patch instead of executing. */
readonly dryRun: boolean,
) {}
@@ -205,7 +220,11 @@ class BuildCli {
}
seen.add(key)
}
return new BuildCli(targets, values['skip-build'], values['build-artifacts'], values['dry-run'])
const compress = values.compress ?? 'Brotli'
if (!isCompressType(compress)) {
throw new Error(`build-exe-for-python-sdk: --compress must be one of ${COMPRESS_TYPES.join(' | ')}, got ${JSON.stringify(compress)}.`)
}
return new BuildCli(targets, values['skip-build'], values['build-artifacts'], compress, values['dry-run'])
}
private static parseRaw(argv: string[]) {
@@ -215,6 +234,7 @@ class BuildCli {
'targets': { type: 'string' },
'skip-build': { type: 'boolean', default: false },
'build-artifacts': { type: 'boolean', default: false },
'compress': { type: 'string' },
'dry-run': { type: 'boolean', default: false },
'help': { type: 'boolean', default: false },
},
@@ -229,10 +249,11 @@ class BuildCli {
' Default: the host platform only (on node24).',
' --skip-build skip `pnpm run build` (lib/ artifacts must already exist).',
' --build-artifacts run `pnpm run build:artifacts` instead, omitting the aggregate test and script typechecks.',
' --compress=<type> pkg archive compression: None | GZip | Brotli | Zstd. Default: Brotli (smallest, slowest).',
' --dry-run print every command and config patch without executing.',
' --help print this help.',
'',
'Build route: @yao-pkg/pkg --sea --compress Brotli (root devDependency, pnpm-patched); see .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.',
'Build route: @yao-pkg/pkg --sea with the selected --compress type (root devDependency, pnpm-patched); see .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.',
`Stages the node carrier in ${PYTHON_RUNTIME_DIR}/${PYTHON_NODE_SUBDIR} and writes executables to ${OUT_DIR}/.`,
].join('\n')
}
@@ -400,7 +421,7 @@ class SingleExeBuild {
this.staging,
'--sea',
'--compress',
'Brotli',
this.cli.compress,
'--targets',
target.spec,
'--output',
+11 -1
View File
@@ -60,7 +60,17 @@ describe('repository build CLI', () => {
})
describe('repository build steps', () => {
it('keeps artifact steps in serial dependency order', () => {
it.each(['complete', 'check', 'typecheck'] as const)('gives Host compilers an 8 GiB heap in %s scope', (scope) => {
const gates = buildGates(root, scope)
for (const gate of gates.filter(gate => gate.id === 'host-src' || gate.id === 'host-check')) {
expect(gate.args[0]).toBe('--max-old-space-size=8192')
}
for (const gate of gates.filter(gate => gate.id === 'client-src' || gate.id === 'client-check')) {
expect(gate.args.some(arg => arg.startsWith('--max-old-space-size'))).toBe(false)
}
})
it('finishes Host and Desktop readers before Client compilation rewrites shared outputs', () => {
expect(graph(buildGates(root, 'artifacts'))).toEqual(artifactGraph)
})
+2 -2
View File
@@ -556,11 +556,11 @@ it('requires the local speech worker and locked runtime in the published payload
}
})
it('requires both Node execution bootstraps in the published payload', () => {
it('requires Node profile configuration and both execution bootstraps in the published payload', () => {
const dir = 'packages/ptc-runtime/ptc-runtime-node'
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/process.js', 'lib/persistent-process.js']) {
for (const omitted of ['lib/configuration.js', 'lib/process.js', 'lib/persistent-process.js']) {
const files = expectedDshPackageFiles(manifest).filter(file => file !== omitted)
expect(checkWorkspaceManifest({ dir, manifest: { ...manifest, files } }))
.toEqual([expect.stringContaining('package.json files must be')])
+7 -3
View File
@@ -21,7 +21,7 @@ import { hasTypertRemoteNavigation, isForbiddenPublicationFile } from './publica
import type { DshBundleManifest } from '../packages/util/package-manifest/src/types.ts'
import { detailPath } from '../packages/util/plugin-detail/src/document.ts'
import { bundlePatchFiles } from '../packages/boot/app-boot/src/profile.ts'
import { OPTIONAL_BUNDLES } from '../packages/boot/official-bundles/src/bundles.ts'
import { OPTIONAL_BUNDLES } from '../packages/util/official-bundles/src/bundles.ts'
import {
PACKAGE_ASSET_EXPORT,
PACKAGE_ASSET_EXPORT_TARGET,
@@ -211,12 +211,16 @@ const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
// The CPython side ships as source .py files, published as-is rather than built.
'@deepseek-ai/dsh-experimental-ptc-runtime-python': ['py/**/*.py'],
'@deepseek-ai/dsh-experimental-speech-to-text-sensevoice': ['runtime/assets.json'],
// Node execution modes each launch a separately bundled bootstrap.
'@deepseek-ai/dsh-ptc-runtime-node': ['lib/process.js', 'lib/persistent-process.js'],
// Node profile configuration and process bootstraps have separate bundles.
'@deepseek-ai/dsh-ptc-runtime-node': ['lib/configuration.js', 'lib/process.js', 'lib/persistent-process.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'],
// Personalized pages ship the location guidance used by their Creator.
'@deepseek-ai/dsh-creation-recommendations': ['references/*.md', 'skills/*.md'],
// Official-plugin recommendations ship the skill text their provider loads.
'@deepseek-ai/dsh-plugin-recommendations': ['skills/*.md'],
// The Web Host mounts the default-off settings owner independently of the
// delegation-tool instances.
'@deepseek-ai/dsh-tool-subagent': ['lib/model-selection-settings.js'],
+95 -12
View File
@@ -14,7 +14,7 @@ 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 across PR workflows while Python runtime targets follow the SDK suite', () => {
it('keeps at most eleven worker jobs active across PR workflows while Python runtime targets follow the SDK suite', () => {
const ci = loadWorkflow('.github/workflows/ci.yml')
const ciWindows = loadWorkflow('.github/workflows/ci-windows.yml')
const ciPython = loadWorkflow('.github/workflows/ci-python.yml')
@@ -86,10 +86,12 @@ describe('CI workflow', () => {
// Python's runtime targets start only after the SDK suite; Windows starts
// immediately. Plan and wheel stages precede the two-target matrix.
const windowsActive = windowsJobs.length
// The lint lane owns its own runner, so it raises the ceiling by one; the
// self-hosted failover pool is still admitted only up to its own limit.
const beforeSdk = ordinaryJobs.length + compatibilityLimit + windowsActive + 1
const afterSdk = ordinaryJobs.length + compatibilityLimit + windowsActive + Math.max(1, targets.split(',').length)
expect(beforeSdk).toBeLessThanOrEqual(10)
expect(afterSdk).toBeLessThanOrEqual(10)
expect(beforeSdk).toBeLessThanOrEqual(11)
expect(afterSdk).toBeLessThanOrEqual(11)
})
it('keeps the union of required PR verdicts equal to the pre-split blocking set', () => {
@@ -117,6 +119,7 @@ describe('CI workflow', () => {
'node-24-bench',
'node-24-consumers',
'node-24-coverage',
'node-24-lint',
'node-compat',
'python-runtime',
'python-sdk',
@@ -143,7 +146,7 @@ describe('CI workflow', () => {
const scripts = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as { scripts: Record<string, string> }
const hostSteps = scripts.scripts['build:lib:host']!.split(' && ')
expect(hostSteps[0]).toBe('tsx scripts/compile-referenced-projects.ts host-libraries')
expect(hostSteps).toContain('node --max-old-space-size=4096 ./node_modules/typescript/bin/tsc -p tsconfig.host.json')
expect(hostSteps).toContain('node --max-old-space-size=8192 ./node_modules/typescript/bin/tsc -p tsconfig.host.json')
expect(hostSteps.at(-1)).toBe('pnpm --filter @deepseek-ai/dsh-desktop run bundle')
expect(scripts.scripts['build:lib:client']).toContain('tsc -b tsconfig.client.json')
expect(scripts.scripts.typecheck).toBe('tsx scripts/build.ts --scope typecheck')
@@ -371,13 +374,14 @@ describe('CI workflow', () => {
|| !isRecord(windowsWorkflow.jobs['windows-checks-passed'])
|| !isRecord(workflow.jobs['node-24'])
|| !isRecord(workflow.jobs['node-24-coverage'])
|| !isRecord(workflow.jobs['node-24-lint'])
|| !isRecord(workflow.jobs['node-24-bench'])
|| !isRecord(workflow.jobs['node-24-consumers'])
|| !isRecord(workflow.jobs['node-compat'])
|| !isRecord(workflow.jobs['all-checks-passed'])
|| !isRecord(masterWorkflow.jobs)
|| !isRecord(masterWorkflow.jobs['serial-windows'])) {
throw new TypeError('CI workflow must define node-24, node-24-coverage, node-24-bench, node-24-consumers, node-compat, and all-checks-passed; CI Windows must define windows-build, windows-coverage, windows-native-tests, and windows-checks-passed; ci-master must define serial-windows')
throw new TypeError('CI workflow must define node-24, node-24-coverage, node-24-lint, node-24-bench, node-24-consumers, node-compat, and all-checks-passed; CI Windows must define windows-build, windows-coverage, windows-native-tests, and windows-checks-passed; ci-master must define serial-windows')
}
const windowsBuild = windowsWorkflow.jobs['windows-build']
@@ -670,12 +674,19 @@ describe('CI workflow', () => {
}
})
it('runs required benchmarks on standard hosted Linux independently of failover', () => {
it('runs required benchmarks on a four-vCPU Blacksmith runner and falls back to hosted Linux', () => {
const workflow = loadWorkflow('.github/workflows/ci.yml')
const benchmark = workflowJob(workflow, 'node-24-bench')
const aggregate = workflowJob(workflow, 'all-checks-passed')
expect(benchmark['runs-on']).toBe('ubuntu-24.04')
// The shared self-hosted pool is not an idle runner, so only the
// Blacksmith leg moves the benchmarks off hosted Linux.
expect(evaluateRunsOn(benchmark['runs-on'], { vars: { DSH_CI_FAILOVER_LINUX: 'blacksmith' } }))
.toBe('blacksmith-4vcpu-ubuntu-2404')
for (const mode of ['', 'selfhosted', 'unexpected']) {
expect(evaluateRunsOn(benchmark['runs-on'], { vars: { DSH_CI_FAILOVER_LINUX: mode } })).toBe('ubuntu-24.04')
}
expect(JSON.stringify(benchmark.steps)).not.toContain('DSH_CI_FAILOVER_LINUX')
expect(benchmark.if).toBe("github.event_name == 'pull_request'")
expect(benchmark.needs).toBeUndefined()
expect(benchmark['continue-on-error']).toBeUndefined()
@@ -738,7 +749,7 @@ describe('CI workflow', () => {
'wine_node "$scratch/logs/host-references.log" --import tsx/esm scripts/compile-referenced-projects.ts host',
)
const aggregate = wineGates.indexOf(
'wine_node "$scratch/logs/host-tsc.log" --max-old-space-size=4096 "$tsc_js" -p tsconfig.host.json --pretty false',
'wine_node "$scratch/logs/host-tsc.log" --max-old-space-size=8192 "$tsc_js" -p tsconfig.host.json --pretty false',
)
const metadata = wineGates.indexOf('wine_node "$scratch/logs/host-typert.log" --import tsx/esm scripts/generate-typert.ts')
expect(metadata).toBeGreaterThan(-1)
@@ -1078,7 +1089,7 @@ describe('DeepSeek e2e workflow', () => {
if (!Array.isArray(e2e.steps)) throw new TypeError('DeepSeek e2e workflow must define steps')
const step = e2e.steps.filter(isRecord).find(candidate => candidate.name === 'E2E tests (real DeepSeek API)')
expect(step).toMatchObject({ env: { DSH_E2E_MAX_WORKERS: 4 } })
expect(step).toMatchObject({ env: { DSH_E2E_MAX_WORKERS: 6 } })
})
})
@@ -1371,6 +1382,27 @@ describe('Weighted approval workflow', () => {
'persist-credentials': false,
},
})
const token = steps.find(step => step.id === 'decision-token')
expect(token).toMatchObject({
'continue-on-error': true,
uses: 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1',
with: {
'client-id': '${{ vars.DSH_APPROVAL_APP_CLIENT_ID }}',
'private-key': '${{ secrets.DSH_APPROVAL_APP_PRIVATE_KEY }}',
owner: 'deepseek-harness',
repositories: 'deepseek-harness',
'permission-checks': 'write',
},
})
const tokenIndex = steps.indexOf(token!)
expect(tokenIndex).toBeGreaterThan(steps.indexOf(checkout!))
expect(tokenIndex).toBeLessThan(steps.findIndex(step => step.id === 'revoke'))
for (const step of steps.filter(step => typeof step.run === 'string' && step.run.startsWith('node .github/review-ownership/check-approval.mjs'))) {
expect(step.env).toMatchObject({
DECISION_TOKEN: '${{ steps.decision-token.outputs.token }}',
DECISION_APP_SLUG: '${{ vars.DSH_APPROVAL_APP_SLUG }}',
})
}
const setupIndex = steps.findIndex(step => typeof step.uses === 'string' && step.uses.startsWith('actions/setup-python@'))
expect(steps[setupIndex]?.if).toBe("steps.revoke.outputs.active == 'true'")
expect(steps[setupIndex]?.uses).toBe('actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1')
@@ -1409,7 +1441,7 @@ describe('Weighted approval workflow', () => {
expect(record?.run).toBe("echo 'Recorded a weighted approval review event.'")
expect(recordSteps).toHaveLength(1)
expect(JSON.stringify(publisher)).not.toContain('github.event.pull_request.head')
expect(JSON.stringify(publisher)).not.toContain('secrets.')
expect(JSON.stringify(publisher).match(/secrets\.[A-Za-z0-9_]+/gu)).toEqual(['secrets.DSH_APPROVAL_APP_PRIVATE_KEY'])
expect(JSON.stringify(reviewEvent)).not.toContain('github.token')
expect(JSON.stringify(reviewEvent)).not.toContain('secrets.')
})
@@ -1554,8 +1586,13 @@ describe('npm release workflows', () => {
isRecord(step) && typeof step.run === 'string' ? [step.run] : [])
expect(Object.keys(workflow.on).sort()).toEqual(['pull_request', 'push', 'workflow_dispatch'])
expect(commands).toContain('pnpm run verify-package-dependencies')
expect(commands).toContain('pnpm run verify-npm-install-layout')
// Both verifications run from one step so the lane pays their longer half.
const combined = commands.find(command => command.includes('verify-package-dependencies'))
expect(combined).toContain('verify-npm-install-layout')
expect(combined).toContain('wait "$dependencies_pid"')
expect(commands).not.toContain('pnpm run verify-package-dependencies')
expect(commands).not.toContain('pnpm run verify-npm-install-layout')
expect(String(dependencies['runs-on'])).toContain('blacksmith-4vcpu-ubuntu-2404')
})
})
@@ -1649,3 +1686,49 @@ function workflowJob(workflow: Record<string, unknown>, job: string): Record<str
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
describe('Policy workflows', () => {
const workflows = [
['pr-check-on-head-branch.yml', ['master']],
['pr-check-after-approve.yml', ['approve', 'review']],
] as const
function stepsOf(job: Record<string, unknown>): Record<string, unknown>[] {
const steps = job.steps as unknown[]
if (!Array.isArray(steps)) throw new TypeError('job must define steps')
return steps.filter(isRecord)
}
it.each(workflows)('%s runs each check from master and reads its mode from a repository variable', (file, jobs) => {
const workflow = loadWorkflow('.github/workflows/' + file)
expect(Object.keys(workflow).filter(key => key !== 'name')).toEqual(['on', 'permissions', 'concurrency', 'jobs'])
const events = workflow.on
if (!isRecord(events)) throw new TypeError(`${file} must declare events`)
expect(events).not.toHaveProperty('pull_request')
expect(events).toHaveProperty('pull_request_target')
for (const job of jobs) {
const check = stepsOf(workflowJob(workflow, job)).find(step => typeof step.uses === 'string' && step.uses.startsWith('deepseek-harness/deepseek-harness-check@'))
if (check === undefined || !isRecord(check.env)) throw new TypeError(`${file} ${job} must run the check action with env`)
expect(String(check.env.CHECK_MODE)).toContain('vars.MERGE_POLICY_')
}
})
it('keeps the pre-merge build behind the merge step, which honours the mode on its failures', () => {
const steps = stepsOf(workflowJob(loadWorkflow('.github/workflows/pr-check-on-head-branch.yml'), 'pre-merge'))
const merge = steps.findIndex(step => step.id === 'merge')
const build = steps.findIndex(step => step.name === 'Run the pre-merge command')
expect(merge).toBeGreaterThanOrEqual(0)
expect(build).toBeGreaterThan(merge)
const merging = steps[merge]
if (merging === undefined || !isRecord(merging.env)) throw new TypeError('the merge step must set env')
expect(String(merging.env.CHECK_MODE)).toContain('MERGE_POLICY_PRE_MERGE')
for (const step of steps.slice(merge)) {
if (typeof step.run !== 'string') continue
expect(step.run).toContain('"$CHECK_MODE" = block')
}
for (const step of steps.slice(merge + 1)) {
if (step.name === 'Run the pre-merge command') continue
expect(step.if).toBe("steps.merge.outputs.merged == 'true'")
}
})
})
+1 -1
View File
@@ -9,7 +9,7 @@ import { describe, expect, it } from 'vitest'
const root = fileURLToPath(new URL('..', import.meta.url))
function clientCssDeclarations(): string[] {
const clientGroups = ['client', 'extensions']
const clientGroups = ['client', 'extensions', 'experimental']
return clientGroups.flatMap((group) => {
const clientRoot = resolve(root, 'packages', group)
return readdirSync(clientRoot, { withFileTypes: true })
+5 -3
View File
@@ -48,7 +48,7 @@ export function runPnpmCommand(root: string, args: readonly string[]): void {
* @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.
* @returns Arguments for `process.execPath`, including the heap limit the Host face needs.
* @returns Arguments for `process.execPath`, including an 8 GiB old-space heap limit for the Host face.
* @throws If the aggregate configuration is invalid, has no references, or can emit files.
*/
export function referencedProjectsCompilerArgs(
@@ -109,7 +109,7 @@ export function aggregateBuildArgs(face: 'host' | 'client'): string[] {
}
function hostHeapArgs(face: 'host' | 'client'): string[] {
return face === 'host' ? ['--max-old-space-size=4096'] : []
return face === 'host' ? ['--max-old-space-size=8192'] : []
}
/**
@@ -166,10 +166,12 @@ function pnpmStep(id: LibraryStepId, label: string, args: readonly string[], nee
/**
* List the package-library build steps in Host, Desktop, then Client dependency order.
* Source-only Typert generation supplies declarations before any package compilation.
* Client compilation waits for Host and Desktop bundling because split compiler faces
* rewrite shared lib/types files consumed by those bundles.
* @param root - Repository root containing the compiler and bundle configurations.
* @param faces - `host` lists Typert generation, Host compilation, and bundling; `all` adds Desktop and Client.
* @param mode - Check package types by default; benchmark emission defers diagnostics to required full builds.
* @returns The ordered steps; each step needs its predecessor.
* @returns Steps with explicit compiler and bundle dependencies.
* @throws If an aggregate configuration is invalid.
*/
export function libraryBuildSteps(root: string, faces: 'host' | 'all', mode: CompilationMode = 'checked'): LibraryStep[] {
+104
View File
@@ -0,0 +1,104 @@
/** Loader resolves configuration imports in their project when Node internals are unavailable. */
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import { existsSync } from 'node:fs'
import * as fs from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { describe, expect, it, onTestFinished, vi } from 'vitest'
import { createResolve } from '../vendor/loader/src/resolve.ts'
vi.mock('node:fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs/promises')>()
const mkdir = vi.fn(actual.mkdir)
return { ...actual, mkdir, default: { ...actual, mkdir } }
})
async function fixture() {
const root = await fs.mkdtemp(join(tmpdir(), 'dsh-cordis-resolve-'))
onTestFinished(() => fs.rm(root, { recursive: true, force: true }))
await fs.writeFile(join(root, 'package.json'), JSON.stringify({
name: 'fixture-app', type: 'module',
imports: { '#local': './local.js' }, exports: { './sub': './sub.js' },
}))
await fs.writeFile(join(root, 'local.js'), 'export const tag = "subpath-import"\n')
await fs.writeFile(join(root, 'sub.js'), 'export const tag = "self-reference"\n')
const dep = join(root, 'node_modules', 'fixture-dep')
await fs.mkdir(dep, { recursive: true })
await fs.writeFile(join(dep, 'package.json'), JSON.stringify({
name: 'fixture-dep', type: 'module',
exports: { '.': { import: './index.js' }, './sub': { import: './sub.js' } },
}))
await fs.writeFile(join(dep, 'index.js'), 'export const tag = "bare-dep"\n')
await fs.writeFile(join(dep, 'sub.js'), 'export const tag = "dependency-subpath"\n')
await fs.mkdir(join(root, 'src'))
await fs.writeFile(join(root, 'src', 'plugin.js'), 'export const tag = "relative"\n')
return { root, baseUrl: pathToFileURL(join(root, 'src', 'cordis.yml')).href }
}
async function treeFor(baseUrl: string) {
const ctx = new Context()
onTestFinished(() => ctx.fiber.dispose())
await ctx.plugin(Loader, { baseUrl })
ctx.loader.internal = undefined
return ctx.loader.root.tree
}
describe('Loader project resolution', () => {
it('loads import-only dependencies, subpaths and package mappings from the config project', async () => {
const { root, baseUrl } = await fixture()
const tree = await treeFor(baseUrl)
for (const [specifier, tag] of [
['fixture-dep', 'bare-dep'], ['fixture-dep/sub', 'dependency-subpath'],
['#local', 'subpath-import'], ['fixture-app/sub', 'self-reference'],
] as const) {
expect(await tree.import(specifier)).toMatchObject({ tag })
}
expect(existsSync(join(root, '.cordis', 'resolve.mjs'))).toBe(true)
expect(existsSync(join(root, 'src', '.cordis'))).toBe(false)
expect(await fs.readFile(join(root, '.cordis', '.gitignore'), 'utf8')).toContain('*')
await expect(tree.import('fixture-dep/missing')).rejects.toThrow()
})
it('keeps project scopes independent and shares concurrent resolution within one scope', async () => {
const first = await fixture()
const second = await fixture()
const [a, b, c] = await Promise.all([
createResolve(first.baseUrl),
createResolve(pathToFileURL(join(first.root, 'cordis.yml')).href),
createResolve(second.baseUrl),
])
expect(a).toBeTypeOf('function')
expect(a).toBe(b)
expect(a?.('fixture-dep')).not.toBe(c?.('fixture-dep'))
})
it('imports relative, URL and builtin specifiers without changing their identity', async () => {
const { root, baseUrl } = await fixture()
const tree = await treeFor(baseUrl)
expect(await tree.import('./plugin.js')).toMatchObject({ tag: 'relative' })
expect(existsSync(join(root, '.cordis'))).toBe(false)
expect(await tree.import(pathToFileURL(join(root, 'local.js')).href)).toMatchObject({ tag: 'subpath-import' })
expect(await tree.import('node:path')).toMatchObject({ join })
await expect(tree.import('./missing.js')).rejects.toThrow()
})
it('retains native import when helper creation is denied', async () => {
const { root, baseUrl } = await fixture()
const tree = await treeFor(baseUrl)
const denied = vi.mocked(fs.mkdir).mockRejectedValueOnce(Object.assign(new Error('read-only project'), { code: 'EACCES' }))
onTestFinished(() => { denied.mockReset() })
expect(await createResolve(baseUrl)).toBeUndefined()
expect(await tree.import('node:path')).toMatchObject({ join })
await expect(tree.import('fixture-dep')).rejects.toThrow()
expect(existsSync(join(root, '.cordis'))).toBe(false)
})
it('retains native import without a file-backed package scope', async () => {
expect(await createResolve(undefined)).toBeUndefined()
expect(await createResolve('https://example.com/cordis.yml')).toBeUndefined()
const tree = await treeFor('https://example.com/cordis.yml')
expect(await tree.import('node:path')).toMatchObject({ join })
})
})
+2 -1
View File
@@ -59,7 +59,7 @@ const PACKAGE_LIBRARIES: Readonly<Record<string, string>> = {
'packages/experimental/browser-use-runtime': 'Provider-owned browser resource management and MCP integration helpers; no plugin entry.',
'packages/boot/app-boot': 'Boot library the app bins import; plain helper exports.',
'packages/boot/cmdline': 'Command-line library the app bins import; plain module exports.',
'packages/boot/official-bundles': 'Bundle-list data library the launcher and plugin manager import; plain constant exports.',
'packages/util/official-bundles': 'Bundle-list data library the launcher and plugin manager import; plain constant exports.',
'packages/client/store': 'Browser-side state primitives; plain function/type exports.',
'packages/client/ui-primitives': 'Browser-side UI component library; plain component exports.',
'packages/client/ui-slots': 'Browser-side slot-map declarations; plain type exports.',
@@ -68,6 +68,7 @@ const PACKAGE_LIBRARIES: Readonly<Record<string, string>> = {
'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/experimental/hook-protocol': 'Shared wire-protocol library between the hook bridges.',
'packages/experimental/liveshare-protocol': 'Shared wire frames and identity contract; plain module exports with no plugin entry.',
'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.',
+12 -1
View File
@@ -1,6 +1,10 @@
/** Experimental package identities, publication policy, and retained npm names. */
/** Explicit npm identities retained across experimental classification and package-role moves. */
/**
* Experimental packages whose npm names omit the experimental prefix, by directory: identities retained across
* experimental classification and package-role moves, and packages named without the prefix so that leaving
* experimental status keeps their names.
*/
export const EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS: Readonly<Record<string, string>> = {
'packages/experimental/cua': '@deepseek-ai/dsh-cua',
'packages/experimental/cua-bundle': '@deepseek-ai/dsh-cua-bundle',
@@ -14,6 +18,13 @@ export const EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS: Readonly<Record<string, strin
'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',
'packages/experimental/action-cards': '@deepseek-ai/dsh-action-cards',
'packages/experimental/client-ui-action-cards': '@deepseek-ai/dsh-client-ui-action-cards',
'packages/experimental/creation-recommendations': '@deepseek-ai/dsh-creation-recommendations',
'packages/experimental/client-ui-creation-recommendations': '@deepseek-ai/dsh-client-ui-creation-recommendations',
'packages/experimental/plugin-recommendations': '@deepseek-ai/dsh-plugin-recommendations',
'packages/experimental/client-ui-plugin-recommendations': '@deepseek-ai/dsh-client-ui-plugin-recommendations',
'packages/experimental/personalized-recommendations': '@deepseek-ai/dsh-personalized-recommendations',
} satisfies Record<`packages/experimental/${string}`, `@deepseek-ai/dsh-${string}`>
/** npm namespace for experimental packages without retained-name exceptions. */
+2 -2
View File
@@ -48,9 +48,9 @@ const MAX_DECL_CHARS = 1200
* share that hands down a subsystem instead of a share, and prose that grew
* into a manual. Every report also lists one line per standard prop seat of its
* scope, so the widest Session-scope slot (`tool.call.toolview`, with the
* global and Session access seats) sets the current 122.
* global and Session access seats) sets the current 123.
*/
const MAX_ENTRY_LINES = 122
const MAX_ENTRY_LINES = 123
/** One register-call option as the catalog teaches it. */
interface OptionDoc {
+19
View File
@@ -60,6 +60,8 @@ export const SERVICE_PAGE: Record<string, string> = {
productAnalytics: 'product-telemetry.md',
connection: 'web-server.md',
pluginManager: 'boot.md',
actionCards: 'boot.md',
pluginRecommendations: 'boot.md',
pluginMarket: 'boot.md',
pluginRegistryProbe: 'boot.md',
configEditor: 'boot.md',
@@ -324,6 +326,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
PluginInstallRequestId: 'boot.md',
PluginSpecInspection: 'boot.md',
PluginChange: 'boot.md',
PluginOperation: 'boot.md',
PluginInstallLogChunk: 'boot.md',
PluginInstallProgress: 'boot.md',
PluginUpdateProgress: 'boot.md',
@@ -449,6 +452,16 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
SessionModelsRequest: 'session.md',
SessionPage: 'session.md',
SessionPageRequest: 'session.md',
SessionEventsPage: 'session.md',
SessionEventFrame: 'session.md',
SessionObserveEventRequest: 'session.md',
SessionProjectionFrame: 'session.md',
SessionProjectionChange: 'session.md',
SessionState: 'session.md',
SessionStateRequest: 'session.md',
SessionWaitRequest: 'session.md',
SessionWaitValue: 'session.md',
PendingRemoteEventInfo: 'typert.md',
SessionPromptRequest: 'session.md',
SessionPromptValue: 'session.md',
SessionRenameRequest: 'session.md',
@@ -549,6 +562,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
ScheduleListRequest: 'schedule.md',
ScheduleDeleteRequest: 'schedule.md',
ScheduleDeleteResult: 'schedule.md',
ScheduleDeleteInactiveResult: 'schedule.md',
ScheduleTimingChange: 'schedule.md',
ScheduleUpdateRequest: 'schedule.md',
ScheduleUpdateResult: 'schedule.md',
@@ -908,6 +922,11 @@ export const FOUNDATION_TYPE_NAMES: ReadonlySet<string> = new Set([
/** Project types deliberately documented outside the subsystems catalog. */
export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
ActionCardDecision: 'packages/experimental/action-cards/README.md',
ActionCardKind: 'packages/experimental/action-cards/README.md',
ActionCardResult: 'packages/experimental/action-cards/README.md',
ActionCardView: 'packages/experimental/action-cards/README.md',
RecommendationDetails: 'packages/experimental/plugin-recommendations/README.md',
ProductEvent: 'Desktop event fields are owned by packages/client/product-analytics/README.md and src/events.ts',
ConnectionFetchHandler: 'shared Fetch dispatch is owned by packages/client/connection/src/rpc.ts',
ConnectionRequestRejection: 'transport rejection status is owned by packages/client/connection/src/rpc.ts',
+18
View File
@@ -107,6 +107,22 @@ const GROUP_ORDER = [
]
const SERVICE_ROLES: ServiceRole[] = [
{
key: 'actionCards',
pkg: 'action-cards',
title: 'Optional action cards',
mode: 'service',
consumers: ['creation-recommendations', 'client-ui-action-cards'],
note: 'Owns the card tool, card state, user actions and status notices; card kinds register their actions and handlers.',
},
{
key: 'pluginRecommendations',
pkg: 'plugin-recommendations',
title: 'Explicit-need plugin recommendations',
mode: 'service',
consumers: ['client-ui-plugin-recommendations'],
note: 'Reads the current catalog text of an official plugin card and enables the plugin through the profile plugin manager after a click.',
},
{
key: 'headlessStartup',
pkg: 'headless',
@@ -1506,6 +1522,8 @@ function renderEventRelations(pkgs: Pkg[], events: readonly EventEntry[]): strin
lines.push(
'This matrix shows which packages dispatch each harness-owned event and which packages listen to it. Events are many-to-many, so the dense relation data is presented as a table rather than one large graph. Receiver and event-name types also cover contained dispatch sites that deliberately bypass `ctx.emit`, such as subagent lifecycle containment.',
'',
'Application listeners are outside this package matrix. Desktop Host subscribes to `plugin-manager/operation` through [`installOperationDiagnostics`](../apps/desktop-host/src/operation-diagnostics.ts) and forwards bounded records to the shell.',
'',
)
const rows = ['| Event | Mode | Declared in | Dispatchers | Listeners |', '| --- | --- | --- | --- | --- |']
for (const event of [...events].sort((a, b) => a.name.localeCompare(b.name))) {
+26 -6
View File
@@ -4,11 +4,13 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, uti
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/official-bundles/src/index.ts'
import { ON_DEMAND_BUNDLES } from '../packages/util/official-bundles/src/index.ts'
import { generateOfficialBundleCatalog, renderOfficialBundleCatalog, syncOfficialBundleCatalog } from './gen-official-bundle-catalog.ts'
import { SHIPPED_LOCALE_IDS } from './shipped-locales.ts'
const roots: string[] = []
const output = 'packages/boot/official-bundles/src/catalog.generated.ts'
const output = 'packages/util/official-bundles/src/catalog.generated.ts'
function write(root: string, path: string, content: unknown): void {
mkdirSync(dirname(join(root, path)), { recursive: true })
@@ -32,6 +34,7 @@ function fixture(): string {
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, `${dir}/locale/ja.json`, { meta: { title: 'ネイティブエージェント', description: 'タスクを委派します。' } })
for (const language of ['es', 'fr', 'de', 'ru', 'pt']) write(root, `${dir}/locale/${language}.json`, { meta: { title: 'Native agent', description: 'Delegate a task.' } })
}
write(root, output, '')
return root
@@ -50,8 +53,8 @@ describe('Official bundle catalog generation', () => {
const entries = generateOfficialBundleCatalog(fixture())
expect(entries.map(entry => entry.packageName)).toEqual(ON_DEMAND_BUNDLES)
expect(entries[0]?.meta).toEqual({
title: { en: 'Native agent', zh: '原生智能体', ja: 'ネイティブエージェント' },
description: { en: 'Delegate a task.', zh: '委派任务。', ja: 'タスクを委派します。' },
title: { en: 'Native agent', zh: '原生智能体', ja: 'ネイティブエージェント', ...Object.fromEntries(['es', 'fr', 'de', 'ru', 'pt'].map(id => [id, 'Native agent'])) },
description: { en: 'Delegate a task.', zh: '委派任务。', ja: 'タスクを委派します。', ...Object.fromEntries(['es', 'fr', 'de', 'ru', 'pt'].map(id => [id, 'Delegate a task.'])) },
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')
@@ -67,6 +70,23 @@ describe('Official bundle catalog generation', () => {
expect(() => generateOfficialBundleCatalog(root)).toThrow(/public DSH bundle|duplicate workspace/)
})
it('embeds the package platform declaration and detects changes before providers are installed', () => {
const root = fixture()
const name = ON_DEMAND_BUNDLES[0]!
updateManifest(root, name, { dsh: { bundle: { patch: './cordis.patch.yml', platforms: ['darwin'] } } })
expect(generateOfficialBundleCatalog(root)[0]?.platforms).toEqual(['darwin'])
syncOfficialBundleCatalog(root, false)
expect(readFileSync(join(root, output), 'utf8')).toContain("platforms: ['darwin']")
updateManifest(root, name, { dsh: { bundle: { patch: './cordis.patch.yml', platforms: ['win32'] } } })
expect(() => { syncOfficialBundleCatalog(root, true) }).toThrow('is stale')
})
it.each([[], ['macos'], 'darwin'].map(platforms => ({ platforms })))('rejects invalid package platform metadata $platforms', ({ platforms }) => {
const root = fixture()
updateManifest(root, ON_DEMAND_BUNDLES[0]!, { dsh: { bundle: { patch: './cordis.patch.yml', platforms } } })
expect(() => generateOfficialBundleCatalog(root)).toThrow('dsh.bundle.platforms')
})
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]!
@@ -81,14 +101,14 @@ describe('Official bundle catalog generation', () => {
expect(() => generateOfficialBundleCatalog(root, [ON_DEMAND_BUNDLES[0]!, ON_DEMAND_BUNDLES[0]!])).toThrow('distinct package names')
})
it.each(['icon', 'en', 'zh', 'ja'])('rejects missing %s metadata', (resource) => {
it.each(['icon', ...SHIPPED_LOCALE_IDS])('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', 'ja'])('requires explicit %s fields instead of metadata fallback text', (language) => {
it.each(SHIPPED_LOCALE_IDS)('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`)
+19 -9
View File
@@ -4,12 +4,15 @@ 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/official-bundles/src/bundles.ts'
import type { OfficialBundleCatalogEntry } from '../packages/boot/official-bundles/src/index.ts'
import type { DshBundleManifest, LocalizedText } from '../packages/util/package-manifest/src/types.ts'
import { ON_DEMAND_BUNDLES } from '../packages/util/official-bundles/src/bundles.ts'
import type { OfficialBundleCatalogEntry } from '../packages/util/official-bundles/src/index.ts'
import { readPluginMeta, resolvePluginResource } from '../packages/boot/app-boot/src/package-meta.ts'
import { assertBundlePlatform, UnsupportedBundlePlatformError } from '../packages/boot/app-boot/src/bundle-platform.ts'
const OUTPUT = 'packages/boot/official-bundles/src/catalog.generated.ts'
import { SHIPPED_LOCALE_IDS } from './shipped-locales.ts'
const OUTPUT = 'packages/util/official-bundles/src/catalog.generated.ts'
/**
* Read complete metadata without importing or activating any provider code.
@@ -19,7 +22,7 @@ const OUTPUT = 'packages/boot/official-bundles/src/catalog.generated.ts'
*/
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[] }>()
const manifests = new Map<string, { path: string; private: boolean; bundle: boolean; platforms?: DshBundleManifest['platforms']; 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
@@ -27,11 +30,12 @@ export function generateOfficialBundleCatalog(root: string, names: readonly stri
dependencies?: Record<string, string>
optionalDependencies?: Record<string, string>
peerDependencies?: Record<string, string>
dsh?: { bundle?: { patch?: unknown } }
dsh?: { bundle?: { patch?: unknown; platforms?: DshBundleManifest['platforms'] } }
}
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,
...manifest.dsh?.bundle?.platforms === undefined ? {} : { platforms: manifest.dsh.bundle.platforms },
runtime: runtime.flatMap(section => Object.keys(manifest[section] ?? {})) })
}
const visited = new Set<string>()
@@ -48,8 +52,12 @@ export function generateOfficialBundleCatalog(root: string, names: readonly stri
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`)
}
try { assertBundlePlatform(entry.platforms, process.platform) } catch (error) {
// Catalog generation validates every declaration, including bundles for another Host.
if (!(error instanceof UnsupportedBundlePlatformError)) throw error
}
const parentURL = pathToFileURL(resolve(root, entry.path)).href
for (const language of ['en', 'zh', 'ja']) {
for (const language of SHIPPED_LOCALE_IDS) {
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() === ''
@@ -63,13 +71,14 @@ export function generateOfficialBundleCatalog(root: string, names: readonly stri
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', 'ja']) {
for (const language of SHIPPED_LOCALE_IDS) {
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 } }
return { packageName, ...entry.platforms === undefined ? {} : { platforms: entry.platforms },
meta: { title: meta.title, description: meta.description, icon: meta.icon } }
})
}
@@ -85,6 +94,7 @@ export function renderOfficialBundleCatalog(entries: readonly OfficialBundleCata
+ 'export const OFFICIAL_ON_DEMAND_CATALOG: readonly OfficialBundleCatalogEntry[] = [\n'
+ entries.map(entry => ' {\n'
+ ` packageName: ${quote(entry.packageName)},\n`
+ (entry.platforms === undefined ? '' : ` platforms: [${entry.platforms.map(quote).join(', ')}],\n`)
+ ' meta: {\n'
+ ` title: ${localized(entry.meta.title)},\n`
+ ` description: ${localized(entry.meta.description)},\n`
+15
View File
@@ -43,6 +43,8 @@ import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubagentProvider } from '@deepseek-ai/dsh-subagent'
import * as ToolSubagentControl from '@deepseek-ai/dsh-tool-subagent-control'
import SkillRegistry from '@deepseek-ai/dsh-skill'
import ActionCards from '@deepseek-ai/dsh-action-cards'
import RemoteAccess from '@deepseek-ai/dsh-api-remote-access'
import * as SkillFileSystem from '@deepseek-ai/dsh-skill-filesystem'
import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local'
import * as ToolAskUser from '@deepseek-ai/dsh-tool-ask-user'
@@ -239,6 +241,19 @@ const TOOL_PACKAGES: ToolPackage[] = [
})
},
},
{
pkg: '@deepseek-ai/dsh-action-cards',
dir: 'action-cards',
source: 'packages/experimental/action-cards/src/index.ts',
requires: ['ctx.tools', 'ctx.agents', 'ctx.sessionProjections', 'ctx.remoteAccess'],
writes: ['tool/call', 'tool/result', 'plugin:action-cards/decision', 'user/message'],
async mount(ctx) {
await ctx.plugin(AgentRegistry)
await ctx.plugin(RemoteAccess)
await ctx.plugin(ActionCards)
},
note: 'Declared only to an Agent that loaded the skill of a registered card kind; the kind packages register their actions, and the Personalized recommendations bundle supplies the shipped kinds.',
},
{
pkg: '@deepseek-ai/dsh-tool-ask-user',
dir: 'tool-ask-user',
+1 -1
View File
@@ -8,7 +8,7 @@ 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/official-bundles/src/bundles.ts'
import { ON_DEMAND_BUNDLES } from '../packages/util/official-bundles/src/bundles.ts'
import { generateOfficialBundleCatalog } from './gen-official-bundle-catalog.ts'
import {
createRegistryManifest,
+46 -34
View File
@@ -1,7 +1,7 @@
/**
* Gate for the invariant `FALLBACK_LOCALE` rests on: every shipped dictionary
* declares the same keys in `zh`, `en`, and `ja`; Japanese dictionaries also
* preserve the English literal-template placeholders.
* declares the same keys in every shipped language; translations also
* preserve the English placeholders, command examples, and API field names.
*
* The locale runtime resolves a key through the active locale, then through
* the single fallback locale (`en`), then surfaces the key itself. With
@@ -27,6 +27,8 @@ import { fileURLToPath } from 'node:url'
import ts from 'typescript'
import { describe, expect, it } from 'vitest'
import { SHIPPED_LOCALE_IDS, localeTokens, hasInvisibleLocaleCharacters, type ShippedLocaleId } from './shipped-locales.ts'
const root = fileURLToPath(new URL('..', import.meta.url))
/** Repo-relative path with `/` separators, so messages and suffix tests match on every OS. */
@@ -101,7 +103,7 @@ function dictionariesIn(file: string): Dictionary[] {
// before parsing — the silent narrowing this gate exists to prevent. A bare
// `\b(zh|en)\b` misses `zhSettings`/`accessZh`, because `\b` does not hold
// between `h` and an uppercase letter.
if (!/\b(zh|en|ja)\b|\b(zh|en|ja)[A-Z]|(Zh|En|Ja)\b/.test(text)) return []
if (!/\b(zh|en|ja|es|fr|de|ru|pt)\b|\b(zh|en|ja|es|fr|de|ru|pt)[A-Z]|(Zh|En|Ja|Es|Fr|De|Ru|Pt)\b/.test(text)) return []
const source = ts.createSourceFile(file, text, ts.ScriptTarget.ESNext, true)
const found: Dictionary[] = []
const rel = relative(file)
@@ -153,7 +155,7 @@ function dictionariesIn(file: string): Dictionary[] {
if (name === 'register' && node.arguments.length >= 3) {
const [ns, tag, dict] = node.arguments
if (ns === undefined || tag === undefined || !ts.isStringLiteral(tag)) return
if (tag.text !== 'zh' && tag.text !== 'en' && tag.text !== 'ja') return
if (!SHIPPED_LOCALE_IDS.some(id => id === tag.text)) return
const raw = unwrap(dict)
const literal = raw !== undefined && ts.isIdentifier(raw)
? (() => {
@@ -185,7 +187,7 @@ function dictionariesIn(file: string): Dictionary[] {
const literal = unwrap(dict)
if (tag === undefined || !ts.isStringLiteral(tag)) continue
if (literal === undefined || !ts.isObjectLiteralExpression(literal)) continue
if (tag.text !== 'zh' && tag.text !== 'en' && tag.text !== 'ja') continue
if (!SHIPPED_LOCALE_IDS.some(id => id === tag.text)) continue
found.push({ file: rel, name: `${tag.text}@inline:${site}`, keys: keysOf(literal), templates: templatesOf(literal) })
}
}
@@ -239,8 +241,8 @@ function unwrap(node: ts.Expression | undefined): ts.Expression | undefined {
* @param name - export name or synthetic inline name.
* @returns locale plus pair key, or undefined when the name names no locale.
*/
function localeOf(name: string): { locale: 'zh' | 'en' | 'ja'; pair: string } | undefined {
for (const locale of ['zh', 'en', 'ja'] as const) {
function localeOf(name: string): { locale: ShippedLocaleId; pair: string } | undefined {
for (const locale of SHIPPED_LOCALE_IDS) {
const other = locale[0]!.toUpperCase() + locale.slice(1)
if (name === locale) return { locale, pair: '' }
// Synthetic names for inline shapes carry their own pair key after the
@@ -255,9 +257,9 @@ function localeOf(name: string): { locale: 'zh' | 'en' | 'ja'; pair: string } |
}
/**
* Check that each object-form registration supplies its Japanese dictionary.
* Check that each object-form registration supplies every shipped-language dictionary.
* @param file - absolute source path.
* @returns registration sites missing Japanese, or using an unresolved local identifier.
* @returns registration sites missing a shipped language, or using an unresolved local identifier.
*/
function registrationProblems(file: string): string[] {
const text = readFileSync(file, 'utf8')
@@ -289,11 +291,13 @@ function registrationProblems(file: string): string[] {
}
}
if (entries.has('zh') && entries.has('en')) {
const ja = entries.get('ja')
const site = `${relative(file)}:${source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1}`
if (ja === undefined) problems.push(`${site}: registration has no Japanese dictionary`)
else if (ts.isIdentifier(ja) && !imported.has(ja.text) && !locals.has(ja.text)) {
problems.push(`${site}: Japanese dictionary ${ja.text} is not imported or declared`)
for (const language of SHIPPED_LOCALE_IDS) {
const dictionary = entries.get(language)
if (dictionary === undefined) problems.push(`${site}: registration has no ${language} dictionary`)
else if (ts.isIdentifier(dictionary) && !imported.has(dictionary.text) && !locals.has(dictionary.text)) {
problems.push(`${site}: ${language} dictionary ${dictionary.text} is not imported or declared`)
}
}
}
}
@@ -305,11 +309,11 @@ function registrationProblems(file: string): string[] {
}
describe('shipped locale dictionaries', () => {
it('registers Japanese alongside every object-form bilingual dictionary', () => {
it('registers every shipped language alongside every object-form bilingual dictionary', () => {
expect(sourceFiles().flatMap(registrationProblems)).toEqual([])
})
it('keeps bilingual keys aligned and Japanese keys and placeholders complete', () => {
it('keeps bilingual keys aligned and shipped-language keys and placeholders complete', () => {
const files = sourceFiles()
// Guard the discovery itself: an empty or narrowed sweep would pass every
// assertion below while checking nothing.
@@ -322,12 +326,18 @@ describe('shipped locale dictionaries', () => {
const perFile = new Map<string, Dictionary[]>()
for (const file of files) {
const dicts = dictionariesIn(file)
if (dicts.length > 0) perFile.set(relative(file), dicts)
if (dicts.length > 0) {
expect(hasInvisibleLocaleCharacters(readFileSync(file, 'utf8')), relative(file)).toBe(false)
for (const dictionary of dicts) {
for (const text of Object.values(dictionary.templates)) expect(hasInvisibleLocaleCharacters(text), relative(file)).toBe(false)
}
perFile.set(relative(file), dicts)
}
}
const groups = new Map<string, Map<'zh' | 'en' | 'ja', Dictionary>>()
const place = (key: string, locale: 'zh' | 'en' | 'ja', dict: Dictionary): void => {
const slot = groups.get(key) ?? new Map<'zh' | 'en' | 'ja', Dictionary>()
const groups = new Map<string, Map<ShippedLocaleId, Dictionary>>()
const place = (key: string, locale: ShippedLocaleId, dict: Dictionary): void => {
const slot = groups.get(key) ?? new Map<ShippedLocaleId, Dictionary>()
if (slot.has(locale)) {
throw new Error(`two ${locale} dictionaries claim pair ${key}: ${slot.get(locale)?.file} and ${dict.file}`)
}
@@ -367,21 +377,23 @@ describe('shipped locale dictionaries', () => {
const enOnly = en.keys.filter(k => !zh.keys.includes(k))
if (zhOnly.length > 0) problems.push(`${zh.file} ${zh.name} has keys absent from ${en.name}: ${zhOnly.join(', ')}`)
if (enOnly.length > 0) problems.push(`${en.file} ${en.name} has keys absent from ${zh.name}: ${enOnly.join(', ')}`)
const ja = slot.get('ja')
if (ja === undefined) {
problems.push(`${key} has no Japanese dictionary`)
} else {
const missing = en.keys.filter(k => !ja.keys.includes(k))
const extra = ja.keys.filter(k => !en.keys.includes(k))
if (missing.length > 0 || extra.length > 0) {
problems.push(`${ja.file} ${ja.name}: missing [${missing.join(', ')}], extra [${extra.join(', ')}]`)
}
for (const [name, text] of Object.entries(en.templates)) {
const translated = ja.templates[name]
if (translated === undefined) continue
const placeholders = (value: string): string => (value.match(/\{\w+\}/gu) ?? []).sort().join(', ')
if (placeholders(text) !== placeholders(translated)) {
problems.push(`${ja.file} ${ja.name}.${name} changes placeholders from ${en.name}`)
for (const language of SHIPPED_LOCALE_IDS.filter(id => id !== 'en' && id !== 'zh')) {
const translatedDictionary = slot.get(language)
if (translatedDictionary === undefined) {
problems.push(`${key} has no ${language} dictionary`)
} else {
const missing = en.keys.filter(k => !translatedDictionary.keys.includes(k))
const extra = translatedDictionary.keys.filter(k => !en.keys.includes(k))
if (missing.length > 0 || extra.length > 0) {
problems.push(`${translatedDictionary.file} ${translatedDictionary.name}: missing [${missing.join(', ')}], extra [${extra.join(', ')}]`)
}
for (const [name, text] of Object.entries(en.templates)) {
const translated = translatedDictionary.templates[name]
if (translated === undefined) continue
const placeholders = (value: string): string => localeTokens(value).join(', ')
if (placeholders(text) !== placeholders(translated)) {
problems.push(`${translatedDictionary.file} ${translatedDictionary.name}.${name} changes placeholders or literal tokens from ${en.name}`)
}
}
}
}
+29 -1
View File
@@ -9,7 +9,16 @@ const workflow = yaml.load(readFileSync(resolve(import.meta.dirname, '../.github
concurrency: { group: string; 'cancel-in-progress': boolean }
jobs: Record<string, {
'runs-on': string[]
steps: Array<{ id?: string; name?: string; if?: string; uses?: string; run?: string; with?: Record<string, unknown> }>
env: Record<string, string>
steps: Array<{
id?: string
name?: string
if?: string
uses?: string
run?: string
env?: Record<string, string>
with?: Record<string, unknown>
}>
}>
}
@@ -29,6 +38,7 @@ it('confines signed Mac packaging to manual dispatch and a dedicated Apple Silic
expect(uploads[0]?.if).toBe("always() && steps.checkout.outcome == 'success'")
expect(uploads[1]?.if).toBe('success()')
expect(uploads[1]?.with?.path).toBe('${{ steps.retained.outputs.directory }}/*')
expect(uploads[1]?.with?.['compression-level']).toBe(0)
})
it('defaults to notarization and labels the explicit debugging option', () => {
@@ -40,3 +50,21 @@ it('defaults to notarization and labels the explicit debugging option', () => {
expect(steps.filter(step => step.run?.includes('args+=(--skip-notarization)'))).toHaveLength(2)
expect(steps.find(step => step.name === 'Retain installers for the selected mode')?.with?.name).toContain('${{ steps.settings.outputs.mode }}')
})
it('retains build inputs outside checkout and run-specific pnpm installations', () => {
const env = workflow.jobs.package!.env
expect(JSON.stringify(env)).not.toContain('runner.')
const steps = workflow.jobs.package!.steps
const cache = steps.find(step => step.name === 'Configure persistent package caches')!
expect(cache.env?.DSH_DESKTOP_PACKAGE_CACHE_DIR).toBe('${{ runner.tool_cache }}/dsh-desktop-package-cache')
expect(cache.run).toContain('PNPM_CONFIG_STORE_DIR=$DSH_DESKTOP_PACKAGE_CACHE_DIR/workspace-pnpm-store')
expect(cache.run).toContain('DSH_DESKTOP_PACKAGE_CACHE_DIR=$DSH_DESKTOP_PACKAGE_CACHE_DIR')
expect(steps.indexOf(cache)).toBeLessThan(steps.findIndex(step => step.name === 'Install locked dependencies'))
})
it('offers ZIP compression choices with a fast default', () => {
expect(workflow.on.workflow_dispatch).toMatchObject({ inputs: { zip_compression_level: {
type: 'choice', options: ['3', '7'], default: '3', required: true,
} } })
expect(workflow.jobs.package!.env.PACKAGE_MACOS_ZIP_COMPRESSION_LEVEL).toBe('${{ inputs.zip_compression_level }}')
})
-10
View File
@@ -214,7 +214,6 @@
"e929814f19128d947cc0188ecfe39833ffd5cf3f2c7155ddfef94ba9740d2bfd": 1
},
"packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts": {
"8204ecbe96e5e470dde7a6dad4f8a0e6809c30bd8095af16a3a5d378e6c6c0c7": 4,
"e9721557773cba96e50cea1b0c6737991a9fb2490bcd83e934311e46310d8b68": 1
},
"packages/api/session-controller/tests/control-queue.host.spec.ts": {
@@ -459,9 +458,6 @@
"packages/client/ui-commands/tests/browser-plugin.client.spec.ts": {
"04984a2ce1486250236e638e0197da0a1a840855ed3975c59130a85358c7e5fe": 1
},
"packages/client/ui-conversation/src/client/apply.ts": {
"4c73f40f3ce989f285848573d7154fb295886bba707ad829e2c8c089e326aeea": 1
},
"packages/client/ui-conversation/src/client/conversation/location-index.ts": {
"a7d8cc8d42e37ad5cc4882241492ac572ed8163c7fb8663180e893c7178e906f": 1
},
@@ -559,9 +555,6 @@
"e01c2c40d53d557b621628de47a0a6677e11b4f151fbefd865c97be99a7b6c48": 1,
"e69e0ec4368790661e1f7cae5c2059f757c213f0ebb0bd40ec23af982bc73d20": 1
},
"packages/client/ui-plan/tests/plan-resource.client.spec.ts": {
"fa53d5cd55d87b522c83a98968e3a2bf933b1cc1b84d2f009e79d6c702099786": 1
},
"packages/client/ui-primitives/tests/highlight-viewport.client.spec.tsx": {
"6061de0a1e01547eb1abf9d4e1b5fa79fda112fa35dda980775491123413dcce": 1
},
@@ -1278,11 +1271,8 @@
"d423bbcd1caff7aab2e2de52564efbc9061e775620a530157805a43d851c6fe9": 1
},
"packages/interaction/user-approval/tests/approval.spec.ts": {
"3261a2af17649cc35cf8698e3b7278101044ed1a07efce757615616e0f918d3a": 1,
"4dd1f265ec710503bb27f057a582099a1e9d8b5db80fb45f148e32fd2afb5325": 2,
"744b30e13b6390439c34f4970441a292a07f4ac3d26f25e9be75d1b5c18f4f33": 1,
"971ec45a1146ab52f2fb13f75b782d5d35c0fb2ddba1fb9ccab14cc54f7291d5": 1,
"a6a2f2761dfd14ff3624a7f8684b025d6aa733d41cac4381db7c83f2a38f0c35": 1,
"f33c4a795e2cda267cb28d2937c632d56134b8e6438d21e4bf74da2735b1c154": 1
},
"packages/interaction/user-questions/tests/user-questions.spec.ts": {
+21
View File
@@ -0,0 +1,21 @@
/** npm `os`/`cpu` package-field matching for build and release scripts. */
/**
* Test whether an npm `os` or `cpu` field admits a value, following npm's install checks.
* @param value - Raw manifest field; `undefined` admits every value.
* @param current - Target platform or architecture.
* @returns Whether the package may install for the target.
* @throws When the field is not an array of strings.
*/
export function acceptsNpmPlatform(value: unknown, current: string): boolean {
if (value === undefined) return true
if (!Array.isArray(value) || !value.every((tag): tag is string => typeof tag === 'string')) {
throw new Error('npm platform constraints must be arrays of strings')
}
let accepted = value.every(tag => tag.startsWith('!'))
for (const tag of value) {
if (tag === `!${current}`) return false
if (tag === current || (tag === 'any' && value.length === 1)) accepted = true
}
return accepted
}
+4 -1
View File
@@ -9,7 +9,7 @@ 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 { bundlePatchPaths, composeEntries, initProfile, readProfileManifest } from '../packages/boot/app-boot/src/profile.ts'
import { OPTIONAL_BUNDLES, PROFILE_TEMPLATES } from '../packages/boot/official-bundles/src/bundles.ts'
import { OPTIONAL_BUNDLES, PROFILE_TEMPLATES } from '../packages/util/official-bundles/src/bundles.ts'
import type { DshBundleManifest } from '../packages/util/package-manifest/src/types.ts'
import { createPluginProfile } from '../apps/desktop/src/project-manager.ts'
@@ -92,6 +92,9 @@ describe.each(['web', 'desktop'] as const)('%s optional bundle overrides', (surf
})
describe('optional bundles', () => {
it('admits Computer Use only on macOS Hosts', () => {
expect(bundles.get('@deepseek-ai/dsh-cua-bundle')?.manifest.dsh?.bundle?.platforms).toEqual(['darwin'])
})
const shipped = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'].map(name => bundle(name).patches)
it('ships at least one bundle switched off', () => {
+3
View File
@@ -57,12 +57,15 @@ const SAFE_HOST_DEPENDENCY_EXPORTS = {
'@deepseek-ai/dsh-deque': ['Deque'],
'@deepseek-ai/dsh-llm': ['callConfigEquals'],
'@deepseek-ai/dsh-session-format': ['sessionFormatLogFilename'],
'@deepseek-ai/dsh-storage-domain': ['defineDomain'],
'@deepseek-ai/dsh-timeout': ['MAX_TIMER_DELAY_MS'],
'@deepseek-ai/schemastery': ['default'],
} as const satisfies HostDependencyExports
/** Runtime exports that require every consumer to resolve the provider's shared peer instance. */
const PEER_REQUIRED_HOST_EXPORTS = {
// Profile settings must match the installed Node provider's configuration schema.
'@deepseek-ai/dsh-ptc-runtime-node/configuration': ['resolvePersistentExecution'],
// CUA checks UserQuestionError and preserves AttachmentError from image admission.
'@deepseek-ai/dsh-attachment': ['admitEncodedImages'],
'@deepseek-ai/dsh-user-questions': ['UserQuestionError'],
+44
View File
@@ -0,0 +1,44 @@
/** Worker entry: collect the source package uses of one slice of workspace packages. */
import { parentPort, workerData } from 'node:worker_threads'
import { readAllSourceUses } from './source-parse-uses.ts'
import type { PackageDependencyManifest } from './verify-package-dependencies.ts'
/** One package whose `src` tree this worker walks. */
interface SourceUsesTask {
/** Position of the package in the caller's selection, used to restore that order. */
readonly index: number
/** Package directory relative to the repository root. */
readonly dir: string
/** Package name, reported back so the caller can key the result. */
readonly name: string
/** Workspace manifest of the package, which decides @types-versus-runtime usage. */
readonly manifest: PackageDependencyManifest
}
/** Work one worker receives when it starts. */
export interface SourceUsesWorkerRequest {
/** Repository root containing the workspace sources. */
readonly root: string
/** Packages this worker walks, in caller order. */
readonly tasks: readonly SourceUsesTask[]
}
/** Source uses collected for one package. */
export interface SourceUsesWorkerResult {
/** Package name of the analysed package. */
readonly name: string
/** Bare package name to the repository paths whose source references it. */
readonly uses: Map<string, string[]>
}
const request = workerData as SourceUsesWorkerRequest
const results: SourceUsesWorkerResult[] = request.tasks.map(task => ({
name: task.name,
uses: readAllSourceUses(request.root, {
dir: task.dir,
manifestPath: `${task.dir}/package.json`,
manifest: task.manifest,
name: task.name,
}),
}))
parentPort?.postMessage(results)
+25 -5
View File
@@ -10,28 +10,48 @@ const workflow = yaml.load(readFileSync(resolve(import.meta.dirname, '../.github
env: Record<string, string>
jobs: Record<'preview', {
'runs-on': string
steps: Array<{ name?: string; uses?: string; run?: string; with?: Record<string, unknown>; env?: Record<string, string> }>
steps: Array<{ name?: string; uses?: string; run?: string; if?: string; 'continue-on-error'?: boolean; with?: Record<string, unknown>; env?: Record<string, string> }>
}>
}
const preview = workflow.jobs.preview
describe('PR preview workflow', () => {
it('keeps every PR author on the selected GitHub-hosted runner', () => {
it('keeps every PR author on the selected runner', () => {
expect(Object.keys(workflow.jobs)).toEqual(['preview'])
expect(preview['runs-on']).toBe('ubuntu-24.04')
// The required outcome is the build, so the lane runs on four vCPUs; the
// hosted label stays as the fallback when the Blacksmith pool is not chosen.
expect(preview['runs-on']).toContain('blacksmith-4vcpu-ubuntu-2404')
expect(preview['runs-on']).toContain("'ubuntu-24.04'")
expect(preview['runs-on']).toContain('DSH_CI_FAILOVER_LINUX')
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/[email protected]')?.with).toEqual({ 'persist-credentials': false })
})
it('fails the check only for build failures and warns about publication', () => {
const byName = (name: string) => preview.steps.find(step => step.name === name)
for (const name of ['Build workspace and check aggregate programs', 'Build the preview page and pack the VFS image', 'Shape the upload']) {
expect(byName(name)?.['continue-on-error'], `${name} must stay blocking`).toBeUndefined()
}
for (const name of ['Upload to Cloudflare Pages', 'Verify the protected deployment serves the image', 'Comment the preview URL']) {
expect(byName(name)?.['continue-on-error'], `${name} must warn instead of failing`).toBe(true)
}
const report = byName('Report preview publication failures')
expect(report?.if).toBe("steps.upload.outcome == 'failure' || steps.verify.outcome == 'failure' || steps.comment.outcome == 'failure'")
expect(report?.run).toContain('::warning::')
})
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:artifacts')
expect(commands).toContain('pnpm run build')
expect(commands).toContain('pnpm --filter @deepseek-ai/dsh-web-frontend run build:preview')
expect(commands.indexOf('pnpm run build:artifacts')).toBeLessThan(commands.indexOf('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'))
// The complete build also runs the aggregate type checks the required
// consumers dropped.
expect(commands).not.toContain('pnpm run build:check')
expect(preview.steps.filter(step => step.uses?.startsWith('actions/cache'))).toHaveLength(1)
expect(preview.steps.find(step => step.uses === 'actions/cache/[email protected]')?.with).toMatchObject({
key: "${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}",
+40 -2
View File
@@ -1,11 +1,11 @@
import { readFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'
import { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, join } from 'node:path'
import { zipSync } from 'fflate'
import { expect, it } from 'vitest'
import { downloadPrimaryRuntimeAsset, prepareOfficeSkillAssets, primaryRuntimePayloadDigest, smokePrimaryRuntime, unpackPrimaryRuntimeWheel } from './prepare.ts'
import { downloadPrimaryRuntimeAsset, prepareOfficeSkillAssets, primaryRuntimePayloadDigest, prunePnpmForeignNativePackages, smokePrimaryRuntime, unpackPrimaryRuntimeWheel } from './prepare.ts'
import lock from './lock.json' with { type: 'json' }
it('covers every SDK wheel target with the shared interpreter lock', () => {
@@ -167,3 +167,41 @@ it('keeps carrier pnpm versions aligned with the shared payload build', () => {
expect(desktop.devDependencies.pnpm).toBe(root.devDependencies.pnpm)
expect(root.packageManager).toBe(`pnpm@${root.devDependencies.pnpm}`)
})
it.each([
['darwin', 'arm64', ['reflink', 'reflink-darwin-arm64'], ['.bin', '@reflink', 'any-cpu', 'not-windows', 'semver']],
['darwin', 'x64', ['reflink', 'reflink-darwin-x64'], ['.bin', '@reflink', 'any-cpu', 'not-windows', 'semver']],
['win32', 'x64', ['reflink', 'reflink-win32-x64-msvc'], ['.bin', '@reflink', 'any-cpu', 'not-darwin', 'semver']],
] as const)('keeps only pnpm-bundled packages that %s/%s admits', async (platform, arch, reflink, top) => {
const root = await mkdtemp(join(tmpdir(), 'primary-pnpm-natives-'))
try {
const modules = join(root, 'dist', 'node_modules')
const packages: Record<string, { os?: string[]; cpu?: string[] }> = {
'semver': {}, '@reflink/reflink': {}, 'any-cpu': { cpu: ['any'] },
'@reflink/reflink-darwin-arm64': { os: ['darwin'], cpu: ['arm64'] },
'@reflink/reflink-darwin-x64': { os: ['darwin'], cpu: ['x64'] },
'@reflink/reflink-win32-x64-msvc': { os: ['win32'], cpu: ['x64'] },
'not-windows': { os: ['!win32'] },
'not-darwin': { os: ['!darwin'] },
'contradictory': { os: ['darwin', 'win32', '!darwin', '!win32'] },
}
for (const [name, fields] of Object.entries(packages)) {
await mkdir(join(modules, name), { recursive: true })
await writeFile(join(modules, name, 'package.json'), JSON.stringify({ name, ...fields }))
}
await mkdir(join(modules, '.bin'))
await writeFile(join(modules, '.modules.yaml'), '')
prunePnpmForeignNativePackages(root, platform, arch)
expect((await readdir(modules)).filter(name => name !== '.modules.yaml').sort()).toEqual(top)
expect((await readdir(join(modules, '@reflink'))).sort()).toEqual(reflink)
} finally { await rm(root, { recursive: true, force: true }) }
})
it('names the bundled manifest whose platform field is invalid', async () => {
const root = await mkdtemp(join(tmpdir(), 'primary-pnpm-invalid-'))
try {
await mkdir(join(root, 'dist', 'node_modules', 'invalid'), { recursive: true })
await writeFile(join(root, 'dist', 'node_modules', 'invalid', 'package.json'), JSON.stringify({ os: 'darwin' }))
expect(() => { prunePnpmForeignNativePackages(root, 'darwin', 'arm64') }).toThrow(/invalid.package\.json: npm platform constraints must be arrays of strings/u)
} finally { await rm(root, { recursive: true, force: true }) }
})
+32 -5
View File
@@ -2,15 +2,16 @@
import { execFileSync } from 'node:child_process'
import { createHash, randomUUID } from 'node:crypto'
import { cpSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, renameSync, rmSync, writeFileSync } from 'node:fs'
import { cpSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, renameSync, rmSync, writeFileSync } from 'node:fs'
import { cp } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { basename, dirname, join, resolve } from 'node:path'
import { parseArgs } from 'node:util'
import extractZip from 'extract-zip'
import { x as extractTar } from 'tar'
import { copyPrimaryRuntime, parsePrimaryRuntime, workspaceDependencyPaths, type PrimaryRuntimeManifest } from '@deepseek-ai/dsh-tool-workspace-dependencies'
import { acceptsNpmPlatform } from '../npm-platform.ts'
import lock from './lock.json' with { type: 'json' }
import { prunePrimaryRuntimePythonTests } from './prune-python-tests.ts'
@@ -125,7 +126,7 @@ export function primaryRuntimePayloadDigest(
// Identity preserves key order within the selected target, wheel records and distribution map, plus wheel-entry order.
// Bump format when extraction or assembly changes payload bytes without changing locked inputs.
return createHash('sha256').update(JSON.stringify({
format: 7, target, pythonVersion, pythonRelease, nodeVersion: pnpmVersion === undefined ? undefined : nodeVersion,
format: 8, target, pythonVersion, pythonRelease, nodeVersion: pnpmVersion === undefined ? undefined : nodeVersion,
artifact: runtimeLock.targets[target], wheels, pythonPackages, pnpm: pnpmVersion,
})).digest('hex')
}
@@ -148,6 +149,29 @@ export async function unpackPrimaryRuntimeWheel(archive: string, destination: st
})
}
/**
* Remove packages bundled directly under pnpm's dist/node_modules whose npm `os`/`cpu` fields exclude the target.
* The pnpm tarball carries every platform's optional native packages, such as `@reflink/reflink-darwin-x64`.
* Nested node_modules and the `libc` field are not inspected; pnpm 11 bundles neither nested native packages nor Linux variants.
* @param pnpm - Unpacked pnpm package directory.
* @param platform - Target Node.js platform.
* @param arch - Target Node.js architecture.
* @throws When a bundled package manifest is missing or has invalid `os`/`cpu` fields.
*/
export function prunePnpmForeignNativePackages(pnpm: string, platform: NodeJS.Platform, arch: NodeJS.Architecture): void {
const modules = join(pnpm, 'dist', 'node_modules')
const directories = (parent: string): string[] => readdirSync(parent, { withFileTypes: true })
.filter(entry => entry.isDirectory() && entry.name !== '.bin').map(entry => join(parent, entry.name))
for (const directory of directories(modules).flatMap(entry => basename(entry).startsWith('@') ? directories(entry) : [entry])) {
const { os, cpu } = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8')) as { os?: unknown; cpu?: unknown }
let admitted: boolean
try { admitted = acceptsNpmPlatform(os, platform) && acceptsNpmPlatform(cpu, arch) } catch (error) {
throw new Error(`primary runtime: ${directory}/package.json: ${(error as Error).message}`, { cause: error })
}
if (!admitted) rmSync(directory, { recursive: true })
}
}
/**
* Copy the skill package's complete asset tree to ordinary filesystem resources.
* @param source - The package's assets directory.
@@ -195,6 +219,8 @@ export async function preparePrimaryRuntime(options: PreparePrimaryRuntimeOption
const output = join(staging, 'payload')
const dependencies = join(output, 'dependencies')
mkdirSync(dependencies, { recursive: true })
const platform = target === 'win-x64' ? 'win32' : target.startsWith('linux-') ? 'linux' : 'darwin'
const arch = target.endsWith('-arm64') ? 'arm64' : 'x64'
let pnpmVersion: string | undefined
if (!options.pythonOnly) {
const node = join(dependencies, 'node')
@@ -209,12 +235,13 @@ export async function preparePrimaryRuntime(options: PreparePrimaryRuntimeOption
pnpmVersion = (JSON.parse(readFileSync(pnpmManifest, 'utf8')) as { version: string }).version
await cp(dirname(pnpmManifest), join(dependencies, 'pnpm'), { recursive: true, dereference: true })
}
prunePnpmForeignNativePackages(join(dependencies, 'pnpm'), platform, arch)
}
await extractTar({ file: await pythonArchive(target, paths.downloads), cwd: dependencies })
const manifest: PrimaryRuntimeManifest = {
desktopVersion: options.version,
platform: target === 'win-x64' ? 'win32' : target.startsWith('linux-') ? 'linux' : 'darwin',
arch: target.endsWith('-arm64') ? 'arm64' : 'x64',
platform,
arch,
payloadDigest: primaryRuntimePayloadDigest(target, lock, pnpmVersion),
python: lock.pythonVersion,
...(pnpmVersion === undefined ? {} : { node: lock.nodeVersion, pnpm: pnpmVersion }),
+10 -1
View File
@@ -5,7 +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/official-bundles/src/bundles.ts'
import { ON_DEMAND_BUNDLES } from '../../packages/util/official-bundles/src/bundles.ts'
import { releaseFamily, type ReleaseMember } from './families.ts'
import { compareVersions, nextVendorVersion, planShared, reachesPayload } from './bump.ts'
@@ -49,6 +49,7 @@ describe('release families', () => {
expect(members
.filter(member => member.directory.startsWith('packages/experimental/'))
.map(member => member.name)).toEqual([
'@deepseek-ai/dsh-action-cards',
'@deepseek-ai/dsh-experimental-agent-team-profile',
'@deepseek-ai/dsh-experimental-agent-team',
'@deepseek-ai/dsh-experimental-api-speech-to-text',
@@ -59,13 +60,18 @@ describe('release families', () => {
'@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-client-ui-action-cards',
'@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-client-ui-creation-recommendations',
'@deepseek-ai/dsh-client-ui-plugin-recommendations',
'@deepseek-ai/dsh-experimental-client-ui-stats',
'@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-creation-recommendations',
'@deepseek-ai/dsh-cua-bundle',
'@deepseek-ai/dsh-experimental-cua-macos',
'@deepseek-ai/dsh-cua',
@@ -74,7 +80,10 @@ describe('release families', () => {
'@deepseek-ai/dsh-hooks-codex',
'@deepseek-ai/dsh-experimental-inspector-profile',
'@deepseek-ai/dsh-experimental-inspector',
'@deepseek-ai/dsh-experimental-liveshare-protocol',
'@deepseek-ai/dsh-personalized-recommendations',
'@deepseek-ai/dsh-experimental-plugin-market-bundle',
'@deepseek-ai/dsh-plugin-recommendations',
'@deepseek-ai/dsh-experimental-ptc-runtime-python',
'@deepseek-ai/dsh-experimental-ralph-bundle',
'@deepseek-ai/dsh-experimental-session-inspector',
+1 -1
View File
@@ -16,7 +16,7 @@ import {
readClientBuildRecord,
} from '../client-build-environment.ts'
import { validateTarballPayload } from '../publication-payload.ts'
import { ON_DEMAND_BUNDLES } from '../../packages/boot/official-bundles/src/bundles.ts'
import { ON_DEMAND_BUNDLES } from '../../packages/util/official-bundles/src/bundles.ts'
/**
* Dependency sections a consumer must publish after, because npm resolves them
@@ -4,7 +4,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/official-bundles/src/bundles.ts'
import { OPTIONAL_BUNDLES } from '../../packages/util/official-bundles/src/bundles.ts'
interface InstalledManifest {
name: string
+2 -14
View File
@@ -22,6 +22,7 @@ import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { parseArgs } from 'node:util'
import { acceptsNpmPlatform } from '../npm-platform.ts'
import { releaseFamily } from './families.ts'
import { capture, isEntry } from './process.ts'
import { packedManifest } from './tarball.ts'
@@ -45,19 +46,6 @@ function consumerEnvironment(consumerRoot: string): NodeJS.ProcessEnv {
return environment
}
function acceptsPlatform(value: unknown, current: string): boolean {
if (value === undefined) return true
if (!Array.isArray(value) || !value.every((tag): tag is string => typeof tag === 'string')) {
throw new Error('packed platform constraints must be arrays of strings')
}
let accepted = value.every(tag => tag.startsWith('!'))
for (const tag of value) {
if (tag === `!${current}`) return false
if (tag === current || (tag === 'any' && value.length === 1)) accepted = true
}
return accepted
}
/**
* Packed tarballs and their compatibility with the verification host, as `file:` dependency entries.
*
@@ -77,7 +65,7 @@ function packedDependencies(directories: readonly string[]): Map<string, { url:
const manifest = packedManifest(tarball)
const { name, version } = manifest
if (typeof name !== 'string' || typeof version !== 'string') throw new Error(`${tarball} manifest lacks name/version`)
const compatible = acceptsPlatform(manifest.os, process.platform) && acceptsPlatform(manifest.cpu, process.arch)
const compatible = acceptsNpmPlatform(manifest.os, process.platform) && acceptsNpmPlatform(manifest.cpu, process.arch)
dependencies.set(name, { url: pathToFileURL(tarball).href, version, compatible })
}
}
+3
View File
@@ -102,6 +102,9 @@ const GENERIC_SKIPS: readonly GenericSkip[] = [
{ file: 'apps/cli/tests/profiles/web/tests/fixtures/creator-plugin-manager.mjs', upstream: ['cordis'] },
{ file: 'apps/web/tests/agent-preset-authoring.e2e.ts', upstream: ['cordis'] },
{ file: 'packages/preset/agent-preset-registry/tests/session.spec.ts', upstream: ['cordis'] },
{ file: 'packages/experimental/creation-recommendations/src/index.ts', upstream: ['cordis'] },
{ file: 'packages/experimental/creation-recommendations/tests/creation.spec.ts', upstream: ['cordis'] },
{ file: 'packages/experimental/creation-recommendations/tests/loader-composition.spec.ts', upstream: ['cordis'] },
// Preset selection, reminder policy and UI copy use the same product id.
{ file: 'apps/cli/tests/optional-bundle-transitions.e2e.ts', upstream: ['cordis'] },
{ file: 'apps/web/tests/agent-preset-selection.e2e.ts', upstream: ['cordis'] },
+70
View File
@@ -0,0 +1,70 @@
/** Run the benchmark suite in the two phases its measurements tolerate. */
import { resolve } from 'node:path'
import { runPnpmCommand } from './compile-referenced-projects.ts'
/**
* Cases whose wall-clock endpoints cannot absorb a sibling: they drive a
* browser page, compare animation frames or paced streams, or spawn fresh
* children and budget their startup. Corpus cases tolerate a sibling.
*/
export const SERIAL_BENCH_FILES = [
'benchmarks/long-session-browser/long-session.bench.ts',
'benchmarks/user-text/user-text.bench.client.ts',
'benchmarks/conversation-fold/conversation-fold.bench.client.ts',
'benchmarks/active-stream-reconnect/reconnect.bench.client.ts',
// Fresh-child reopen case: its endpoints budget 63 ms and 120 ms, and a
// sibling process pushed them to 76 ms and 131 ms.
'benchmarks/session-open/session-open.bench.ts',
// Request-history budgets 297 ms against a 212 ms healthy median; a sibling
// pushed it to 342 ms.
'benchmarks/agent-continuation/agent-continuation.bench.ts',
] as const
const CONFIG = ['--config', 'vitest.bench.config.ts'] as const
/**
* Run one phase of the suite.
* @param root - Repository root containing the Vitest configuration.
* @param phase - `first` clears the report and runs the CPU cases two at a time;
* `last` runs the browser cases one at a time and renders the report.
* @throws If the phase fails.
*/
export function runBenchPhase(root: string, phase: 'first' | 'last'): void {
process.env.DSH_BENCH_REPORT_PHASE = phase
try {
if (phase === 'first') {
runPnpmCommand(root, [
'exec', 'vitest', 'run', ...CONFIG,
...SERIAL_BENCH_FILES.map(file => `--exclude=${file}`),
])
return
}
runPnpmCommand(root, [
'exec', 'vitest', 'run', ...CONFIG,
'--no-file-parallelism', '--maxWorkers=1',
...SERIAL_BENCH_FILES,
])
} finally {
delete process.env.DSH_BENCH_REPORT_PHASE
}
}
/**
* Run both phases, so a first-phase failure still reports every later case.
* @param root - Repository root containing the Vitest configuration.
* @returns the process exit code: 1 when either phase failed.
*/
export function runBenchSuite(root: string): number {
let failed = false
for (const phase of ['first', 'last'] as const) {
try {
runBenchPhase(root, phase)
} catch (error) {
console.error(error instanceof Error ? error.message : String(error))
failed = true
}
}
return failed ? 1 : 0
}
if (import.meta.main) process.exitCode = runBenchSuite(resolve(import.meta.dirname, '..'))
+12 -10
View File
@@ -282,7 +282,7 @@ describe('gate graph validation', () => {
'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')
expect(scripts['test:bench:built']).toBe('tsx scripts/run-bench-suite.ts')
})
it('checks all maintained repository references locally and in CI', () => {
@@ -837,10 +837,12 @@ describe('Typert contract preparation', () => {
it('reuses contracts from the validated consumer build', () => {
const subject = withPnpmEntrypoint(() => gatesForMode('ci-consumers'))
expect(subject.find(item => item.id === 'lint-and-duplication')).toMatchObject({
displayCommand: 'pnpm run check:ci:lint:contracts-ready',
args: ['/private/pnpm.cjs', 'run', 'check:ci:lint:contracts-ready'],
})
// Lint and duplication run in their own lane; the consumers graph only
// keeps the readers that consume built artifacts.
expect(subject.map(item => item.id)).not.toContain('lint-and-duplication')
expect(withPnpmEntrypoint(() => gatesForMode('ci-lint-contracts-ready'))).toContainEqual(expect.objectContaining({
id: 'lint',
}))
expect(subject.find(item => item.id === 'doc-typecheck')).toMatchObject({
displayCommand: 'pnpm run doc-typecheck:contracts-ready',
args: ['/private/pnpm.cjs', 'run', 'doc-typecheck:contracts-ready'],
@@ -956,7 +958,6 @@ describe('Node 24 lane ownership', () => {
'build',
'node-compat',
'publint',
'lint-and-duplication',
'snapshot',
'expected-output',
'web-snapshot',
@@ -965,13 +966,15 @@ describe('Node 24 lane ownership', () => {
'built-bin-smoke',
])
expect(subject.find(item => item.id === 'publint')?.needs).toEqual(['build'])
expect(subject.find(item => item.id === 'build')?.env).toEqual({
DSH_BUILD_CLIENT_PROFILE: 'official',
// The consumers lane emits artifacts only: the aggregate test and script
// type checks belong to the master lanes, not to every pull request.
expect(subject.find(item => item.id === 'build')).toMatchObject({
displayCommand: 'pnpm run build:artifacts',
env: { DSH_BUILD_CLIENT_PROFILE: 'official' },
})
expect(subject.find(item => item.id === 'node-compat')?.env).toEqual({
DSH_BUILD_CLIENT_PROFILE: 'official',
})
expect(subject.find(item => item.id === 'lint-and-duplication')?.needs).toEqual(['build'])
for (const id of [
'snapshot',
'expected-output',
@@ -1003,7 +1006,6 @@ describe('Node 24 lane ownership', () => {
env: { DSH_SNAPSHOT: 'replay' },
after: [
'publint',
'lint-and-duplication',
'snapshot',
'expected-output',
'doc-typecheck',
+15 -8
View File
@@ -279,6 +279,17 @@ function ciBuildGate(id = 'build', options: Partial<Gate> = {}): Gate {
})
}
/**
* Emit the artifacts every artifact consumer reads, without the aggregate
* test and script type checks that the master lanes own.
*/
function ciArtifactBuildGate(id = 'build', options: Partial<Gate> = {}): Gate {
return pnpmScript(id, 'build:artifacts', {
...options,
env: { ...options.env, [CLIENT_BUILD_PROFILE_SELECTOR]: 'official' },
})
}
function pnpmExec(id: string, args: string[], options: Partial<Gate> = {}): Gate {
return {
id,
@@ -392,8 +403,8 @@ function ciSharedStaticGates(): Gate[] {
function i18nGates(includeDictionaries: boolean): Gate[] {
return [
pnpmScript('installer-locales', 'verify-installer-locales', { label: 'Windows installer locales' }),
pnpmScript('locale-metadata', 'verify-locale-metadata', { label: 'Japanese plugin metadata' }),
...includeDictionaries ? [pnpmExec('locale-dictionaries', ['vitest', 'run', 'scripts/locale-dictionary-parity.spec.ts'], { label: 'locale dictionaries' })] : [],
pnpmScript('locale-metadata', 'verify-locale-metadata', { label: 'plugin locale metadata' }),
...includeDictionaries ? [pnpmExec('locale-dictionaries', ['vitest', 'run', 'scripts/locale-dictionary-parity.spec.ts', 'scripts/shipped-locales.spec.ts', 'packages/client/locale/tests/apply.client.spec.ts'], { label: 'locale dictionaries' })] : [],
]
}
@@ -530,7 +541,6 @@ function ciConsumerGates(): Gate[] {
// writer starts; `after` preserves the web diagnostic even if a reader fails.
const buildArtifactReaders = [
'publint',
'lint-and-duplication',
'snapshot',
'expected-output',
'doc-typecheck',
@@ -538,16 +548,12 @@ function ciConsumerGates(): Gate[] {
'built-bin-smoke',
]
return [
ciBuildGate(),
ciArtifactBuildGate(),
pnpmScript('node-compat', 'check:node-compat', {
label: 'Node compatibility',
env: { [CLIENT_BUILD_PROFILE_SELECTOR]: 'official' },
}),
pnpmScript('publint', 'publint', { needs: builtTree }),
pnpmScript('lint-and-duplication', 'check:ci:lint:contracts-ready', {
label: 'lint and duplication',
needs: builtTree,
}),
snapshotGate(builtTree),
expectedOutputGate(builtTree),
webSnapshotGate(builtTree, buildArtifactReaders),
@@ -873,6 +879,7 @@ function docSyncLeafGates(options: {
'website/tests/mermaid-viewer.spec.ts',
'website/tests/image-viewer.spec.ts',
'website/tests/code-groups.spec.ts',
'website/tests/table-filter.spec.ts',
'website/tests/page-markdown-actions.spec.ts', 'website/tests/raw-markdown.spec.ts',
], {
label: 'documentation site checks',
+44
View File
@@ -0,0 +1,44 @@
import { readFileSync } from 'node:fs'
import * as desktop from '../apps/desktop/src/locale.ts'
import { readInstallerLocales } from '../apps/desktop/scripts/installer-locales.mjs'
/** Translations preserve executable command examples and named interpolation fields. */
import { expect, it } from 'vitest'
import { SHIPPED_LOCALE_IDS, localeTokens, hasInvisibleLocaleCharacters } from './shipped-locales.ts'
it('retains repeated placeholders, commands, and API identifiers', () => {
expect(localeTokens('Use /plan off, /compact and /THEME=dark with {count}, {count} and reasoning_effort.'))
.toEqual(['/THEME=dark', '/compact', '/plan off', 'reasoning_effort', '{count}', '{count}'])
})
it('ignores translated prose and non-command slashes', () => {
expect(localeTokens('Cmd/Ctrl+Enter · tok/s · https://example.test/v1')).toEqual([])
expect(localeTokens('Используйте /plan off и {count}'))
.toEqual(localeTokens('Use /plan off and {count}'))
})
it.each([['/plan off', '/plan désactivé'], ['/THEME=light', '/THEME=claro'], ['reasoning_effort', 'Reasoning_effort'], ['{count}', '{other}']])('detects a change from %s to %s', (source, translated) => {
expect(localeTokens(translated)).not.toEqual(localeTokens(source))
})
it('recognizes commands at the start of copy or after CJK punctuation', () => {
expect(localeTokens('/THEME=auto、/THEME=light')).toEqual(['/THEME=auto', '/THEME=light'])
})
it.each(['\u200b', '\ufeff'])('rejects an invisible translation character', (character) => {
expect(hasInvisibleLocaleCharacters(`text${character}text`)).toBe(true)
expect(hasInvisibleLocaleCharacters('normal text')).toBe(false)
})
it('matches Desktop dictionaries, resolution and installer resources to the shipped catalog', () => {
const dictionaries = Object.keys(desktop).filter(name => /^[a-z]{2}$/u.test(name)).sort()
expect(dictionaries).toEqual([...SHIPPED_LOCALE_IDS].sort())
for (const id of SHIPPED_LOCALE_IDS) {
expect(desktop.resolveDesktopLocale(id).id).toBe(id === 'zh' ? 'zh-CN' : id)
expect(desktop.resolveDesktopStartupLocale(null, [id]).id).toBe(id === 'zh' ? 'zh-CN' : id)
}
expect(readInstallerLocales().map(locale => locale.id.split('_')[0]).sort()).toEqual(dictionaries)
})
it('rejects invisible characters in the Desktop locale source', () => {
expect(hasInvisibleLocaleCharacters(readFileSync(new URL('../apps/desktop/src/locale.ts', import.meta.url), 'utf8'))).toBe(false)
})
+23
View File
@@ -0,0 +1,23 @@
/** Languages whose UI dictionaries and package metadata ship with Harness. */
export const SHIPPED_LOCALE_IDS = ['en', 'zh', 'ja', 'es', 'fr', 'de', 'ru', 'pt'] as const
/** Locale id required by repository translation checks. */
export type ShippedLocaleId = typeof SHIPPED_LOCALE_IDS[number]
/**
* Extract copy tokens that translations must preserve verbatim.
* @param text - Dictionary text containing placeholders, slash commands or API field names.
* @returns Sorted tokens, retaining repeated occurrences.
*/
export function localeTokens(text: string): string[] {
return (text.match(/\{\w+\}|(?<![A-Za-z0-9/])\/[A-Za-z][\w-]*(?:=[\w-]+| (?:on|off))?|\b[a-z]+_[a-z_]+\b/gu) ?? []).sort()
}
/**
* Detect translation-service artifacts that must not enter locale resources.
* @param text - Raw resource or localized text.
* @returns Whether the text contains a zero-width space or byte-order mark.
*/
export function hasInvisibleLocaleCharacters(text: string): boolean {
return /[\u200b\ufeff]/u.test(text)
}
@@ -98,7 +98,7 @@
"content": [
{
"type": "text",
"text": "Serial agent creation completed."
"text": "Serial agent creation completed. Ordinary and nested forks are independent; historical fork metadata remains readable."
}
],
"source": {
@@ -1156,7 +1156,7 @@
"content": [
{
"type": "text",
"text": "Serial agent creation completed."
"text": "Serial agent creation completed. Ordinary and nested forks are independent; historical fork metadata remains readable."
}
],
"source": {
@@ -2,14 +2,14 @@
{"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-step","start":0,"inserted":[{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed. Ordinary and nested forks are independent; historical fork metadata remains readable."}],"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":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed. Ordinary and nested forks are independent; historical fork metadata remains readable."}],"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"}
@@ -98,7 +98,7 @@
"content": [
{
"type": "text",
"text": "Serial agent creation completed."
"text": "Serial agent creation completed. Ordinary and nested forks are independent; historical fork metadata remains readable."
}
],
"source": {
@@ -1628,7 +1628,7 @@
"content": [
{
"type": "text",
"text": "Serial agent creation completed."
"text": "Serial agent creation completed. Ordinary and nested forks are independent; historical fork metadata remains readable."
}
],
"source": {
@@ -2,14 +2,14 @@
{"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-step","start":0,"inserted":[{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed. Ordinary and nested forks are independent; historical fork metadata remains readable."}],"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":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed. Ordinary and nested forks are independent; historical fork metadata remains readable."}],"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"}
+225
View File
@@ -0,0 +1,225 @@
/** Source-text parsing shared by the package dependency and client package gates. */
import { globSync, readFileSync } from 'node:fs'
import { isBuiltin } from 'node:module'
import { resolve, sep } from 'node:path'
import ts from 'typescript'
import type { PackageDependencyManifest, WorkspacePackageManifest } from './verify-package-dependencies.ts'
export type DependencySection = 'dependencies' | 'devDependencies' | 'optionalDependencies' | 'peerDependencies'
export const ROOT_TYPE_PACKAGES: ReadonlySet<string> = new Set(['@types/react', '@types/react-dom'])
/**
* Report whether a specifier names a bare package rather than a path or subpath import.
* @param specifier - module specifier text.
* @returns whether the specifier resolves outside this package.
*/
function isBareSpecifier(specifier: string): boolean {
return !specifier.startsWith('.') && !specifier.startsWith('/') && !specifier.startsWith('#')
}
/**
* Read the leading package segments of one bare module specifier.
* @param specifier - module specifier text that already passed {@link isBareSpecifier}.
* @returns the package name this specifier belongs to.
*/
function packageNameForUses(specifier: string): string {
const segments = specifier.split('/')
return segments.slice(0, specifier.startsWith('@') ? 2 : 1).join('/')
}
/**
* Read the bare package name of one module specifier.
* @param specifier - module specifier text.
* @returns the package name, or undefined for paths, builtins, and non-package specifiers.
*/
export function packageNameOf(specifier: string): string | undefined {
if (isBuiltin(specifier) || specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('#')
|| specifier.includes(':') || specifier.includes('*')) {
return undefined
}
const parts = specifier.split('/')
return specifier.startsWith('@') ? parts.length >= 2 ? `${parts[0]}/${parts[1]}` : undefined : parts[0]
}
/**
* Read package manifests used for scope discovery and workspace-name checks.
* @param root - Repository containing the workspace manifests.
* @returns Every workspace package, the non-experimental release packages, and the packages the policy covers.
*/
export function addUse(target: Map<string, string[]>, name: string, path: string): void {
const paths = target.get(name) ?? []
if (!paths.includes(path)) paths.push(path)
target.set(name, paths)
}
export function section(manifest: PackageDependencyManifest, name: DependencySection): Record<string, string> {
return manifest[name] ?? {}
}
export function mutableSection(manifest: PackageDependencyManifest, name: DependencySection): Record<string, string> {
manifest[name] ??= {}
return manifest[name]
}
export function declaredSections(manifest: PackageDependencyManifest, name: string): DependencySection[] {
return (['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'] as const)
.filter(sectionName => section(manifest, sectionName)[name] !== undefined)
}
function importCarriesRuntimeValue(node: ts.ImportDeclaration): boolean {
const clause = node.importClause
if (clause === undefined) return true
if (clause.phaseModifier === ts.SyntaxKind.TypeKeyword) return false
const bindings = clause.namedBindings
return clause.name !== undefined
|| bindings === undefined
|| ts.isNamespaceImport(bindings)
|| bindings.elements.length === 0
|| bindings.elements.some(element => !element.isTypeOnly)
}
function exportCarriesRuntimeValue(node: ts.ExportDeclaration): boolean {
if (node.isTypeOnly) return false
const clause = node.exportClause
if (clause === undefined || ts.isNamespaceExport(clause)) return true
return clause.elements.length === 0 || clause.elements.some(element => !element.isTypeOnly)
}
export function collectSourceFileUses(
sourceFile: ts.SourceFile,
runtimeOnly: boolean,
key: 'local' | 'package' | 'specifier',
includeRootRelative = false,
): Set<string> {
const uses = new Set<string>()
const add = (specifier: ts.Expression | undefined): void => {
if (specifier === undefined || !ts.isStringLiteralLike(specifier)) return
if (key === 'local') {
if (specifier.text.startsWith('.') || includeRootRelative && specifier.text.startsWith('/')) uses.add(specifier.text)
return
}
if (!isBareSpecifier(specifier.text)) return
uses.add(key === 'package' ? packageNameForUses(specifier.text) : specifier.text)
}
const visit = (node: ts.Node): void => {
if (ts.isImportDeclaration(node)) {
if (!runtimeOnly || importCarriesRuntimeValue(node)) add(node.moduleSpecifier)
} else if (ts.isExportDeclaration(node)) {
if (!runtimeOnly || exportCarriesRuntimeValue(node)) add(node.moduleSpecifier)
} else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) {
if (!runtimeOnly || !node.isTypeOnly) add(node.moduleReference.expression)
} else if (!runtimeOnly && ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
add(node.argument.literal)
} else if (ts.isCallExpression(node)
&& (node.expression.kind === ts.SyntaxKind.ImportKeyword
|| ts.isIdentifier(node.expression) && node.expression.text === 'require')) {
add(node.arguments[0])
} else if (!runtimeOnly && key !== 'local' && ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) {
add(node.name)
} else if (key !== 'local'
&& (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node))) {
uses.add('react')
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
return uses
}
/**
* Read browser-module declarations from workspace manifests.
* @param root - Absolute repository root.
* @returns Declarations and malformed dsh.client fields.
*/
export function collectSourcePackageUses(path: string, source: string): Set<string> {
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
return collectSourceFileUses(sourceFile, false, 'package')
}
/**
* Collect bare packages whose values one production source file reaches at runtime.
* @param path - File path used to select TypeScript's parser mode.
* @param source - Source text to inspect.
* @returns Bare package names retained by runtime imports, exports, requires, or JSX.
*/
export function collectRuntimeSourcePackageUses(path: string, source: string): Set<string> {
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
return collectSourceFileUses(sourceFile, true, 'package')
}
/**
* Collect exact bare specifiers retained by one production source file.
* @param path - File path used to select TypeScript's parser mode.
* @param source - Source text to inspect.
* @returns Exact specifiers retained by runtime imports, exports, requires, or JSX.
*/
export function collectRuntimeSourceSpecifiers(path: string, source: string): Set<string> {
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
return collectSourceFileUses(sourceFile, true, 'specifier')
}
/**
* Collect relative module specifiers used to follow one source entry's local closure.
* @param path - File path used to select TypeScript's parser mode.
* @param source - Source text to inspect.
* @returns Relative imports, exports, requires, and import types.
*/
export function collectLocalSourceSpecifiers(path: string, source: string): Set<string> {
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
return collectSourceFileUses(sourceFile, false, 'local')
}
/**
* Collect local module specifiers retained by one production source file.
* @param path - File path used to select TypeScript's parser mode.
* @param source - Source text to inspect.
* @param includeRootRelative - Include absolute paths, such as Vite's Web-root imports.
* @returns Local imports, exports, and requires that survive compilation.
*/
export function collectRuntimeLocalSourceSpecifiers(path: string, source: string, includeRootRelative = false): Set<string> {
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
return collectSourceFileUses(sourceFile, true, 'local', includeRootRelative)
}
/**
* Collect package-asset module specifiers retained by one production source file.
* @param path - File path used to select TypeScript's parser mode.
* @param source - Source text to inspect.
* @returns Relative runtime imports, re-exports, and requires whose extension has a package-asset media type.
*/
/**
* Normalize a repository path to forward slashes.
* @param path - platform path inside the repository.
* @returns the same path with POSIX separators.
*/
export function normalizePath(path: string): string {
return path.split(sep).join('/')
}
export function readAllSourceUses(root: string, pkg: WorkspacePackageManifest): Map<string, string[]> {
const uses = new Map<string, string[]>()
for (const sourcePath of globSync('src/**/*.{ts,tsx,mts,cts}', { cwd: resolve(root, pkg.dir) }).sort()) {
const source = readFileSync(resolve(root, pkg.dir, sourcePath), 'utf8')
const displayPath = `${pkg.dir}/${normalizePath(sourcePath)}`
let runtimeUses: Set<string> | undefined
for (const specifier of collectSourcePackageUses(sourcePath, source)) {
const name = packageNameOf(specifier)
if (name === undefined) continue
const typesName = `@types/${name.replace(/^@/, '').replace('/', '__')}`
if (!ROOT_TYPE_PACKAGES.has(typesName)
&& declaredSections(pkg.manifest, name).length === 0
&& declaredSections(pkg.manifest, typesName).length > 0) {
runtimeUses ??= collectRuntimeSourcePackageUses(sourcePath, source)
if (!runtimeUses.has(name)) {
addUse(uses, typesName, displayPath)
continue
}
}
addUse(uses, name, displayPath)
}
}
return uses
}
+1 -1
View File
@@ -45,7 +45,7 @@ export async function probeRestrictedWorker(
// The probe program calls no bindings and prints nothing, so the host serves only readiness.
const channel = createProcessRpc<ProgramMethods, Pick<HostMethods, 'ready'>>(control, 1_048_576, {
ready() {
void channel.run(workspace, { code, namespaces: [], maxOutputBytes: 65_536 }).then((result) => {
void channel.run(workspace, { code, retainsState: false, namespaces: [], maxOutputBytes: 65_536 }).then((result) => {
if (result.error !== undefined) completed.reject(new Error(JSON.stringify(result.error)))
else completed.resolve(decodePtcJsonWire(result.value))
channel.$close()
@@ -0,0 +1,26 @@
/** Unit suites cannot reach the official DeepSeek model listing unless they answer `fetch` themselves. */
import { createServer } from 'node:http'
import { afterEach, expect, it } from 'vitest'
import type { Server } from 'node:http'
const servers: Server[] = []
afterEach(async () => {
await Promise.all(servers.splice(0).map(server => new Promise(resolve => server.close(resolve))))
})
it.each([
['string', () => fetch('https://api.deepseek.com/models')],
['URL', () => fetch(new URL('https://api.deepseek.com/models'))],
['Request', () => fetch(new Request('https://api.deepseek.com/models', { headers: { 'x-api-key': 'fixture-key' } }))],
])('rejects the official model listing given as a %s', async (_kind, request) => {
await expect(request()).rejects.toThrow('unit suites do not reach https://api.deepseek.com/models; answer it with a fetch stub')
})
it('passes every other request to the ambient fetch', async () => {
const server = createServer((_request, response) => { response.end('local') })
servers.push(server)
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
const address = server.address()
if (address === null || typeof address === 'string') throw new Error('no port')
await expect((await fetch(`http://127.0.0.1:${address.port}/models`)).text()).resolves.toBe('local')
})
+22
View File
@@ -0,0 +1,22 @@
/**
* Keep unit suites from requesting the official DeepSeek model listing.
*
* The API-key and account routes request `GET https://api.deepseek.com/models` whenever they hold a
* credential for the official endpoint, and suites routinely boot those routes with a dummy key or
* inherit a developer's real one. Such a request would reach production and, on a network that never
* answers, hold the route's first catalog access for its whole listing deadline. Here it fails at once,
* so the route serves its built-in catalog; a suite that exercises the listing replaces `fetch` itself.
* @module
*/
/** The listing endpoint of the official DeepSeek API. */
const OFFICIAL_LISTING = 'https://api.deepseek.com/models'
const ambientFetch = globalThis.fetch
globalThis.fetch = (input, init) => {
const url = input instanceof Request ? input.url : input instanceof URL ? input.href : input
if (url === OFFICIAL_LISTING) {
return Promise.reject(new TypeError(`unit suites do not reach ${OFFICIAL_LISTING}; answer it with a fetch stub`))
}
return ambientFetch(input, init)
}
+4 -2
View File
@@ -139,8 +139,10 @@ for (const [file, jobIds] of [['release.yml', ['dependencies', 'pack']], ['relea
it('retains the dependency and pack verification commands', () => {
const commands = job.steps.flatMap(step => step.run === undefined ? [] : [step.run])
if (jobId === 'dependencies') {
expect(commands).toContain('pnpm run verify-package-dependencies')
expect(commands).toContain('pnpm run verify-npm-install-layout')
// Both verifications share one step so the lane pays the longer half.
const combined = commands.find(command => command.includes('verify-package-dependencies'))
expect(combined).toContain('verify-npm-install-layout')
expect(combined).toContain('wait "$layout_pid"')
} else {
const family = file === 'release.yml' ? 'dsh' : 'vendor'
const output = family === 'dsh' ? 'dist/npm' : 'dist/npm-vendor'
+10
View File
@@ -342,6 +342,11 @@
"symbol": "LegacyScheduleRecord",
"source": "packages/schedule/schedule/src/types.ts"
},
{
"doc": "docs/subsystems/schedule.md",
"symbol": "ScheduleDeleteInactiveResult",
"source": "packages/schedule/schedule/src/types.ts"
},
{
"doc": "docs/subsystems/schedule.md",
"symbol": "LocalAtInput",
@@ -1431,6 +1436,11 @@
"symbol": "PtcJsonValue",
"source": "packages/ptc-runtime/ptc-runtime/src/types.ts"
},
{
"doc": "docs/subsystems/ptc-runtime.md",
"symbol": "PtcProgramFormat",
"source": "packages/ptc-runtime/ptc-runtime/src/types.ts"
},
{
"doc": "docs/subsystems/ptc-runtime.md",
"symbol": "PtcRunRequest",
+8 -127
View File
@@ -15,6 +15,14 @@ import {
} from '../packages/client/modules/src/assets.ts'
import { TypeScriptProject } from './ts-project.ts'
export {
collectLocalSourceSpecifiers,
collectRuntimeLocalSourceSpecifiers,
collectRuntimeSourcePackageUses,
collectRuntimeSourceSpecifiers,
collectSourcePackageUses,
} from './source-parse-uses.ts'
import { collectSourceFileUses } from './source-parse-uses.ts'
const GATE = 'verify-client-packages'
const CLIENT_MANIFEST_GLOB = 'packages/client/*/package.json'
const MANIFEST_GLOBS = ['packages/*/*/package.json', 'apps/*/package.json', 'vendor/*/package.json']
@@ -89,62 +97,6 @@ export interface ClientDeclarations {
* @param source - Source text to inspect.
* @returns Bare package names referenced by imports, declarations, or JSX.
*/
export function collectSourcePackageUses(path: string, source: string): Set<string> {
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
return collectSourceFileUses(sourceFile, false, 'package')
}
/**
* Collect bare packages whose values one production source file reaches at runtime.
* @param path - File path used to select TypeScript's parser mode.
* @param source - Source text to inspect.
* @returns Bare package names retained by runtime imports, exports, requires, or JSX.
*/
export function collectRuntimeSourcePackageUses(path: string, source: string): Set<string> {
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
return collectSourceFileUses(sourceFile, true, 'package')
}
/**
* Collect exact bare specifiers retained by one production source file.
* @param path - File path used to select TypeScript's parser mode.
* @param source - Source text to inspect.
* @returns Exact specifiers retained by runtime imports, exports, requires, or JSX.
*/
export function collectRuntimeSourceSpecifiers(path: string, source: string): Set<string> {
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
return collectSourceFileUses(sourceFile, true, 'specifier')
}
/**
* Collect relative module specifiers used to follow one source entry's local closure.
* @param path - File path used to select TypeScript's parser mode.
* @param source - Source text to inspect.
* @returns Relative imports, exports, requires, and import types.
*/
export function collectLocalSourceSpecifiers(path: string, source: string): Set<string> {
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
return collectSourceFileUses(sourceFile, false, 'local')
}
/**
* Collect local module specifiers retained by one production source file.
* @param path - File path used to select TypeScript's parser mode.
* @param source - Source text to inspect.
* @param includeRootRelative - Include absolute paths, such as Vite's Web-root imports.
* @returns Local imports, exports, and requires that survive compilation.
*/
export function collectRuntimeLocalSourceSpecifiers(path: string, source: string, includeRootRelative = false): Set<string> {
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
return collectSourceFileUses(sourceFile, true, 'local', includeRootRelative)
}
/**
* Collect package-asset module specifiers retained by one production source file.
* @param path - File path used to select TypeScript's parser mode.
* @param source - Source text to inspect.
* @returns Relative runtime imports, re-exports, and requires whose extension has a package-asset media type.
*/
export function collectSourceAssetSpecifiers(path: string, source: string): Set<string> {
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
return new Set([...collectSourceFileUses(sourceFile, true, 'local')].filter(isAssetSpecifier))
@@ -227,73 +179,6 @@ export function readStylesheetAssetReferences(
}
}
function importCarriesRuntimeValue(node: ts.ImportDeclaration): boolean {
const clause = node.importClause
if (clause === undefined) return true
if (clause.phaseModifier === ts.SyntaxKind.TypeKeyword) return false
const bindings = clause.namedBindings
return clause.name !== undefined
|| bindings === undefined
|| ts.isNamespaceImport(bindings)
|| bindings.elements.length === 0
|| bindings.elements.some(element => !element.isTypeOnly)
}
function exportCarriesRuntimeValue(node: ts.ExportDeclaration): boolean {
if (node.isTypeOnly) return false
const clause = node.exportClause
if (clause === undefined || ts.isNamespaceExport(clause)) return true
return clause.elements.length === 0 || clause.elements.some(element => !element.isTypeOnly)
}
function collectSourceFileUses(
sourceFile: ts.SourceFile,
runtimeOnly: boolean,
key: 'local' | 'package' | 'specifier',
includeRootRelative = false,
): Set<string> {
const uses = new Set<string>()
const add = (specifier: ts.Expression | undefined): void => {
if (specifier === undefined || !ts.isStringLiteralLike(specifier)) return
if (key === 'local') {
if (specifier.text.startsWith('.') || includeRootRelative && specifier.text.startsWith('/')) uses.add(specifier.text)
return
}
if (!isBareSpecifier(specifier.text)) return
uses.add(key === 'package' ? packageNameOf(specifier.text) : specifier.text)
}
const visit = (node: ts.Node): void => {
if (ts.isImportDeclaration(node)) {
if (!runtimeOnly || importCarriesRuntimeValue(node)) add(node.moduleSpecifier)
} else if (ts.isExportDeclaration(node)) {
if (!runtimeOnly || exportCarriesRuntimeValue(node)) add(node.moduleSpecifier)
} else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) {
if (!runtimeOnly || !node.isTypeOnly) add(node.moduleReference.expression)
} else if (!runtimeOnly && ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
add(node.argument.literal)
} else if (ts.isCallExpression(node)
&& (node.expression.kind === ts.SyntaxKind.ImportKeyword
|| ts.isIdentifier(node.expression) && node.expression.text === 'require')) {
add(node.arguments[0])
} else if (!runtimeOnly && key !== 'local' && ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) {
add(node.name)
} else if (key !== 'local'
&& (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node))) {
uses.add('react')
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
return uses
}
/**
* Read browser-module declarations from workspace manifests.
* @param root - Absolute repository root.
* @returns Declarations and malformed dsh.client fields.
*/
export function readClientDeclarations(root: string): ClientDeclarations {
const malformed: string[] = []
const declarations = globSync(MANIFEST_GLOBS, { cwd: root })
@@ -853,10 +738,6 @@ function rowPackageOf(specifier: string, rows: ReadonlySet<string>): string | un
return rows.has(stripped) ? stripped : undefined
}
function isBareSpecifier(specifier: string): boolean {
return !specifier.startsWith('.') && !specifier.startsWith('/') && !specifier.startsWith('#')
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
+1 -1
View File
@@ -50,7 +50,7 @@ export function findConcreteTermViolations(file: string, source: string): Concre
}
function trackedFiles(repoRoot: string): string[] {
const files = execFileSync('git', ['ls-files', '-z'], { cwd: repoRoot, encoding: 'utf8' })
const files = execFileSync('git', ['ls-files', '-z'], { cwd: repoRoot, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 })
.split('\0')
.filter(file => file !== '')
if (!files.includes('AGENTS.md')
@@ -13,7 +13,7 @@ const roots: string[] = []
const experimental = '@deepseek-ai/dsh-experimental-prototype'
const core = '@deepseek-ai/dsh-core'
const base = '@deepseek-ai/dsh-base'
const profile = 'packages/boot/official-bundles/src/bundles.ts'
const profile = 'packages/util/official-bundles/src/bundles.ts'
const preset = 'packages/bundle/web-app/presets/standard.patch.yml'
const patch = 'packages/bundle/base/cordis.patch.yml'
+1 -1
View File
@@ -23,7 +23,7 @@ import {
import { isExperimentalPackageName } from './experimental-package-policy.ts'
// The independently published entry package owns platform-engine dependencies.
const EXTERNAL_KIT_PACKAGES = new Set(['@deepseek-ai/libreoffice-kit'])
const PROFILE_SOURCE = 'packages/boot/official-bundles/src/bundles.ts'
const PROFILE_SOURCE = 'packages/util/official-bundles/src/bundles.ts'
const PRESET_PATTERN = 'packages/bundle/web-app/presets/*.patch.yml'
const RUNTIME_SECTIONS = ['dependencies', 'optionalDependencies', 'peerDependencies'] as const
+38 -4
View File
@@ -1,10 +1,12 @@
/** Metadata coverage rejects missing translations through the same checker used by CI. */
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, expect, it } from 'vitest'
import { localeMetadataProblems } from './verify-locale-metadata.ts'
import { SHIPPED_LOCALE_IDS } from './shipped-locales.ts'
const roots: string[] = []
afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) })
@@ -16,6 +18,9 @@ function fixture(ja: unknown = { meta: { title: '名前 {name}', description: '
writeFileSync(join(directory, 'en.json'), JSON.stringify({ meta: { title: 'Name {name}' } }))
writeFileSync(join(directory, 'zh.json'), JSON.stringify({ meta: { title: '名称 {name}', description: '描述' } }))
writeFileSync(join(directory, 'ja.json'), JSON.stringify(ja))
for (const language of ['es', 'fr', 'de', 'ru', 'pt']) {
writeFileSync(join(directory, `${language}.json`), JSON.stringify({ meta: { title: 'Name {name}', description: 'Localized description.' } }))
}
return root
}
@@ -41,8 +46,8 @@ it.each([
it('checks referenced detail documents', () => {
const root = fixture()
const directory = join(root, 'packages/demo/plugin/locale')
const meta = { title: 'Name {name}', description: 'Description.', detail: 'docs/DETAIL.md' }
for (const language of ['en', 'zh', 'ja']) writeFileSync(join(directory, `${language}.json`), JSON.stringify({ meta }))
const meta = { title: 'Name {name}', detail: 'docs/DETAIL.md' }
for (const language of SHIPPED_LOCALE_IDS) writeFileSync(join(directory, `${language}.json`), JSON.stringify({ meta }))
expect(localeMetadataProblems(root).join('\n')).toContain('missing detail resource')
mkdirSync(join(directory, '../docs'))
writeFileSync(join(directory, '../docs/DETAIL.md'), '# Detail\n')
@@ -84,6 +89,7 @@ it('checks nested subpath locale resources and package-relative detail files', (
expect(localeMetadataProblems(root).join('\n')).toContain('locale/tools')
expect(localeMetadataProblems(root).join('\n')).toContain('ja.json')
writeFileSync(join(directory, 'ja.json'), JSON.stringify({ meta: { title: 'ツール', detail: 'docs/TOOLS.md' } }))
for (const language of ['es', 'fr', 'de', 'ru', 'pt']) writeFileSync(join(directory, `${language}.json`), JSON.stringify({ meta: { title: 'Tools', detail: 'docs/TOOLS.md' } }))
expect(localeMetadataProblems(root)).toEqual([])
})
@@ -97,7 +103,7 @@ it('requires sentence punctuation for English descriptions', () => {
const file = join(root, 'packages/demo/plugin/locale/en.json')
writeFileSync(file, JSON.stringify({ meta: { title: 'Name {name}', description: 'Description' } }))
expect(localeMetadataProblems(root).join('\n')).toContain('sentence punctuation')
writeFileSync(file, JSON.stringify({ meta: { title: 'Name {name}', description: 'Description.' } }))
writeFileSync(file, JSON.stringify({ meta: { title: 'Name {name}', description: 'English description.' } }))
expect(localeMetadataProblems(root)).toEqual([])
})
@@ -108,6 +114,7 @@ it('permits missing Chinese resources for loadable plugins', () => {
writeFileSync(join(directory, 'src/index.ts'), 'export function apply() {}')
writeFileSync(join(directory, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh-demo' }))
rmSync(join(directory, 'locale/zh.json'))
for (const language of ['es', 'fr', 'de', 'ru', 'pt']) writeFileSync(join(directory, `locale/${language}.json`), JSON.stringify({ meta: { title: 'Name {name}' } }))
expect(localeMetadataProblems(root)).toEqual([])
})
@@ -119,3 +126,30 @@ it('excludes packages formally classified as test helpers', () => {
writeFileSync(join(directory, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh-llm-replay' }))
expect(localeMetadataProblems(root)).toEqual([])
})
it.each(['es', 'fr', 'de', 'ru', 'pt'])('rejects missing %s metadata', (language) => {
const root = fixture()
rmSync(join(root, `packages/demo/plugin/locale/${language}.json`))
expect(localeMetadataProblems(root).join('\n')).toContain(`${language}.json`)
})
it.each(['es', 'fr', 'de', 'ru', 'pt'])('rejects untranslated Chinese descriptions in %s metadata', (language) => {
const root = fixture()
writeFileSync(join(root, 'packages/demo/plugin/locale/en.json'), JSON.stringify({ meta: { title: 'Name {name}', description: 'English description.' } }))
writeFileSync(join(root, `packages/demo/plugin/locale/${language}.json`), JSON.stringify({ meta: { title: 'Name {name}', description: '描述' } }))
expect(localeMetadataProblems(root).join('\n')).toContain(`${language}.json: meta.description contains untranslated source text`)
})
it.each(['es', 'fr', 'de', 'ru', 'pt'])('rejects copied English descriptions in %s metadata', (language) => {
const root = fixture()
writeFileSync(join(root, 'packages/demo/plugin/locale/en.json'), JSON.stringify({ meta: { title: 'Name {name}', description: 'English description.' } }))
writeFileSync(join(root, `packages/demo/plugin/locale/${language}.json`), JSON.stringify({ meta: { title: 'Name {name}', description: 'English description.' } }))
expect(localeMetadataProblems(root).join('\n')).toContain(`${language}.json: meta.description contains untranslated source text`)
})
it.each(['\u200b', '\ufeff', String.raw`\u200b`, String.raw`\ufeff`])('rejects invisible characters in JSON resources', (character) => {
const root = fixture()
const file = join(root, 'packages/demo/plugin/locale/es.json')
writeFileSync(file, readFileSync(file, 'utf8').replace('Localized description.', `Localized${character} description.`))
expect(localeMetadataProblems(root).join('\n')).toContain('invisible locale character')
})
+21 -12
View File
@@ -1,8 +1,9 @@
/** Check Japanese coverage of package metadata and plugin templates without loading plugins. */
/** Check shipped-language coverage of package metadata and plugin templates without loading plugins. */
import { existsSync, globSync, readFileSync } from 'node:fs'
import { basename, posix, join, resolve } from 'node:path'
import { detailPath } from '../packages/util/plugin-detail/src/document.ts'
import { PRODUCT_PACKAGE_POLICY } from './product-package-policy.ts'
import { SHIPPED_LOCALE_IDS, localeTokens, hasInvisibleLocaleCharacters } from './shipped-locales.ts'
import { collectConfigCatalog } from './gen-config-catalog.ts'
/** Flatten metadata strings and reject values that cannot be translated. */
@@ -13,14 +14,14 @@ function strings(value: unknown, key = ''): Record<string, string> {
}
/**
* Compare Japanese metadata with every field supplied by English or Chinese.
* Compare shipped-language metadata with every field supplied by English or Chinese.
* @param root - repository or fixture root.
* @returns actionable missing-file, field, placeholder and resource diagnostics.
*/
export function localeMetadataProblems(root: string): string[] {
const directories = new Set(globSync('packages/**/locale/**/*.json', {
cwd: root, exclude: ['packages/**/node_modules/**', 'packages/**/lib/**', 'packages/**/tests/**'],
}).map(file => file.replaceAll('\\', '/')).filter(file => ['en.json', 'zh.json', 'ja.json'].includes(basename(file))).map(file => posix.dirname(file)))
}).map(file => file.replaceAll('\\', '/')).filter(file => SHIPPED_LOCALE_IDS.some(language => basename(file) === `${language}.json`)).map(file => posix.dirname(file)))
for (const entry of collectConfigCatalog(root)) {
const dir = entry.dir.replaceAll('\\', '/')
if (PRODUCT_PACKAGE_POLICY[dir]?.category === 'test') continue
@@ -34,7 +35,9 @@ export function localeMetadataProblems(root: string): string[] {
try {
const read = (language: string): Record<string, string> => {
const file = join(root, directory, `${language}.json`)
const value: unknown = JSON.parse(readFileSync(file, 'utf8'))
const source = readFileSync(file, 'utf8')
if (hasInvisibleLocaleCharacters(source) || /\\u(?:200b|feff)/iu.test(source)) throw new Error(`${language}.json: invisible locale character`)
const value: unknown = JSON.parse(source)
const dictionary = strings(value)
const detail = detailPath(dictionary['meta.detail'], `${directory}/${language}.json: meta.detail`)
const packageDir = directory.slice(0, directory.lastIndexOf('/locale'))
@@ -47,14 +50,20 @@ export function localeMetadataProblems(root: string): string[] {
const zh = existsSync(join(root, directory, 'zh.json')) ? read('zh') : {}
const description = en['meta.description']
if (description !== undefined && !/[.!?]$/u.test(description)) problems.push(`${directory}/en.json: meta.description must end with sentence punctuation`)
const ja = read('ja')
const expected = { ...zh, ...en }
for (const key of new Set([...Object.keys(expected), ...Object.keys(ja)])) {
if (ja[key] === undefined) problems.push(`${directory}/ja.json: missing ${key}`)
else if (expected[key] === undefined) problems.push(`${directory}/ja.json: unexpected ${key}`)
else {
const placeholders = (text: string): string => (text.match(/\{\w+\}/gu) ?? []).sort().join(',')
if (placeholders(ja[key]) !== placeholders(expected[key])) problems.push(`${directory}/ja.json: ${key} changes placeholders`)
for (const language of SHIPPED_LOCALE_IDS.filter(id => id !== 'en' && id !== 'zh')) {
const translated = read(language)
for (const key of new Set([...Object.keys(expected), ...Object.keys(translated)])) {
if (translated[key] === undefined) problems.push(`${directory}/${language}.json: missing ${key}`)
else if (expected[key] === undefined) problems.push(`${directory}/${language}.json: unexpected ${key}`)
else {
if (language !== 'ja' && key === 'meta.description' && ((zh[key] !== undefined && /\p{Script=Han}/u.test(zh[key]) && translated[key] === zh[key])
|| (en[key] !== undefined && translated[key] === en[key]))) {
problems.push(`${directory}/${language}.json: ${key} contains untranslated source text`)
}
const placeholders = (text: string): string => localeTokens(text).join(',')
if (placeholders(translated[key]) !== placeholders(expected[key])) problems.push(`${directory}/${language}.json: ${key} changes placeholders or literal tokens`)
}
}
}
} catch (error) {
@@ -68,5 +77,5 @@ if (import.meta.filename === resolve(process.argv[1] ?? '')) {
const problems = localeMetadataProblems(resolve(import.meta.dirname, '..'))
for (const problem of problems) console.error(problem)
if (problems.length > 0) process.exitCode = 1
else console.log('verify-locale-metadata: all package and template metadata has complete Japanese translations.')
else console.log('verify-locale-metadata: all package and template metadata has complete shipped-language translations.')
}
+1 -1
View File
@@ -139,6 +139,6 @@ describe('resolution work budget', () => {
expect(assertResolutionWorkBudget({ dshPackagesPerVersion: 300, checkedDshEdges: 2800 }))
.toBe(840_000)
expect(() => assertResolutionWorkBudget({ dshPackagesPerVersion: 400, checkedDshEdges: 3000 }))
.toThrow('400 package(s) per release x 3000 internal edge(s) = 1200000 unit(s), budget 875000 unit(s)')
.toThrow(`400 package(s) per release x 3000 internal edge(s) = 1200000 unit(s), budget ${String(MAX_RESOLUTION_WORK_UNITS)} unit(s)`)
})
})
+1 -1
View File
@@ -30,7 +30,7 @@ const DEPENDENCY_FIELDS = ['dependencies', 'optionalDependencies', 'peerDependen
* measured counts instead of failing on whichever runner happens to be slow.
* Raising it requires re-measuring the resolution and recording the new counts.
*/
export const MAX_RESOLUTION_WORK_UNITS = 875_000
export const MAX_RESOLUTION_WORK_UNITS = 880_950
/**
* Measured npm seconds per resolution work unit on the reference host: 180.66 s
+137 -74
View File
@@ -5,9 +5,10 @@
import { spawnSync } from 'node:child_process'
import { existsSync, globSync, readFileSync, writeFileSync } from 'node:fs'
import { isBuiltin } from 'node:module'
import { availableParallelism } from 'node:os'
import { dirname, extname, join, normalize, relative, resolve, sep } from 'node:path'
import { pathToFileURL } from 'node:url'
import { Worker } from 'node:worker_threads'
import ts from 'typescript'
import { WorkspaceTypertGenerator } from '../packages/typert/generator/src/workspace.ts'
import { PACKAGE_ASSET_EXPORT } from '../packages/client/modules/src/assets.ts'
@@ -17,11 +18,22 @@ import {
PACKAGE_DEPENDENCY_POLICY,
type PackageDependencyPolicy,
} from './package-dependency-policy.ts'
import type {
SourceUsesWorkerRequest,
SourceUsesWorkerResult,
} from './package-source-uses.worker.ts'
import {
ROOT_TYPE_PACKAGES,
addUse,
normalizePath,
collectRuntimeLocalSourceSpecifiers,
collectRuntimeSourcePackageUses,
collectSourcePackageUses,
} from './verify-client-packages.ts'
declaredSections,
mutableSection,
packageNameOf,
readAllSourceUses,
section,
type DependencySection,
} from './source-parse-uses.ts'
const GATE = 'verify-package-dependencies'
const CORDIS = '@deepseek-ai/cordis'
@@ -29,7 +41,6 @@ const CORDIS = '@deepseek-ai/cordis'
* Type packages declared only in the root devDependencies. Each package-local pnpm link is one more
* location that TypeScript scans when it computes module specifiers for every JSX file.
*/
const ROOT_TYPE_PACKAGES: ReadonlySet<string> = new Set(['@types/react', '@types/react-dom'])
function workspaceRange(name: string): 'workspace:*' | 'workspace:~' {
return name === '@deepseek-ai/dsh' || name.startsWith('@deepseek-ai/dsh-') ? 'workspace:*' : 'workspace:~'
}
@@ -42,9 +53,15 @@ const WORKSPACE_MANIFEST_GLOBS = [
'vendor/*/package.json',
]
type DependencySection = 'dependencies' | 'devDependencies' | 'optionalDependencies' | 'peerDependencies'
export type PackageDependencyRole = 'client-only' | 'client-host' | 'configured-host'
/** One workspace package the dependency policy covers, with its resolved role. */
export type SelectedWorkspacePackage = WorkspacePackageManifest & { readonly role: PackageDependencyRole }
/** Upper bound on source-walk workers: each one pays its own TypeScript module load. */
export const MAX_DEPENDENCY_WORKERS = 16
/** Manifest fields read and repaired by the package dependency policy. */
export interface PackageDependencyManifest {
name?: string
@@ -104,24 +121,6 @@ export interface ExpectedPackageDependency {
readonly origins: readonly string[]
}
function normalizePath(path: string): string {
return path.split(sep).join('/')
}
function packageNameOf(specifier: string): string | undefined {
if (isBuiltin(specifier) || specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('#')
|| specifier.includes(':') || specifier.includes('*')) {
return undefined
}
const parts = specifier.split('/')
return specifier.startsWith('@') ? parts.length >= 2 ? `${parts[0]}/${parts[1]}` : undefined : parts[0]
}
/**
* Read package manifests used for scope discovery and workspace-name checks.
* @param root - Repository containing the workspace manifests.
* @returns Every workspace package, the non-experimental release packages, and the packages the policy covers.
*/
export function readWorkspacePackageManifests(root: string): {
all: WorkspacePackageManifest[]
release: WorkspacePackageManifest[]
@@ -211,12 +210,6 @@ export function discoverPackageDependencyScope(
}
}
function addUse(target: Map<string, string[]>, name: string, path: string): void {
const paths = target.get(name) ?? []
if (!paths.includes(path)) paths.push(path)
target.set(name, paths)
}
const NAMESPACE_RUNTIME_EXPORT = '*'
const SIDE_EFFECT_RUNTIME_EXPORT = '(side effect)'
@@ -519,31 +512,6 @@ function readHostRuntimeUses(
}
}
function readAllSourceUses(root: string, pkg: WorkspacePackageManifest): Map<string, string[]> {
const uses = new Map<string, string[]>()
for (const sourcePath of globSync('src/**/*.{ts,tsx,mts,cts}', { cwd: resolve(root, pkg.dir) }).sort()) {
const source = readFileSync(resolve(root, pkg.dir, sourcePath), 'utf8')
const displayPath = `${pkg.dir}/${normalizePath(sourcePath)}`
let runtimeUses: Set<string> | undefined
for (const specifier of collectSourcePackageUses(sourcePath, source)) {
const name = packageNameOf(specifier)
if (name === undefined) continue
const typesName = `@types/${name.replace(/^@/, '').replace('/', '__')}`
if (!ROOT_TYPE_PACKAGES.has(typesName)
&& declaredSections(pkg.manifest, name).length === 0
&& declaredSections(pkg.manifest, typesName).length > 0) {
runtimeUses ??= collectRuntimeSourcePackageUses(sourcePath, source)
if (!runtimeUses.has(name)) {
addUse(uses, typesName, displayPath)
continue
}
}
addUse(uses, name, displayPath)
}
}
return uses
}
/**
* Read authored and generated source usage for one already-classified package.
* @param root - Repository containing source files and face tsconfigs.
@@ -562,13 +530,14 @@ export async function readPackageDependencyFacts(
workspaceNames: ReadonlySet<string>,
policy: PackageDependencyPolicy = PACKAGE_DEPENDENCY_POLICY,
generatedHostSource?: string,
precomputedSourceUses?: Map<string, string[]>,
): Promise<PackageDependencyFacts> {
const inject = pkg.manifest.dsh?.client?.inject ?? []
const hostRuntime = role === 'client-only'
? { packageUses: new Map<string, string[]>(), exportUses: [] }
: readHostRuntimeUses(root, pkg, await readHostBuildEntries(root, pkg), generatedHostSource)
const servicePeers = policy.requiredServicePeers?.[pkg.name] ?? []
const allSourceUses = readAllSourceUses(root, pkg)
const allSourceUses = precomputedSourceUses ?? readAllSourceUses(root, pkg)
for (const name of servicePeers) {
if (!workspaceNames.has(name) || !allSourceUses.has(name)) {
throw new Error(`${pkg.manifestPath}: requiredServicePeers must name a referenced workspace package: ${name}`)
@@ -676,10 +645,7 @@ export async function readPackageDependencyState(
const workspaceNames = new Set(packages.all.map(pkg => pkg.name))
const discovered = discoverPackageDependencyScope(packages.policy, policy)
const generated = generatedHostSources(root, discovered.selected.filter(pkg => pkg.role !== 'client-only'))
const facts: PackageDependencyFacts[] = []
for (const pkg of discovered.selected) {
facts.push(await readPackageDependencyFacts(root, pkg, pkg.role, workspaceNames, policy, generated.get(pkg.name)))
}
const facts = await readPackageDependencyFactsConcurrently(root, discovered.selected, workspaceNames, policy, generated)
const selectedNames = new Set(facts.map(fact => fact.manifest.name))
return {
facts,
@@ -696,6 +662,117 @@ export async function readPackageDependencyState(
}
}
/**
* Read dependency facts for every selected package, walking package sources in parallel.
*
* Walking a package's `src` tree with the TypeScript parser is the expensive part of
* this gate and shares no state across packages, so those walks run in worker threads
* while the caller's package order is preserved for the policy checks.
* @param root - Repository containing the workspace sources and build configs.
* @param packages - Selected packages, in policy order.
* @param workspaceNames - Identities of every workspace package.
* @param policy - Reviewed scope overrides and Host export classifications.
* @param generated - Host modules the batched Typert pass emitted in memory, keyed by package name.
* @param workers - Worker count for the source walk; defaults to the host CPU count, capped at {@link MAX_DEPENDENCY_WORKERS}.
* @returns Facts in the same order as `packages`.
*/
export async function readPackageDependencyFactsConcurrently(
root: string,
packages: readonly SelectedWorkspacePackage[],
workspaceNames: ReadonlySet<string>,
policy: PackageDependencyPolicy,
generated: ReadonlyMap<string, string>,
workers = Math.min(availableParallelism(), packages.length, MAX_DEPENDENCY_WORKERS),
): Promise<PackageDependencyFacts[]> {
const sourceUses = await readSourceUses(root, packages, workers)
const facts: PackageDependencyFacts[] = []
for (const pkg of packages) {
facts.push(await readPackageDependencyFacts(
root,
pkg,
pkg.role,
workspaceNames,
policy,
generated.get(pkg.name),
sourceUses.get(pkg.dir),
))
}
return facts
}
/** Source uses per package directory, collected either in-process or by worker threads. */
async function readSourceUses(
root: string,
packages: readonly SelectedWorkspacePackage[],
workers: number,
): Promise<Map<string, Map<string, string[]>>> {
if (workers < 2 || packages.length < 2) {
return new Map(packages.map(pkg => [pkg.dir, readAllSourceUses(root, pkg)]))
}
const sizes = sliceSizes(packages.length, workers)
const tasks = packages.map((pkg, index) => ({ index, dir: pkg.dir, name: pkg.name, manifest: pkg.manifest }))
const slices: typeof tasks[] = []
let offset = 0
for (const size of sizes) {
slices.push(tasks.slice(offset, offset + size))
offset += size
}
const collected = (await Promise.all(slices.map(async slice => runSourceUsesWorker(root, slice)))).flat()
const byName = new Map(collected.map(result => [result.name, result.uses]))
return new Map(packages.map(pkg => [pkg.dir, byName.get(pkg.name) ?? new Map<string, string[]>()]))
}
/**
* Run one slice of packages in a worker thread.
* @param root - Repository containing the workspace sources.
* @param tasks - Packages this worker walks.
* @returns one result per task.
* @throws If the worker crashes or exits before reporting.
*/
function runSourceUsesWorker(root: string, tasks: readonly SourceUsesTaskForWorker[]): Promise<SourceUsesWorkerResult[]> {
const request: SourceUsesWorkerRequest = { root, tasks }
const url = new URL('./package-source-uses.worker.ts', import.meta.url)
return new Promise((resolveResults, rejectResults) => {
const worker = new Worker(url, { workerData: request })
let settled = false
worker.once('message', (results: SourceUsesWorkerResult[]) => {
settled = true
void worker.terminate()
resolveResults(results)
})
worker.once('error', (error) => {
if (settled) return
settled = true
rejectResults(error)
})
worker.once('exit', (code) => {
if (settled) return
settled = true
rejectResults(new Error(`${GATE}: source-uses worker exited with ${String(code)} before reporting`))
})
})
}
interface SourceUsesTaskForWorker {
readonly index: number
readonly dir: string
readonly name: string
readonly manifest: PackageDependencyManifest
}
/**
* Distribute a task count over a worker count as evenly as possible.
* @param tasks - Number of tasks to place.
* @param workers - Number of workers that must stay busy.
* @returns One slice size per non-empty slice.
*/
export function sliceSizes(tasks: number, workers: number): number[] {
const slices = Math.min(tasks, Math.max(1, workers))
const base = Math.floor(tasks / slices)
const remainder = tasks % slices
return Array.from({ length: slices }, (_, index) => base + (index < remainder ? 1 : 0))
}
/** Derive the required npm section for each relationship owned by the policy. */
export function expectedPackageDependencies(
facts: PackageDependencyFacts,
@@ -790,20 +867,6 @@ export function formatPeerRequiredRuntimeDependencies(state: PackageDependencySt
]
}
function section(manifest: PackageDependencyManifest, name: DependencySection): Record<string, string> {
return manifest[name] ?? {}
}
function mutableSection(manifest: PackageDependencyManifest, name: DependencySection): Record<string, string> {
manifest[name] ??= {}
return manifest[name]
}
function declaredSections(manifest: PackageDependencyManifest, name: string): DependencySection[] {
return (['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'] as const)
.filter(sectionName => section(manifest, sectionName)[name] !== undefined)
}
function describeSections(sections: readonly DependencySection[]): string {
return sections.length === 0 ? 'no dependency section' : sections.join(' + ')
}
@@ -33,12 +33,13 @@ const NO_MODEL_EXPERIENCE_SECTION: Readonly<Record<string, string>> = {
'packages/fs/fs-patch': 'The pure library only parses caller-supplied patch text and applies hunks to text; consumers own tools, execution, capture, and model output.',
'packages/util/file-drag': 'The package resolves export names and declares native file operations; it does not construct model input.',
'packages/core/scope': 'The package is a model-agnostic registration and lifecycle primitive; model-facing consumers own any context selection.',
'packages/experimental/liveshare-protocol': 'The package only defines wire frames and identity values that its consumers exchange; it registers nothing model-facing.',
'packages/util/brand': 'The package only constructs plain string values and registers nothing model-facing.',
'packages/util/home-paths': 'The package only resolves harness-owned host paths; model-facing consumers own any rendered use.',
'packages/util/launch-environment': 'The package only resolves host environment values; model-facing consumers own any rendered use.',
'packages/util/workspace-path': 'The package only formats Workspace paths for browser UI; it never constructs model input.',
'packages/util/values': 'The package only validates, snapshots, compares, freezes, or rejects caller-owned values; consumers own every model-facing use.',
'packages/boot/official-bundles': 'The package only lists bundle package names and their display metadata; the loaded plugin tree owns model context.',
'packages/util/official-bundles': 'The package only lists bundle package names and their display metadata; the loaded plugin tree owns model context.',
'packages/util/code-language': 'The package only maps a filename suffix to a syntax-highlighting language id; the read consumer owns the persisted hint.',
}
@@ -57,6 +58,10 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/experimental/speech-to-text-sensevoice': { kind: 'none', reason: 'Local recognition returns transient text without modifying model context.' },
'packages/experimental/client-ui-voice-input': { kind: 'none', reason: 'Inserts reviewable text into the unsent draft without submitting to the Agent.' },
'packages/experimental/plugin-market-bundle': { kind: 'indirect', reason: 'Mounts the pluginMarket service, which lets the Creator preset register the plugin_market tool documented in packages/boot/plugin-market.' },
'packages/experimental/client-ui-action-cards': { kind: 'indirect', reason: 'Card actions go to the Host card service, which owns the logged tool, decisions and status notice.' },
'packages/experimental/client-ui-creation-recommendations': { kind: 'indirect', reason: 'The page card body starts a Creator through the Host page kind; the Host owns its logged task.' },
'packages/experimental/client-ui-plugin-recommendations': { kind: 'indirect', reason: 'The plugin card body enables through the Host plugin kind; the Host card service owns the logged tool, decisions and status notice.' },
'packages/experimental/personalized-recommendations': { kind: 'indirect', reason: 'The bundle composes the card service and both card kinds; their packages own the skills, the logged tool and the status notice.' },
'packages/experimental/voice-input-bundle': { kind: 'none', reason: 'Composes dictation and preparation plugins without adding any model-facing contribution.' },
'packages/telemetry/otel': { kind: 'none', reason: 'The service delivers caller-selected records without changing model context.' },
'packages/host/product-telemetry-otel': { kind: 'none', reason: 'The plugin exports explicit analytics records without contributing model context.' },
@@ -64,6 +69,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/client/ui-sidebar-terminal': { kind: 'none', reason: 'The browser renders user terminal screens without exposing them to the model.' },
'packages/ssh/ssh-helper-runtime': { kind: 'none', reason: 'The private executable carrier registers no model-facing content; its providers and consumers own operation results.' },
'packages/experimental/worktree': { kind: 'indirect', reason: 'The worktree tool renders creation results; the working-directory service owns model-visible directory context.' },
'packages/experimental/client-ui-stats': { kind: 'none', reason: 'The panel reads Session events and renders them in the browser; it registers no tool, prompt section, or model input.' },
'packages/ssh/ssh': { kind: 'none', reason: 'The connection owner transports private provider operations; consumers own all model-facing content.' },
'packages/ssh/fs-ssh': { kind: 'indirect', reason: 'The remote filesystem delegates model rendering to the existing filesystem consumers.' },
'packages/ssh/subprocess-ssh': { kind: 'indirect', reason: 'The remote process provider delegates model rendering to Bash, terminal, LSP and ptc-runtime consumers.' },
+4 -4
View File
@@ -13,7 +13,7 @@ const base = '@deepseek-ai/dsh-base'
const core = '@deepseek-ai/dsh-core'
const candidate = '@deepseek-ai/dsh-candidate'
const candidateDir = 'packages/core/candidate'
const profile = 'packages/boot/official-bundles/src/bundles.ts'
const profile = 'packages/util/official-bundles/src/bundles.ts'
const patch = 'packages/bundle/base/cordis.patch.yml'
function pkg(root: string, directory: string, name: string, fields: Record<string, unknown> = {}): void {
@@ -141,7 +141,7 @@ describe('product package use', () => {
const root = fixture()
pkg(root, candidateDir, candidate)
const children = [{ name: candidate }]
if (!initial) write(root, 'packages/boot/official-bundles/src/included.yml', children)
if (!initial) write(root, 'packages/util/official-bundles/src/included.yml', children)
write(root, patch, [{ insert: [{ name: core }, {
name: 'cordis:include', disabled: true, config: { path: './included.yml', ...(initial ? { initial: children } : {}) },
}] }])
@@ -161,12 +161,12 @@ describe('product package use', () => {
it('applies Include patches before collecting runtime mounts and uses files ahead of initial entries', () => {
const root = fixture()
pkg(root, candidateDir, candidate)
write(root, 'packages/boot/official-bundles/src/included.yml', [{ id: 'child', name: candidate }])
write(root, 'packages/util/official-bundles/src/included.yml', [{ id: 'child', name: candidate }])
write(root, patch, [{ insert: [{ name: core }, { name: 'cordis:include', config: {
path: './included.yml', initial: [{ name: candidate }], patches: [{ id: 'child', disabled: true }],
} }] }])
expect(failures(root)).toContain('no product runtime use')
write(root, 'packages/boot/official-bundles/src/included.yml', [])
write(root, 'packages/util/official-bundles/src/included.yml', [])
expect(failures(root)).toContain('no product runtime use')
})
+1 -1
View File
@@ -14,7 +14,7 @@ import { isAgentPresetEntry, isCordisGroupEntry, loadCordisYaml } from './cordis
import { PRODUCT_PACKAGE_POLICY, type ProductPackagePolicy } from './product-package-policy.ts'
import { collectRuntimeLocalSourceSpecifiers, collectRuntimeSourceSpecifiers } from './verify-client-packages.ts'
const PROFILE_SOURCE = 'packages/boot/official-bundles/src/bundles.ts'
const PROFILE_SOURCE = 'packages/util/official-bundles/src/bundles.ts'
const PROFILES = ['acp', 'web', 'headless', 'sdk', 'sdk-minimal']
const PICKER_SOURCE = 'packages/host/directory-picker-auto/src/index.ts'
const DYNAMIC_MOUNTS: Readonly<Record<string, readonly string[]>> = {
+1 -1
View File
@@ -66,7 +66,7 @@ export function findUnavailableRepositoryReferences(file: string, source: string
}
function trackedFiles(repoRoot: string): string[] {
return execFileSync('git', ['ls-files', '-z'], { cwd: repoRoot, encoding: 'utf8' })
return execFileSync('git', ['ls-files', '-z'], { cwd: repoRoot, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 })
.split('\0')
.filter(file => file !== '')
}
+66
View File
@@ -0,0 +1,66 @@
import { execFileSync } from 'node:child_process'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { expect, it } from 'vitest'
import { prepareWindowsBuildWorkspace, removeWindowsBuildWorkspace } from '../apps/desktop/scripts/windows-build-workspace.mjs'
it('clones the exact clean commit and removes only its run-owned checkout', () => {
const root = realpathSync(mkdtempSync(join(tmpdir(), 'package-workspace-')))
try {
const sourceDirectory = join(root, 'original')
mkdirSync(sourceDirectory)
const git = (...args: string[]) => execFileSync('git', args, { cwd: sourceDirectory, encoding: 'utf8', stdio: 'pipe' }).trim()
git('init')
writeFileSync(join(sourceDirectory, 'source.txt'), 'selected source')
writeFileSync(join(sourceDirectory, '.gitignore'), 'private.env\n')
git('add', '.')
git('-c', 'user.name=Fixture', '-c', '[email protected]', 'commit', '-m', 'fixture')
writeFileSync(join(sourceDirectory, 'private.env'), 'ignored fixture')
const options = { sourceDirectory, buildRoot: join(root, 'builds'), runId: '123', runAttempt: '1', sourceSha: git('rev-parse', 'HEAD') }
expect(() => prepareWindowsBuildWorkspace({ ...options, buildRoot: sourceDirectory })).toThrow('outside existing Git')
expect(() => prepareWindowsBuildWorkspace({ ...options, buildRoot: 'relative' })).toThrow('absolute')
expect(() => prepareWindowsBuildWorkspace({ ...options, runId: '../other' })).toThrow('identity')
expect(() => prepareWindowsBuildWorkspace({ ...options, sourceSha: '0'.repeat(40) })).toThrow('match')
writeFileSync(join(sourceDirectory, 'source.txt'), 'dirty')
expect(() => prepareWindowsBuildWorkspace(options)).toThrow('clean')
writeFileSync(join(sourceDirectory, 'source.txt'), 'selected source')
const output = join(root, 'actions-output')
execFileSync(process.execPath, [resolve(import.meta.dirname, '../apps/desktop/scripts/windows-build-workspace.mjs')], {
cwd: sourceDirectory,
env: {
...process.env,
GITHUB_WORKSPACE: sourceDirectory,
PACKAGE_BUILD_ROOT: options.buildRoot,
GITHUB_RUN_ID: options.runId,
GITHUB_RUN_ATTEMPT: options.runAttempt,
PACKAGE_SOURCE_SHA: options.sourceSha,
GITHUB_OUTPUT: output,
},
stdio: 'pipe',
})
const directory = join(options.buildRoot, 'windows-package-123-1', 'source')
const temporaryDirectory = join(options.buildRoot, 'windows-package-123-1', 'temp')
expect(readFileSync(output, 'utf8')).toBe(`directory=${directory}\ntemporaryDirectory=${temporaryDirectory}\n`)
writeFileSync(join(temporaryDirectory, 'temporary.txt'), 'owned temporary data')
expect(readFileSync(join(directory, 'source.txt'), 'utf8')).toBe('selected source')
expect(existsSync(join(directory, 'private.env'))).toBe(false)
expect(() => prepareWindowsBuildWorkspace(options)).toThrow()
expect(() => { removeWindowsBuildWorkspace({ ...options, sourceSha: '0'.repeat(40) }) }).toThrow('ownership')
expect(existsSync(directory)).toBe(true)
const external = join(root, 'external')
mkdirSync(external)
writeFileSync(join(external, 'keep.txt'), 'keep')
symlinkSync(external, join(directory, 'node_modules'), process.platform === 'win32' ? 'junction' : 'dir')
removeWindowsBuildWorkspace(options)
expect(existsSync(directory)).toBe(false)
expect(existsSync(temporaryDirectory)).toBe(false)
expect(readFileSync(join(external, 'keep.txt'), 'utf8')).toBe('keep')
expect(readFileSync(join(sourceDirectory, 'source.txt'), 'utf8')).toBe('selected source')
const linked = join(root, 'linked')
symlinkSync(external, linked, process.platform === 'win32' ? 'junction' : 'dir')
expect(() => prepareWindowsBuildWorkspace({ ...options, buildRoot: join(linked, 'builds') })).toThrow('links')
} finally {
rmSync(root, { recursive: true, force: true })
}
})
+184 -5
View File
@@ -1,13 +1,13 @@
import { execFileSync } from 'node:child_process'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { resolve } from 'node:path'
import { parse, resolve } from 'node:path'
import yaml from 'js-yaml'
import { expect, it } from 'vitest'
const root = resolve(import.meta.dirname, '..')
const workflow = yaml.load(readFileSync(resolve(root, '.github/workflows/windows-package.yml'), 'utf8')) as {
on: Record<string, { inputs: Record<string, { required?: boolean; default?: string; description?: string }> }>
on: Record<string, { inputs: Record<string, { type?: string; required?: boolean; default?: string; description?: string }> }>
permissions: Record<string, string>
concurrency: { group: string; 'cancel-in-progress': boolean }
jobs: Record<string, {
@@ -19,6 +19,7 @@ const workflow = yaml.load(readFileSync(resolve(root, '.github/workflows/windows
uses?: string
run?: string
if?: string
'working-directory'?: string
env?: Record<string, string>
with?: Record<string, unknown>
}>
@@ -61,12 +62,17 @@ it('routes manual packaging to the dedicated runner with explicit preset and sig
expect(Object.keys(workflow.on)).toEqual(['workflow_dispatch'])
expect(workflow.jobs.package).not.toHaveProperty('name')
expect(job['runs-on']).toBe('dsh-win-package-trial')
expect(JSON.stringify(job.env)).not.toContain('runner.')
expect(job.steps.find(step => step.name === 'Configure persistent pnpm store')?.env?.DSH_DESKTOP_PACKAGE_CACHE_DIR)
.toBe('${{ runner.tool_cache }}/dsh-desktop-package-cache')
expect(workflow.concurrency).toEqual({ group: 'windows-desktop-package', 'cancel-in-progress': false })
expect(workflow.permissions).toEqual({ contents: 'read' })
expect(Object.keys(workflow.on.workflow_dispatch!.inputs).sort()).toEqual([
'build_version', 'preset', 'signing', 'source_ref', 'test_release_id', 'update_channel', 'version_mode',
'build_root', 'build_version', 'cache_namespace', 'compression_level', 'preset', 'signing', 'source_ref', 'test_release_id', 'update_channel', 'version_mode',
])
expect(workflow.on.workflow_dispatch!.inputs.signing!.default).toBe('signed')
expect(workflow.on.workflow_dispatch!.inputs.compression_level).toMatchObject({ type: 'choice', options: ['3', '7'], default: '3', required: true })
expect(job.env.PACKAGE_WINDOWS_COMPRESSION_LEVEL).toBe('${{ inputs.compression_level }}')
expect(workflow.on.workflow_dispatch!.inputs.build_version!.required).toBe(false)
expect(workflow.on.workflow_dispatch!.inputs.version_mode!.default).toBe('default')
expect(workflow.on.workflow_dispatch!.inputs.source_ref!.default).toBe('master')
@@ -101,9 +107,182 @@ it('retains failure diagnostics and uploads only the selected successful install
const uploads = job.steps.filter(step => step.uses?.startsWith('actions/upload-artifact@'))
expect(uploads).toHaveLength(3)
expect(uploads[0]!.if).toBe("always() && steps.checkout.outcome == 'success'")
expect(uploads[0]!.with).toMatchObject({ path: 'apps/desktop/.desktop-build/packaging-runs/', 'include-hidden-files': true })
expect(uploads[0]!.with).toMatchObject({
path: '${{ steps.build_workspace.outputs.directory || github.workspace }}/apps/desktop/.desktop-build/packaging-runs/',
'include-hidden-files': true,
})
for (const mode of ['unsigned', 'signed']) {
const upload = uploads.find(step => step.if === `success() && inputs.signing == '${mode}'`)!
expect(upload.with).toMatchObject({ path: '${{ steps.retained.outputs.directory }}/*', 'if-no-files-found': 'error' })
expect(upload.with).toMatchObject({ path: '${{ steps.retained.outputs.directory }}/*', 'if-no-files-found': 'error', 'compression-level': 0 })
}
})
it('routes all packaging consumers to the selected workspace and removes it only after successful uploads', () => {
expect(workflow.on.workflow_dispatch!.inputs.build_root).toMatchObject({ type: 'string', default: '', required: false })
expect(job.env.PACKAGE_BUILD_ROOT).toBe('${{ inputs.build_root }}')
const selected = job.steps.find(step => step.id === 'build_workspace')!
expect(selected.name).toBe('Select packaging workspace')
expect(selected.env).toEqual({ PACKAGE_SOURCE_SHA: '${{ steps.checkout.outputs.commit }}' })
expect(selected['working-directory']).toBeUndefined()
expect(selected.run).toContain('if (!$env:PACKAGE_BUILD_ROOT)')
expect(selected.run).toContain('"directory=$env:GITHUB_WORKSPACE" >> $env:GITHUB_OUTPUT')
expect(selected.run).toContain('} else {')
expect(selected.run).toContain('node "$env:PACKAGE_CI_HELPERS/windows-build-workspace.mjs"')
expect(selected.run).toContain('exit $LASTEXITCODE')
const index = job.steps.indexOf(selected)
expect(index).toBeGreaterThan(job.steps.findIndex(step => step.uses?.startsWith('actions/setup-node@')))
expect(index).toBeLessThan(job.steps.findIndex(step => step.name === 'Configure persistent pnpm store'))
const consumers = [
'Install locked dependencies',
'Validate preset and resolve build version',
'Check packaging configuration and toolchain',
'Build installer and verify packaged runtime',
'Retain complete package outside the checkout',
]
for (const name of consumers) {
const step = job.steps.find(entry => entry.name === name)!
expect(step['working-directory']).toBe('${{ steps.build_workspace.outputs.directory }}')
expect(job.steps.indexOf(step)).toBeGreaterThan(index)
}
expect(job.steps.filter(step => step['working-directory'] === '${{ steps.build_workspace.outputs.directory }}'))
.toHaveLength(consumers.length)
const store = job.steps.find(step => step.name === 'Configure persistent pnpm store')!
expect(store.run).toContain('[IO.Path]::GetPathRoot($env:GITHUB_WORKSPACE)')
expect(store['working-directory']).toBeUndefined()
const cleanup = job.steps.at(-1)!
expect(cleanup).toMatchObject({
name: 'Remove isolated packaging workspace',
if: "success() && inputs.build_root != ''",
env: { PACKAGE_SOURCE_SHA: '${{ steps.checkout.outputs.commit }}' },
run: 'node "$env:PACKAGE_CI_HELPERS/windows-build-workspace.mjs" --remove',
})
expect(cleanup['working-directory']).toBeUndefined()
for (const upload of job.steps.filter(step => step.uses?.startsWith('actions/upload-artifact@'))) {
expect(job.steps.indexOf(upload)).toBeLessThan(job.steps.indexOf(cleanup))
}
})
it('normalizes priority in the dependency and packaging shells before launching their children', () => {
const command = '& "$env:PACKAGE_CI_HELPERS/windows-packaging-priority.ps1" -BestEffort'
expect(job.steps.find(entry => entry.name === 'Validate selected source and adapter support')!.run)
.not.toContain('windows-packaging-priority.ps1')
for (const name of ['Install locked dependencies', 'Build installer and verify packaged runtime']) {
const step = job.steps.find(entry => entry.name === name)!
expect(step.run!.trimStart().startsWith(command + '\n')).toBe(true)
}
})
it('scopes the optional temporary directory to installer building without changing Actions or later steps', () => {
const build = job.steps.find(step => step.name === 'Build installer and verify packaged runtime')!
expect(build.env?.PACKAGE_BUILD_TEMP).toBe('${{ steps.build_workspace.outputs.temporaryDirectory }}')
expect(build.run).toContain([
'if ($env:PACKAGE_BUILD_TEMP) {',
' $env:TEMP = $env:PACKAGE_BUILD_TEMP',
' $env:TMP = $env:PACKAGE_BUILD_TEMP',
'}',
].join('\n'))
expect(build.run!.indexOf('if ($env:PACKAGE_BUILD_TEMP)')).toBeLessThan(build.run!.indexOf('$packageArgs ='))
expect(build.run).not.toContain('GITHUB_ENV')
expect(build.run).not.toContain('RUNNER_TEMP')
expect(job.env).not.toHaveProperty('TEMP')
expect(job.env).not.toHaveProperty('TMP')
expect(job.env).not.toHaveProperty('PACKAGE_BUILD_TEMP')
for (const step of job.steps.filter(step => step !== build)) {
expect(step.env ?? {}).not.toHaveProperty('PACKAGE_BUILD_TEMP')
expect(step.env ?? {}).not.toHaveProperty('TEMP')
expect(step.env ?? {}).not.toHaveProperty('TMP')
expect(step.run ?? '').not.toMatch(/\$env:(?:TEMP|TMP)\s*=/u)
}
const selected = job.steps.find(step => step.id === 'build_workspace')!
const defaultBranch = selected.run!.split('} else {')[0]!
expect(defaultBranch).toContain('"directory=$env:GITHUB_WORKSPACE" >> $env:GITHUB_OUTPUT')
expect(defaultBranch).not.toContain('temporaryDirectory')
})
it.skipIf(process.platform !== 'win32')('keeps the dependency store stable across run-specific pnpm homes', () => {
const directory = mkdtempSync(resolve(tmpdir(), 'windows-package-store-'))
try {
const workspace = resolve(directory, 'runner', 'repo')
const packageCache = resolve(directory, 'tool-cache', 'desktop')
const store = resolve(parse(directory).root, '.pnpm-store')
const step = job.steps.find(entry => entry.name === 'Configure persistent pnpm store')!
expect(job.steps.indexOf(step)).toBeLessThan(job.steps.findIndex(entry => entry.name === 'Install locked dependencies'))
const script = resolve(directory, 'store.ps1')
writeFileSync(script, "$ErrorActionPreference = 'Stop'\n" + step.run!)
const outputs = ['first', 'second'].map((run) => {
const environmentFile = resolve(directory, run + '.env')
execFileSync('pwsh', ['-NoProfile', '-File', script], {
env: { ...process.env, GITHUB_WORKSPACE: workspace,
PACKAGE_CACHE_NAMESPACE: '', DSH_DESKTOP_PACKAGE_CACHE_DIR: packageCache,
PNPM_HOME: resolve(directory, run, 'pnpm'), GITHUB_ENV: environmentFile },
stdio: 'pipe',
})
return readFileSync(environmentFile, 'utf8').trim()
})
expect(outputs.map(output => output.split(/\r?\n/u))).toEqual(Array.from({ length: 2 }, () => [
`DSH_DESKTOP_PACKAGE_CACHE_DIR=${packageCache}`, `PNPM_CONFIG_STORE_DIR=${store}`,
]))
const runNamespace = (namespace: string) => {
const output = resolve(directory, 'namespace.env')
rmSync(output, { force: true })
execFileSync('pwsh', ['-NoProfile', '-File', script], {
env: { ...process.env, GITHUB_WORKSPACE: workspace,
PACKAGE_CACHE_NAMESPACE: namespace, DSH_DESKTOP_PACKAGE_CACHE_DIR: packageCache, GITHUB_ENV: output },
stdio: 'pipe',
})
return readFileSync(output, 'utf8').trim().split(/\r?\n/u)
}
expect(runNamespace('perf-1')).toEqual([
`DSH_DESKTOP_PACKAGE_CACHE_DIR=${resolve(packageCache, 'perf-1')}`, `PNPM_CONFIG_STORE_DIR=${resolve(store, 'perf-1')}`,
])
for (const invalid of ['../escape', 'Upper', 'has space', 'con', 'perf\n', 'a'.repeat(65)]) {
expect(() => runNamespace(invalid)).toThrow()
}
} finally {
rmSync(directory, { recursive: true, force: true, maxRetries: 3 })
}
})
it('pins environment helpers to the workflow commit before selecting product source', () => {
const helper = job.steps.find(step => step.name === 'Checkout workflow helpers')!
const stage = job.steps.find(step => step.name === 'Stage workflow helpers')!
const source = job.steps.find(step => step.id === 'checkout')!
expect(helper.with).toMatchObject({ ref: '${{ github.workflow_sha }}', 'persist-credentials': false })
expect(job.steps.indexOf(helper)).toBeLessThan(job.steps.indexOf(stage))
expect(job.steps.indexOf(stage)).toBeLessThan(job.steps.indexOf(source))
expect(stage.run).toContain('RUNNER_TEMP')
expect(stage.run).toContain('windows-build-workspace.mjs')
expect(stage.run).toContain('windows-packaging-priority.ps1')
for (const step of job.steps.filter(step => step.name !== 'Stage workflow helpers')) {
expect(step.run ?? '').not.toContain('./apps/desktop/scripts/windows-packaging-priority.ps1')
expect(step.run ?? '').not.toContain('node apps/desktop/scripts/windows-build-workspace.mjs')
}
})
it.skipIf(process.platform !== 'win32')('stages helpers that remain usable when product source lacks them', () => {
const directory = mkdtempSync(resolve(tmpdir(), 'workflow-helpers-'))
try {
const source = resolve(directory, 'source')
const scripts = resolve(source, 'apps/desktop/scripts')
const temporary = resolve(directory, 'temporary')
mkdirSync(scripts, { recursive: true })
mkdirSync(temporary)
for (const name of ['windows-packaging-priority.ps1', 'windows-build-workspace.mjs']) {
writeFileSync(resolve(scripts, name), readFileSync(resolve(root, 'apps/desktop/scripts', name)))
}
const stage = resolve(directory, 'stage.ps1')
writeFileSync(stage, job.steps.find(step => step.name === 'Stage workflow helpers')!.run!)
const env = { ...process.env, RUNNER_TEMP: temporary, GITHUB_RUN_ID: '123', GITHUB_RUN_ATTEMPT: '1', GITHUB_ENV: resolve(directory, 'env') }
execFileSync('pwsh', ['-NoProfile', '-File', stage], { cwd: source, env, stdio: 'pipe' })
rmSync(scripts, { recursive: true })
const helper = resolve(temporary, 'packaging-helpers-123-1/windows-packaging-priority.ps1')
expect(execFileSync('pwsh', ['-NoProfile', '-File', helper, '-InspectOnly'], { cwd: source, stdio: 'pipe' }).toString())
.toContain('packaging-process-priority')
expect(existsSync(resolve(temporary, 'packaging-helpers-123-1/windows-build-workspace.mjs'))).toBe(true)
expect(() => execFileSync('pwsh', ['-NoProfile', '-File', stage], {
cwd: source, env: { ...env, GITHUB_RUN_ATTEMPT: '2' }, stdio: 'pipe',
})).toThrow()
} finally {
rmSync(directory, { recursive: true, force: true, maxRetries: 3 })
}
})
+1 -1
View File
@@ -253,7 +253,7 @@ grep -q '^smoke: win32 x64' "$scratch/logs/smoke.log" || { echo 'wine-windows-ga
build_gate() {
wine_node "$scratch/logs/host-typert.log" --import tsx/esm scripts/generate-typert.ts || return $?
wine_node "$scratch/logs/host-references.log" --import tsx/esm scripts/compile-referenced-projects.ts host || return $?
wine_node "$scratch/logs/host-tsc.log" --max-old-space-size=4096 "$tsc_js" -p tsconfig.host.json --pretty false || return $?
wine_node "$scratch/logs/host-tsc.log" --max-old-space-size=8192 "$tsc_js" -p tsconfig.host.json --pretty false || return $?
wine_node "$scratch/logs/host-tsdown.log" "$tsdown_js" --env.DSH_BUILD_FACE host || return $?
(cd apps/desktop && wine_node "$scratch/logs/desktop-tsdown.log" "../../$tsdown_js") || return $?
wine_node "$scratch/logs/client-tsc.log" "$tsc_js" -b tsconfig.client.json --pretty false || return $?