mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
Merge commit '8be1a8e63a48f357807d52070ea54db61912794e' into feat/schedule-clear-ended
# Conflicts: # packages/client/ui-schedule/src/client/index.ts
This commit is contained in:
commit
a6f92bfd79
1355 files changed
+29552
-12045
No files matched your search
@@ -328,6 +328,24 @@ describe('package payload constraints', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('publishes tools entry points and shared runtime chunks', () => {
|
||||
const dir = 'packages/core/tools'
|
||||
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/pipeline.js', 'lib/shared-*.js']) {
|
||||
expect(checkWorkspaceManifest({ dir, manifest: {
|
||||
...manifest, files: manifest.files!.filter(file => file !== omitted),
|
||||
} })).toEqual([expect.stringContaining('package.json files must be')])
|
||||
}
|
||||
expect(checkWorkspaceManifest({ dir, manifest: {
|
||||
...manifest,
|
||||
exports: {
|
||||
...manifest.exports,
|
||||
'./pipeline': { types: './lib/types/pipeline.d.ts', default: './lib/index.js' },
|
||||
},
|
||||
} })).toContainEqual(expect.stringContaining('exports["./pipeline"] must expose'))
|
||||
})
|
||||
|
||||
it('publishes the directory of each locale detail document after the locale resources', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-detail-files-'))
|
||||
onTestFinished(() => { rmSync(dir, { recursive: true, force: true }) })
|
||||
|
||||
@@ -20,7 +20,8 @@ import {
|
||||
import { hasTypertRemoteNavigation, isForbiddenPublicationFile } from './publication-payload.ts'
|
||||
import type { DshBundleManifest } from '../packages/util/package-manifest/src/types.ts'
|
||||
import { detailPath } from '../packages/util/plugin-detail/src/document.ts'
|
||||
import { OPTIONAL_BUNDLES, bundlePatchFiles } from '../packages/boot/app-boot/src/profile.ts'
|
||||
import { bundlePatchFiles } from '../packages/boot/app-boot/src/profile.ts'
|
||||
import { OPTIONAL_BUNDLES } from '../packages/boot/official-bundles/src/bundles.ts'
|
||||
import {
|
||||
PACKAGE_ASSET_EXPORT,
|
||||
PACKAGE_ASSET_EXPORT_TARGET,
|
||||
@@ -184,6 +185,8 @@ export function readWorkspaceManifests(repositoryRoot: string): WorkspaceManifes
|
||||
}
|
||||
|
||||
const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
|
||||
// Tools entry points share error classes and invocation stages through runtime chunks.
|
||||
'@deepseek-ai/dsh-tools': ['lib/pipeline.js', 'lib/shared-*.js'],
|
||||
// The embedded OAR implementation retains its upstream license.
|
||||
'@deepseek-ai/dsh-experimental-subagent-oar': ['LICENSE-OAR'],
|
||||
// CUA publishes its client API and attributed native source for local macOS builds.
|
||||
@@ -577,6 +580,10 @@ export function checkWorkspaceManifest({ dir, manifest }: WorkspaceManifest): st
|
||||
if (rootEntry?.default !== './lib/index.js') {
|
||||
errors.push(`${label}: package.json exports["."].default must be "./lib/index.js"`)
|
||||
}
|
||||
if (manifest.name === '@deepseek-ai/dsh-tools'
|
||||
&& !hasExportPair(manifest, './pipeline', './lib/types/pipeline.d.ts', './lib/pipeline.js')) {
|
||||
errors.push(`${label}: exports["./pipeline"] must expose lib/types/pipeline.d.ts and lib/pipeline.js`)
|
||||
}
|
||||
const expectedFiles = expectedDshPackageFiles(manifest, detailDocumentFiles(join(root, dir)))
|
||||
if (!sameStringList(manifest.files, expectedFiles)) {
|
||||
errors.push(`${label}: package.json files must be ${JSON.stringify(expectedFiles)}`)
|
||||
|
||||
@@ -25,7 +25,11 @@ interface CompatibilityJob {
|
||||
}
|
||||
|
||||
const workflow = yaml.load(readFileSync(resolve(import.meta.dirname, '../.github/workflows/ci.yml'), 'utf8')) as {
|
||||
jobs: { 'node-compat': CompatibilityJob; 'python-sdk': { 'runs-on': string } }
|
||||
jobs: { 'node-compat': CompatibilityJob }
|
||||
}
|
||||
// Python keeps its own required verdict in ci-python.yml.
|
||||
const pythonWorkflow = yaml.load(readFileSync(resolve(import.meta.dirname, '../.github/workflows/ci-python.yml'), 'utf8')) as {
|
||||
jobs: { 'python-sdk': { 'runs-on': string } }
|
||||
}
|
||||
const job = workflow.jobs['node-compat']
|
||||
const labels = ['self-hosted', 'linux', 'x64', 'vm-backup']
|
||||
@@ -86,7 +90,7 @@ describe('Node compatibility self-hosted routing', () => {
|
||||
expect(job.env.DSH_GATE_CONCURRENCY).toBe('${{ matrix.gate_concurrency }}')
|
||||
expect(job.steps.map(step => step.run)).toContain('pnpm run check:node-compat')
|
||||
expect(job.steps.map(step => step.run)).toContain('pnpm exec vitest run packages/boot/app-boot/tests/loader-shape.compat.spec.ts')
|
||||
expect(workflow.jobs['python-sdk']['runs-on']).toBe('ubuntu-latest')
|
||||
expect(pythonWorkflow.jobs['python-sdk']['runs-on']).toBe('ubuntu-latest')
|
||||
})
|
||||
|
||||
it('isolates version installs and enables hosted package caching only on hosted runners', () => {
|
||||
|
||||
+120
-46
@@ -14,12 +14,18 @@ const runnerPrivatePnpmDestination = /^\$\{\{ runner\.temp \}\}\/setup-pnpm-\$\{
|
||||
const nativeWindowsPnpmDestination = '${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}'
|
||||
|
||||
describe('CI workflow', () => {
|
||||
it('keeps at most ten worker jobs active while runtime targets follow the SDK suite', () => {
|
||||
it('keeps at most ten 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')
|
||||
const compatibility = workflowJob(ci, 'node-compat')
|
||||
const runtime = workflowJob(ci, 'python-runtime')
|
||||
if (!isRecord(ci.jobs) || !isRecord(compatibility.strategy) || !isRecord(runtime.with)) {
|
||||
throw new TypeError('CI must define jobs, compatibility scheduling, and runtime inputs')
|
||||
const runtime = workflowJob(ciPython, 'python-runtime')
|
||||
if (!isRecord(ci.jobs)
|
||||
|| !isRecord(ciWindows.jobs)
|
||||
|| !isRecord(ciPython.jobs)
|
||||
|| !isRecord(compatibility.strategy)
|
||||
|| !isRecord(runtime.with)) {
|
||||
throw new TypeError('CI, CI Windows, and CI Python must define jobs, compatibility scheduling, and runtime inputs')
|
||||
}
|
||||
const compatibilityLimit = compatibility.strategy['max-parallel']
|
||||
const targets = runtime.with.targets
|
||||
@@ -34,17 +40,31 @@ describe('CI workflow', () => {
|
||||
})
|
||||
expect(workflowJob(ci, 'all-checks-passed').needs).toContain('node-compat')
|
||||
const ordinaryJobs = Object.entries(ci.jobs)
|
||||
.filter(([name]) => !['node-compat', 'python-runtime', 'all-checks-passed'].includes(name))
|
||||
.filter(([name]) => !['node-compat', 'all-checks-passed'].includes(name))
|
||||
for (const [name, job] of ordinaryJobs) {
|
||||
if (!isRecord(job)) throw new TypeError(`${name} must define a job`)
|
||||
expect(job).not.toHaveProperty('strategy')
|
||||
expect(job).not.toHaveProperty('uses')
|
||||
}
|
||||
expect(ordinaryJobs.map(([name]) => name)).toContain('python-sdk')
|
||||
// Python reports its own required verdict, so neither its jobs nor a
|
||||
// reusable workflow caller remain in the Linux or Windows workflow.
|
||||
expect(Object.keys(ci.jobs).filter(name => name.startsWith('python'))).toEqual([])
|
||||
expect(Object.keys(ciWindows.jobs)).not.toContain('python-runtime')
|
||||
|
||||
const pythonSdk = workflowJob(ciPython, 'python-sdk')
|
||||
expect(pythonSdk).not.toHaveProperty('strategy')
|
||||
expect(pythonSdk).not.toHaveProperty('uses')
|
||||
expect(runtime.needs).toBe('python-sdk')
|
||||
expect(runtime.strategy).toBeUndefined()
|
||||
expect(runtime.with).toMatchObject({ targets: 'node24-linux-x64,node24-win-x64' })
|
||||
expect(runtime.uses).toBe('./.github/workflows/build-exe-for-python-sdk.yml')
|
||||
const windowsJobs = Object.entries(ciWindows.jobs)
|
||||
.filter(([name]) => name !== 'windows-checks-passed')
|
||||
for (const [name, job] of windowsJobs) {
|
||||
if (!isRecord(job)) throw new TypeError(`${name} must define a job`)
|
||||
expect(job).not.toHaveProperty('strategy')
|
||||
expect(job).not.toHaveProperty('uses')
|
||||
}
|
||||
|
||||
const builder = loadWorkflow('.github/workflows/build-exe-for-python-sdk.yml')
|
||||
if (!isRecord(builder.jobs)) throw new TypeError('Python runtime builder must define jobs')
|
||||
@@ -63,19 +83,54 @@ describe('CI workflow', () => {
|
||||
if (!isRecord(build.strategy)) throw new TypeError('Python runtime build must define its target matrix')
|
||||
expect(build.strategy.matrix).toEqual({ include: '${{ fromJSON(needs.plan.outputs.matrix) }}' })
|
||||
|
||||
const beforeSdk = ordinaryJobs.length + compatibilityLimit
|
||||
const afterSdk = ordinaryJobs.length - 1 + compatibilityLimit + Math.max(1, targets.split(',').length)
|
||||
// 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
|
||||
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)
|
||||
})
|
||||
|
||||
it('keeps the union of required PR verdicts equal to the pre-split blocking set', () => {
|
||||
const verdicts = [
|
||||
['ci.yml', 'all-checks-passed'],
|
||||
['ci-windows.yml', 'windows-checks-passed'],
|
||||
['ci-python.yml', 'python-checks-passed'],
|
||||
] as const
|
||||
const blocking: string[] = []
|
||||
for (const [file, name] of verdicts) {
|
||||
const workflow = loadWorkflow('.github/workflows/' + file)
|
||||
const verdict = workflowJob(workflow, name)
|
||||
if (!isRecord(workflow.jobs) || !Array.isArray(verdict.needs)) {
|
||||
throw new TypeError(`${file} must define jobs and the ${name} verdict needs`)
|
||||
}
|
||||
const jobs = workflow.jobs
|
||||
expect(verdict.needs.every(id => typeof id === 'string' && id in jobs)).toBe(true)
|
||||
blocking.push(...(verdict.needs as string[]))
|
||||
}
|
||||
// Splitting the workflows only moves jobs between verdicts; the blocking
|
||||
// set itself must stay identical, or branch protection would accept a
|
||||
// failure it rejected before the split.
|
||||
expect(blocking.sort()).toEqual([
|
||||
'node-24',
|
||||
'node-24-bench',
|
||||
'node-24-consumers',
|
||||
'node-24-coverage',
|
||||
'node-compat',
|
||||
'python-runtime',
|
||||
'python-sdk',
|
||||
'windows-build',
|
||||
'windows-native-tests',
|
||||
])
|
||||
})
|
||||
|
||||
it('retains aggregate diagnostics in required builds and protected publication', () => {
|
||||
const ci = loadWorkflow('.github/workflows/ci.yml')
|
||||
for (const [name, command] of [
|
||||
['node-24-consumers', 'pnpm run check:ci:consumers'],
|
||||
['windows-build', 'pnpm run check:ci:windows-blocking'],
|
||||
for (const [file, name, command] of [
|
||||
['ci.yml', 'node-24-consumers', 'pnpm run check:ci:consumers'],
|
||||
['ci-windows.yml', 'windows-build', 'pnpm run check:ci:windows-blocking'],
|
||||
] as const) {
|
||||
const job = workflowJob(ci, name)
|
||||
const job = workflowJob(loadWorkflow('.github/workflows/' + file), name)
|
||||
expect(job.steps).toContainEqual(expect.objectContaining({ run: command }))
|
||||
}
|
||||
for (const [file, command] of [
|
||||
@@ -182,7 +237,7 @@ describe('CI workflow', () => {
|
||||
})
|
||||
|
||||
it('skips coverage-history uploads on cancellation but retains Wine cleanup', () => {
|
||||
const coverage = workflowJob(loadWorkflow('.github/workflows/ci.yml'), 'windows-coverage')
|
||||
const coverage = workflowJob(loadWorkflow('.github/workflows/ci-windows.yml'), 'windows-coverage')
|
||||
const wine = workflowJob(loadWorkflow('.github/workflows/ci-master.yml'), 'windows')
|
||||
expect(coverage.steps).toContainEqual(expect.objectContaining({
|
||||
name: 'Save coverage duration history', if: '${{ !cancelled() }}',
|
||||
@@ -191,10 +246,10 @@ describe('CI workflow', () => {
|
||||
})
|
||||
|
||||
it.each([
|
||||
['node-24-coverage', 'Linux', 'LINUX', 'pnpm run check:ci:coverage'],
|
||||
['windows-coverage', 'Windows', 'WINDOWS', 'pnpm run check:ci:coverage'],
|
||||
])('reuses %s coverage timings only within its platform and runner pool', (jobName, platform, variable, command) => {
|
||||
const job = workflowJob(loadWorkflow('.github/workflows/ci.yml'), jobName)
|
||||
['node-24-coverage', 'ci.yml', 'Linux', 'LINUX', 'pnpm run check:ci:coverage'],
|
||||
['windows-coverage', 'ci-windows.yml', 'Windows', 'WINDOWS', 'pnpm run check:ci:coverage'],
|
||||
])('reuses %s coverage timings only within its platform and runner pool', (jobName, file, platform, variable, command) => {
|
||||
const job = workflowJob(loadWorkflow('.github/workflows/' + file), jobName)
|
||||
if (!Array.isArray(job.steps)) throw new TypeError(`${jobName} must define steps`)
|
||||
const steps = job.steps.filter(isRecord)
|
||||
const checkout = steps.findIndex(step => step.uses === 'actions/checkout@v7.0.1')
|
||||
@@ -219,7 +274,7 @@ describe('CI workflow', () => {
|
||||
})
|
||||
|
||||
it('isolates every pnpm action setup destination per runner', () => {
|
||||
const files = ['.github/workflows/ci.yml', '.github/workflows/ci-master.yml']
|
||||
const files = ['.github/workflows/ci.yml', '.github/workflows/ci-windows.yml', '.github/workflows/ci-master.yml']
|
||||
const setups: Array<{ jobName: string; step: unknown }> = []
|
||||
for (const file of files) {
|
||||
const workflow: unknown = yaml.load(readFileSync(resolve(root, file), 'utf8'))
|
||||
@@ -304,13 +359,16 @@ describe('CI workflow', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps split native Windows PR jobs with failover, plus a master-only standby', () => {
|
||||
it('keeps split native Windows PR jobs with failover in their own workflow, plus a master-only standby', () => {
|
||||
const workflow = loadWorkflow('.github/workflows/ci.yml')
|
||||
const windowsWorkflow = loadWorkflow('.github/workflows/ci-windows.yml')
|
||||
const masterWorkflow = loadWorkflow('.github/workflows/ci-master.yml')
|
||||
if (!isRecord(workflow.jobs)
|
||||
|| !isRecord(workflow.jobs['windows-build'])
|
||||
|| !isRecord(workflow.jobs['windows-coverage'])
|
||||
|| !isRecord(workflow.jobs['windows-native-tests'])
|
||||
|| !isRecord(windowsWorkflow.jobs)
|
||||
|| !isRecord(windowsWorkflow.jobs['windows-build'])
|
||||
|| !isRecord(windowsWorkflow.jobs['windows-coverage'])
|
||||
|| !isRecord(windowsWorkflow.jobs['windows-native-tests'])
|
||||
|| !isRecord(windowsWorkflow.jobs['windows-checks-passed'])
|
||||
|| !isRecord(workflow.jobs['node-24'])
|
||||
|| !isRecord(workflow.jobs['node-24-coverage'])
|
||||
|| !isRecord(workflow.jobs['node-24-bench'])
|
||||
@@ -319,12 +377,13 @@ describe('CI workflow', () => {
|
||||
|| !isRecord(workflow.jobs['all-checks-passed'])
|
||||
|| !isRecord(masterWorkflow.jobs)
|
||||
|| !isRecord(masterWorkflow.jobs['serial-windows'])) {
|
||||
throw new TypeError('CI workflow must define windows-build, windows-coverage, windows-native-tests, node-24, node-24-coverage, node-24-bench, node-24-consumers, node-compat, and all-checks-passed; ci-master must define 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')
|
||||
}
|
||||
|
||||
const windowsBuild = workflow.jobs['windows-build']
|
||||
const windowsCoverage = workflow.jobs['windows-coverage']
|
||||
const windowsNativeTests = workflow.jobs['windows-native-tests']
|
||||
const windowsBuild = windowsWorkflow.jobs['windows-build']
|
||||
const windowsCoverage = windowsWorkflow.jobs['windows-coverage']
|
||||
const windowsNativeTests = windowsWorkflow.jobs['windows-native-tests']
|
||||
const windowsAggregate = windowsWorkflow.jobs['windows-checks-passed']
|
||||
const serialWindows = masterWorkflow.jobs['serial-windows']
|
||||
const node24 = workflow.jobs['node-24']
|
||||
const node24Coverage = workflow.jobs['node-24-coverage']
|
||||
@@ -332,9 +391,10 @@ describe('CI workflow', () => {
|
||||
const node24Consumers = workflow.jobs['node-24-consumers']
|
||||
const nodeCompat = workflow.jobs['node-compat']
|
||||
const aggregate = workflow.jobs['all-checks-passed']
|
||||
if (!Array.isArray(aggregate.needs)) {
|
||||
throw new TypeError('CI aggregate must define needs')
|
||||
if (!Array.isArray(aggregate.needs) || !Array.isArray(windowsAggregate.needs)) {
|
||||
throw new TypeError('CI and CI Windows aggregates must define needs')
|
||||
}
|
||||
expect(Object.keys(workflow.jobs).filter(name => name.startsWith('windows'))).toEqual([])
|
||||
// The split native jobs all resolve their pool through the Windows switch.
|
||||
for (const [jobName, job] of [['windows-build', windowsBuild], ['windows-coverage', windowsCoverage], ['windows-native-tests', windowsNativeTests]] as const) {
|
||||
expect(typeof job['runs-on']).toBe('string')
|
||||
@@ -414,7 +474,7 @@ describe('CI workflow', () => {
|
||||
expect(nativeTestCommand).toContain('tool-pwsh/tests/loader.spec.ts')
|
||||
expect(nativeTestCommand).toContain('workflow-ptc.spec.ts')
|
||||
|
||||
expect(workflow.jobs['windows-observational']).toBeUndefined()
|
||||
expect(windowsWorkflow.jobs['windows-observational']).toBeUndefined()
|
||||
expect(windowsBuild['continue-on-error']).not.toBe(true)
|
||||
const observational = buildCommands.find(step => step.id === 'observational')
|
||||
expect(observational).toMatchObject({
|
||||
@@ -473,10 +533,13 @@ describe('CI workflow', () => {
|
||||
expect(serialGate).toBeDefined()
|
||||
expect(serialGate!.env).toMatchObject({ DSH_COVERAGE_TEST_TIMEOUT_MS: '90000' })
|
||||
|
||||
expect(aggregate.needs).not.toContain('windows')
|
||||
// windows-coverage is temporarily non-blocking while Windows ACP
|
||||
// half-close tests are stabilized; observational stays out too.
|
||||
expect(aggregate.needs).not.toContain('windows')
|
||||
expect(aggregate.needs).toContain('windows-build')
|
||||
expect(windowsAggregate.needs).toEqual(['windows-build', 'windows-native-tests'])
|
||||
expect(windowsAggregate.name).toBe('windows checks passed')
|
||||
expect(windowsAggregate['runs-on']).toBe(aggregate['runs-on'])
|
||||
expect(windowsAggregate.if).toBe(aggregate.if)
|
||||
// The benchmark lane is a required verdict input and runs alone so its
|
||||
// wall-clock budgets never share a runner with a concurrent aggregate.
|
||||
expect(aggregate.needs).toContain('node-24-bench')
|
||||
@@ -492,10 +555,7 @@ describe('CI workflow', () => {
|
||||
env: { DSH_GATE_VERBOSE: '1' },
|
||||
run: 'pnpm run check:ci:bench',
|
||||
})
|
||||
expect(aggregate.needs).not.toContain('windows-coverage')
|
||||
expect(aggregate.needs).toContain('windows-native-tests')
|
||||
expect(aggregate.needs).not.toContain('windows-observational')
|
||||
expect(aggregate.needs).not.toContain('serial-windows')
|
||||
expect(windowsAggregate.needs).not.toContain('serial-windows')
|
||||
|
||||
// Linux failover is a separate switch: the three enterprise Linux workers
|
||||
// and the verdict job resolve their pool through DSH_CI_FAILOVER_LINUX,
|
||||
@@ -712,6 +772,18 @@ describe('CI workflow', () => {
|
||||
}
|
||||
expect(Object.keys(workflow.on).sort()).toEqual(['push', 'workflow_dispatch'])
|
||||
expect(Object.keys(prWorkflow.on)).toEqual(['pull_request'])
|
||||
const windowsWorkflow = loadWorkflow('.github/workflows/ci-windows.yml')
|
||||
if (!isRecord(windowsWorkflow.on)) throw new TypeError('CI Windows workflow must define on')
|
||||
expect(Object.keys(windowsWorkflow.on)).toEqual(['pull_request'])
|
||||
expect(windowsWorkflow.concurrency).toEqual(workflow.concurrency)
|
||||
// Duplicated env must not drift: a Node bump on one lane only would run
|
||||
// Linux and Windows against different versions.
|
||||
expect(windowsWorkflow.env).toEqual(prWorkflow.env)
|
||||
const pythonWorkflow = loadWorkflow('.github/workflows/ci-python.yml')
|
||||
if (!isRecord(pythonWorkflow.on)) throw new TypeError('CI Python workflow must define on')
|
||||
expect(Object.keys(pythonWorkflow.on)).toEqual(['pull_request'])
|
||||
expect(pythonWorkflow.concurrency).toEqual(workflow.concurrency)
|
||||
expect(pythonWorkflow.env).toEqual(prWorkflow.env)
|
||||
|
||||
// Drills share the parent run’s supersession policy.
|
||||
for (const name of ['serial-linux-selfhosted', 'serial-windows']) {
|
||||
@@ -789,27 +861,29 @@ describe('CI workflow', () => {
|
||||
expect(config).not.toContain('packages/lsp/lsp-stdio/src/instance.ts')
|
||||
})
|
||||
|
||||
it('requires release-shaped Python runtime validation on Linux and Windows x64', () => {
|
||||
const workflow = loadWorkflow('.github/workflows/ci.yml')
|
||||
it('requires release-shaped Python runtime validation on Linux and Windows x64 in CI Python', () => {
|
||||
const workflow = loadWorkflow('.github/workflows/ci-python.yml')
|
||||
const pythonRuntime = workflowJob(workflow, 'python-runtime')
|
||||
const aggregate = workflowJob(workflow, 'all-checks-passed')
|
||||
const aggregate = workflowJob(workflow, 'python-checks-passed')
|
||||
if (!Array.isArray(aggregate.needs)) {
|
||||
throw new TypeError('CI aggregate must define required job dependencies')
|
||||
throw new TypeError('CI Python aggregate must define required job dependencies')
|
||||
}
|
||||
|
||||
expect(pythonRuntime).toMatchObject({
|
||||
if: "github.event_name == 'pull_request'",
|
||||
name: 'python runtime / release-shaped matrix',
|
||||
uses: './.github/workflows/build-exe-for-python-sdk.yml',
|
||||
with: {
|
||||
targets: 'node24-linux-x64,node24-win-x64',
|
||||
ci: true,
|
||||
},
|
||||
with: { targets: 'node24-linux-x64,node24-win-x64', ci: true },
|
||||
secrets: {
|
||||
DEEPSEEK_API_KEY_EXTERNAL: '${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}',
|
||||
},
|
||||
})
|
||||
expect(aggregate.needs).toContain('python-runtime')
|
||||
expect(aggregate.needs).toEqual(['python-sdk', 'python-runtime'])
|
||||
for (const file of ['ci.yml', 'ci-windows.yml']) {
|
||||
const other = loadWorkflow('.github/workflows/' + file)
|
||||
if (!isRecord(other.jobs)) throw new TypeError(`${file} must define jobs`)
|
||||
expect(other.jobs).not.toHaveProperty('python-runtime')
|
||||
}
|
||||
})
|
||||
|
||||
it('builds SSH executables only for explicit manual or release requests', () => {
|
||||
@@ -1308,7 +1382,7 @@ describe('Weighted approval workflow', () => {
|
||||
if: "failure() && steps.revoke.outputs.active == 'true'",
|
||||
run: 'node .github/review-ownership/check-approval.mjs error',
|
||||
})
|
||||
const pythonJob = workflowJob(loadWorkflow('.github/workflows/ci.yml'), 'python-sdk')
|
||||
const pythonJob = workflowJob(loadWorkflow('.github/workflows/ci-python.yml'), 'python-sdk')
|
||||
expect(pythonJob.steps).toContainEqual({
|
||||
name: 'Test production blame scoring',
|
||||
run: "uv run --python 3.10 --with-requirements .github/review-ownership/requirements.txt python -m unittest discover -s .github/review-ownership -p 'test_*.py'",
|
||||
|
||||
@@ -29,6 +29,8 @@ const roots: string[] = []
|
||||
const dshBuildWorkflows = [
|
||||
'build-exe-for-python-sdk.yml',
|
||||
'ci.yml',
|
||||
'ci-python.yml',
|
||||
'ci-windows.yml',
|
||||
'e2e.yml',
|
||||
'release.yml',
|
||||
'release-publish.yml',
|
||||
|
||||
@@ -234,6 +234,14 @@ describe('client bundle purity gate', () => {
|
||||
expect(() => resolveId('@deepseek-ai/dsh-spill-policy/notice/internal')).toThrow(/purity/)
|
||||
})
|
||||
|
||||
it('inlines the stateless file-drag entry without admitting its subpaths or UI plugin', () => {
|
||||
expect(resolveId('@deepseek-ai/dsh-util-file-drag')).toBeNull()
|
||||
expect(() => resolveId('@deepseek-ai/dsh-util-file-drag/internal')).toThrow(/purity/)
|
||||
expect(() => resolveId('@deepseek-ai/dsh-client-ui-file-drag')).toThrow(/purity/)
|
||||
const deps = clientConfigs()[0]?.deps as { neverBundle: (specifier: string) => boolean }
|
||||
expect(deps.neverBundle('@deepseek-ai/dsh-util-file-drag')).toBe(false)
|
||||
})
|
||||
|
||||
it('inlines the browser-safe Timer entry while sharing Cordis through the module table', () => {
|
||||
const timer = '@deepseek-ai/cordis-plugin-timer'
|
||||
expect(resolveId(timer)).toBeNull()
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/** Raw retention and candidate evaluation preserve the running plugin's config identity. */
|
||||
import { Context, type Fiber } from '@deepseek-ai/cordis'
|
||||
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
import { describe, expect, it, onTestFinished, vi } from 'vitest'
|
||||
|
||||
function root(): Context {
|
||||
const ctx = new Context()
|
||||
onTestFinished(() => ctx.fiber.dispose())
|
||||
return ctx
|
||||
}
|
||||
|
||||
describe('Fiber configuration access', () => {
|
||||
it('keeps raw and running references unchanged while resolving valid or invalid candidates', async () => {
|
||||
const ctx = root()
|
||||
await ctx.plugin(Loader)
|
||||
const Config = z.object({ value: z.number().min(0).volatile() })
|
||||
const applied = vi.fn()
|
||||
ctx.loader.builtins.consumer = { Config, apply: applied }
|
||||
const raw = { value: { __jsExpr: '1 + 1' } }
|
||||
const id = await ctx.loader.create({ name: 'cordis:consumer', config: raw })
|
||||
const fiber = ctx.loader.resolve(id).fiber!
|
||||
await fiber.await()
|
||||
const running = applied.mock.calls[0]![1] as ReturnType<typeof Config>
|
||||
expect(fiber.rawConfig).toBe(raw)
|
||||
expect(fiber.effectiveConfig).toBe(running)
|
||||
const candidate = fiber.resolveConfig({ value: { __jsExpr: '2 + 2' } }) as ReturnType<typeof Config>
|
||||
expect(candidate.value.get()).toBe(4)
|
||||
expect(candidate.value).not.toBe(running.value)
|
||||
expect(() => fiber.resolveConfig({ value: -1 })).toThrow()
|
||||
expect(fiber.rawConfig).toBe(raw)
|
||||
expect(fiber.effectiveConfig).toBe(running)
|
||||
expect(running.value.get()).toBe(2)
|
||||
expect(applied).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('uses the canonical fiber through registry wrappers and retains raw input without updating', async () => {
|
||||
const ctx = root()
|
||||
const initial = { value: 1 }
|
||||
const apply = vi.fn()
|
||||
const wrapper = ctx.plugin(apply, initial)
|
||||
await wrapper
|
||||
const fiber = wrapper.ctx.fiber
|
||||
const hook = vi.fn(function (this: Fiber, _config: unknown, next: () => unknown) {
|
||||
expect(this).toBe(fiber)
|
||||
return next()
|
||||
})
|
||||
fiber.ctx.on('internal/config', hook)
|
||||
const raw = { value: 2 }
|
||||
wrapper.rawConfig = raw
|
||||
expect(fiber.rawConfig).toBe(raw)
|
||||
expect(wrapper.effectiveConfig).toBe(initial)
|
||||
expect(wrapper.resolveConfig(raw)).toBe(raw)
|
||||
expect(hook).toHaveBeenCalledOnce()
|
||||
expect(fiber.effectiveConfig).toBe(initial)
|
||||
expect(apply).toHaveBeenCalledOnce()
|
||||
await wrapper.restart()
|
||||
expect(fiber.effectiveConfig).toBe(raw)
|
||||
expect(apply).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('publishes updates through registry wrappers to the canonical fiber', async () => {
|
||||
const ctx = root()
|
||||
const initial = { value: 1 }
|
||||
const apply = vi.fn()
|
||||
const wrapper = ctx.plugin(apply, initial)
|
||||
await wrapper
|
||||
const raw = { value: 2 }
|
||||
wrapper.update(raw)
|
||||
await wrapper.await()
|
||||
expect(apply).toHaveBeenLastCalledWith(wrapper.ctx, raw)
|
||||
expect(wrapper.rawConfig).toBe(raw)
|
||||
expect(wrapper.effectiveConfig).toBe(raw)
|
||||
expect(wrapper.ctx.fiber.rawConfig).toBe(raw)
|
||||
expect(wrapper.ctx.fiber.effectiveConfig).toBe(raw)
|
||||
expect(apply).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('retains a pending raw update until its injection is available', async () => {
|
||||
const ctx = root()
|
||||
ctx.provide('configAccessTest')
|
||||
const apply = vi.fn()
|
||||
const wrapper = ctx.plugin({ inject: ['configAccessTest'], apply }, { value: 1 })
|
||||
const raw = { value: 2 }
|
||||
wrapper.update(raw)
|
||||
expect(wrapper.rawConfig).toBe(raw)
|
||||
expect(wrapper.effectiveConfig).toBeUndefined()
|
||||
ctx.set('configAccessTest', {})
|
||||
await wrapper
|
||||
expect(wrapper.effectiveConfig).toBe(raw)
|
||||
expect(apply).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
@@ -10,9 +10,10 @@ const child = spawn(process.execPath, [
|
||||
'apps/cli/src/bin.ts',
|
||||
'--profile',
|
||||
'headless',
|
||||
'--patch',
|
||||
'apps/cli/config/examples/ptc.yml',
|
||||
task,
|
||||
], {
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, DSH_TOOLS_MODE: 'ptc' },
|
||||
})
|
||||
child.on('exit', (code, signal) => { process.exit(signal !== null ? 1 : code ?? 1) })
|
||||
@@ -55,9 +55,11 @@ const KIND_TEMPLATES: Readonly<Record<string, string>> = {
|
||||
* library; the check re-derives the entry shape so a stale entry fails loud.
|
||||
*/
|
||||
const PACKAGE_LIBRARIES: Readonly<Record<string, string>> = {
|
||||
'packages/util/file-drag': 'File naming and native transfer declarations without a plugin entry.',
|
||||
'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/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.',
|
||||
@@ -81,6 +83,7 @@ const PACKAGE_LIBRARIES: Readonly<Record<string, string>> = {
|
||||
'packages/subprocess/win32-process': 'Low-level Win32 process and Job Object primitives.',
|
||||
'packages/test-support/session-snapshot': 'Test infrastructure; mounts nothing into a product composition.',
|
||||
'packages/test-support/agent-loop-testkit': 'Test helper library; mounts nothing into a product composition.',
|
||||
'packages/test-support/tool-testkit': 'Tool fixture library; exports helpers without a plugin entry.',
|
||||
'packages/test-support/client-runtime': 'Browser-side test infrastructure.',
|
||||
'packages/test-support/llm-mock-server': 'Test server library; substitutes provider wire behavior.',
|
||||
'packages/test-support/loader-smoke': 'Test harness library; mounts nothing into a product composition.',
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
/** Add a native tool after a file read and unregister it after its first execution. */
|
||||
/** Activate a native tool after a file read and disable it after its first execution. */
|
||||
export const name = 'python-sdk-dynamic-tools'
|
||||
export const inject = ['tools']
|
||||
|
||||
/** @param {import('@deepseek-ai/cordis').Context} ctx - Scenario-owned tool registry. */
|
||||
export function apply(ctx) {
|
||||
let removeTool
|
||||
let active = false
|
||||
ctx.tools.register({
|
||||
name: 'snapshot_ping',
|
||||
description: 'Return pong once.',
|
||||
parameters: { type: 'object', properties: {}, additionalProperties: false },
|
||||
output: {
|
||||
schema: { type: 'string' },
|
||||
render(_args, value) { return [{ type: 'text', text: value }] },
|
||||
},
|
||||
async execute() { return 'pong' },
|
||||
}, { when: async ({ modelSelection }) => active && modelSelection.provider === 'deepseek-official' })
|
||||
ctx.on('tools/post-execute', async (exec, result, next) => {
|
||||
const downstream = await next()
|
||||
if (result.isError) return downstream
|
||||
if (exec.name === 'read' && exec.arguments.file_path === 'dynamic-tool-task.txt') {
|
||||
removeTool = ctx.effect(() => ctx.tools.register({
|
||||
name: 'snapshot_ping',
|
||||
description: 'Return pong once.',
|
||||
parameters: { type: 'object', properties: {}, additionalProperties: false },
|
||||
output: {
|
||||
schema: { type: 'string' },
|
||||
render(_args, value) { return [{ type: 'text', text: value }] },
|
||||
},
|
||||
async execute() { return 'pong' },
|
||||
}))
|
||||
active = true
|
||||
} else if (exec.name === 'snapshot_ping') {
|
||||
removeTool?.()
|
||||
removeTool = undefined
|
||||
active = false
|
||||
}
|
||||
return downstream
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import { PROFILE_TEMPLATES } from '@deepseek-ai/dsh-app-boot'
|
||||
import { PROFILE_TEMPLATES } from '@deepseek-ai/dsh-official-bundles'
|
||||
import {
|
||||
computeTranslationPairingRecord,
|
||||
renderTranslationPairingRecord,
|
||||
|
||||
@@ -193,6 +193,7 @@ export const SERVICE_WALK_EXEMPTIONS: Record<string, string> = {
|
||||
shortcuts: 'client-side interface-typed keyboard service — packages/client/shortcuts/README.md owns the API',
|
||||
userQuestionPanels: 'client-side slot-contract accessor (UserQuestionPanels) — packages/client/ui-tool/README.md owns the API',
|
||||
commandUi: 'client-side interface-typed browser service — packages/client/ui-commands/README.md owns the API',
|
||||
fileDrag: 'client-side file preparation and transfer service — packages/client/ui-file-drag/README.md owns the API',
|
||||
feedbackUi: 'client-side feedback dialog service — packages/client/ui-message-feedback/README.md owns the API',
|
||||
conversation: 'client-side interface-typed browser service — packages/client/ui-conversation/README.md owns the API',
|
||||
layout: 'client-side interface-typed browser service — packages/client/ui-layout/README.md owns the API',
|
||||
@@ -466,6 +467,8 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
PromptFileBinding: 'attachment.md',
|
||||
FileUploadReceiptId: 'attachment.md',
|
||||
FileUploadValue: 'attachment.md',
|
||||
ImageUploadValue: 'attachment.md',
|
||||
StagedAttachment: 'attachment.md',
|
||||
SessionStartSource: 'core.md',
|
||||
ToolCallId: 'core.md',
|
||||
SessionLogSnapshot: 'session-query.md',
|
||||
@@ -748,9 +751,18 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
ToolExecutionResult: 'tools.md',
|
||||
ToolExecutionToken: 'tools.md',
|
||||
ToolGuard: 'tools.md',
|
||||
ToolPresentationMode: 'tools.md',
|
||||
ToolRuntime: 'tools.md',
|
||||
ToolsService: 'tools.md',
|
||||
ToolExecutor: 'tools.md',
|
||||
ToolModelInput: 'tools.md',
|
||||
ToolPromptSection: 'tools.md',
|
||||
ToolBatchInput: 'tools.md',
|
||||
ToolCallResult: 'tools.md',
|
||||
ToolBatchResult: 'tools.md',
|
||||
ToolRestriction: 'tools.md',
|
||||
ToolActivationContext: 'tools.md',
|
||||
ToolRegistrationOptions: 'tools.md',
|
||||
ResolvedToolSet: 'tools.md',
|
||||
ToolSchema: 'tools.md',
|
||||
SettingsNamespace: 'settings.md',
|
||||
SettingsDescriptor: 'settings.md',
|
||||
@@ -807,6 +819,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
PresetOption: 'permission-presets.md',
|
||||
PresetSpec: 'permission-presets.md',
|
||||
WebRoute: 'web-server.md',
|
||||
WebResourceRoute: 'web-server.md',
|
||||
IndexInjection: 'web-server.md',
|
||||
StorageBackend: 'storage.md',
|
||||
StorageForms: 'storage.md',
|
||||
@@ -979,6 +992,7 @@ export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
|
||||
JsonValue: 'JSON value union is owned by packages/core/session/src/json.ts',
|
||||
KnobState: 'projection unit state fields are owned by packages/interaction/permission-presets/README.md',
|
||||
PromptAssembly: 'assembly result is owned by packages/core/system-prompt/README.md',
|
||||
PromptPresentation: 'presentation inputs are owned by packages/core/system-prompt/README.md',
|
||||
RequestRunId: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
RpcReceipt: 'carrier-layer receipt is owned by packages/client/connection/src/rpc.ts',
|
||||
SessionForkSource: 'service-local fork input is owned by packages/core/session/src/index.ts',
|
||||
|
||||
@@ -574,10 +574,10 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
{
|
||||
key: 'tools',
|
||||
pkg: 'tools',
|
||||
title: 'Tool registry and guarded execution pipeline',
|
||||
title: 'Scoped tool registry and executor selection',
|
||||
mode: 'core',
|
||||
consumers: ['agent-loop', 'tool-ask-user', 'tool-bash', 'tool-cordis', 'tool-fs', 'tool-terminal', 'tool-skill', 'tool-subagent', 'tool-todo', 'tool-web'],
|
||||
note: 'Registers capabilities, owns PTC mode transport, and routes calls through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation.',
|
||||
note: 'Registers capabilities and scoped executors. Native and PTC providers own descriptions and complete raw call batches; their optional shared pipeline handles tool policy and dispatch.',
|
||||
},
|
||||
{
|
||||
key: 'userQuestions',
|
||||
@@ -755,8 +755,8 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
title: 'PTC execution seam',
|
||||
mode: 'seam',
|
||||
implementations: ['ptc-runtime-node', 'experimental-ptc-runtime-python'],
|
||||
consumers: ['tools', 'workflow-ptc'],
|
||||
note: 'Runs programs against host-provided async bindings; tools owns PTC presentation and workflow-ptc owns workflow orchestration.',
|
||||
consumers: ['tool-executor-ptc', 'workflow-ptc'],
|
||||
note: 'Runs programs against host-provided async bindings; tool-executor-ptc owns PTC presentation and workflow-ptc owns workflow orchestration.',
|
||||
},
|
||||
{
|
||||
key: 'fs',
|
||||
@@ -1577,7 +1577,8 @@ function renderLifecycle(): string {
|
||||
' Note over Agent,Driver: claim pending next-step input plus one queued prompt',
|
||||
` Driver-->>SDK: ${mermaidCode('agent/inbox/spliced')} pure deletion`,
|
||||
` Driver-->>SDK: ${mermaidCode('agent/inbox/claimed')} { message, turn } per message`,
|
||||
` Driver->>Prompt: ${mermaidCode('system-prompt/assemble')} waterfall`,
|
||||
' Driver->>Tools: resolve({ agent, modelSelection, signal }) once per proposed step',
|
||||
` Driver->>Prompt: ${mermaidCode('system-prompt/assemble')} waterfall with the step tool set`,
|
||||
` Driver->>Hooks: ${mermaidCode('agent/pre-step')} waterfall`,
|
||||
' Hooks-->>Driver: authoritative reject or enter(messages)',
|
||||
' alt proposed step rejected, first batch empty, or pre-step failed',
|
||||
@@ -1617,6 +1618,7 @@ function renderLifecycle(): string {
|
||||
' end',
|
||||
' end',
|
||||
` Driver->>Session: ${mermaidCode('step/end')}`,
|
||||
' Driver->>Tools: dispose the step tool set',
|
||||
' opt natural stop and next-step inbox empty',
|
||||
` Driver->>Hooks: ${mermaidCode('agent/turn-stopping')} serial terminal checkpoint`,
|
||||
' end',
|
||||
@@ -1648,7 +1650,7 @@ function renderToolPipeline(): string {
|
||||
const maintenance = 'curated Mermaid flow; exact tool schemas and event signatures live in generated catalogs'
|
||||
return [
|
||||
...generatedHeader('Tool Execution Pipeline'),
|
||||
'This graph shows where policy, hooks, sandboxing, filesystem guards, result rewriting, final-outcome observation, and UI rendering run without changing the loop. The `tools/pre-execute` waterfall runs first, monotonic guards run next, and the `tools/execute` and `tools/post-execute` waterfalls follow; the three waterfalls may transform a call. Definition-owned `projectContent` installs prepared content before post-execute; `finalizeContent` and `tools/result` run afterward.',
|
||||
'Built-in executors own raw batch parsing, scheduling, cancellation, and Session call/result logging. This graph shows where their shared invocation pipeline applies policy, hooks, sandboxing, filesystem guards, result rewriting, final-outcome observation, and UI rendering without changing the loop. The `tools/pre-execute` waterfall runs first, monotonic guards run next, and the `tools/execute` and `tools/post-execute` waterfalls follow; the three waterfalls may transform a call. Definition-owned `projectContent` installs prepared content before post-execute; `finalizeContent` and `tools/result` run afterward.',
|
||||
'',
|
||||
'```mermaid',
|
||||
'flowchart TD',
|
||||
@@ -1665,14 +1667,15 @@ function renderToolPipeline(): string {
|
||||
` owned["Tool-owned session events<br/>${mermaidCode('todo/write')}, ${mermaidCode('fs/observed')}, ${mermaidCode('hook/invoked')}, ${mermaidCode('hook/result')}, ${mermaidCode('tool/ptc-dispatch')}"]`,
|
||||
' project["ToolDefinition.projectContent<br/>execution-prepared text and images"]',
|
||||
` post["${mermaidCode('tools/post-execute')} waterfall<br/>accept, block, replace, add context"]`,
|
||||
' normalized["Registry outer normalization<br/>pipeline/result snapshot throws become isError"]',
|
||||
' normalized["Invocation pipeline normalization<br/>pipeline/result snapshot throws become isError"]',
|
||||
' finalize["ToolDefinition.finalizeContent<br/>last content-only invariant"]',
|
||||
` final["${mermaidCode('tools/result')} synchronous notification<br/>frozen authoritative outcome"]`,
|
||||
' context["Active-batch additionalContexts FIFO<br/>injected user/message after recorded tool results"]',
|
||||
` toolResult["Session event: ${mermaidCode('tool/result')}<br/>single model-facing outcome"]`,
|
||||
' allResults["Tool batch settled<br/>recorded tool/result events complete"]',
|
||||
' presentResult["UI completed card<br/>presentResult(args, result)"]',
|
||||
' model --> toolCall',
|
||||
' executor["Selected ToolExecutor.execute<br/>raw batch, Agent, optional logging step"]',
|
||||
' model --> executor --> toolCall',
|
||||
' toolCall --> presentCall',
|
||||
' toolCall --> pre',
|
||||
' pre -->|allow| guards',
|
||||
@@ -1705,7 +1708,7 @@ function renderToolPipeline(): string {
|
||||
' allResults --> context',
|
||||
'```',
|
||||
'',
|
||||
'Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`. The registry losslessly snapshots the candidate result and normalizes a snapshot failure before the visible definition\'s snapshotted `finalizeContent` callback enforces its synchronous content-only invariant. `tools/result` then observes the immutable, lossless-JSON outcome. This lets hooks span tool families without coupling the tools to one policy service. PTC mode sends both the reserved `run_code` transport and its serialized sub-calls through the pipeline; sub-calls carry the parent token, log `tool/ptc-dispatch`, return denials as binding rejections, and omit `additionalContexts` to preserve call/result adjacency.',
|
||||
'Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`. The pipeline losslessly snapshots the candidate result and normalizes a snapshot failure before the visible definition\'s snapshotted `finalizeContent` callback enforces its synchronous content-only invariant. `tools/result` then observes the immutable, lossless-JSON outcome. This lets hooks span tool families without coupling the tools to one policy service. PTC mode sends both the reserved `run_code` transport and its ordered sub-calls through the pipeline; sub-calls carry the parent token, log `tool/ptc-dispatch`, return denials as binding rejections, and omit `additionalContexts` to preserve call/result adjacency.',
|
||||
'',
|
||||
...maintenanceFooter(maintenance),
|
||||
].join('\n')
|
||||
|
||||
@@ -4,11 +4,11 @@ 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/app-boot/src/official-bundles.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../packages/boot/official-bundles/src/index.ts'
|
||||
import { generateOfficialBundleCatalog, renderOfficialBundleCatalog, syncOfficialBundleCatalog } from './gen-official-bundle-catalog.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
const output = 'packages/boot/app-boot/src/official-bundles.generated.ts'
|
||||
const output = 'packages/boot/official-bundles/src/catalog.generated.ts'
|
||||
|
||||
function write(root: string, path: string, content: unknown): void {
|
||||
mkdirSync(dirname(join(root, path)), { recursive: true })
|
||||
|
||||
@@ -5,11 +5,11 @@ import { resolve } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import ts from 'typescript'
|
||||
import type { LocalizedText } from '../packages/util/package-manifest/src/types.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../packages/boot/app-boot/src/official-bundle-packages.ts'
|
||||
import type { OfficialBundleCatalogEntry } from '../packages/boot/app-boot/src/official-bundles.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../packages/boot/official-bundles/src/bundles.ts'
|
||||
import type { OfficialBundleCatalogEntry } from '../packages/boot/official-bundles/src/index.ts'
|
||||
import { readPluginMeta, resolvePluginResource } from '../packages/boot/app-boot/src/package-meta.ts'
|
||||
|
||||
const OUTPUT = 'packages/boot/app-boot/src/official-bundles.generated.ts'
|
||||
const OUTPUT = 'packages/boot/official-bundles/src/catalog.generated.ts'
|
||||
|
||||
/**
|
||||
* Read complete metadata without importing or activating any provider code.
|
||||
@@ -76,11 +76,11 @@ export function generateOfficialBundleCatalog(root: string, names: readonly stri
|
||||
/**
|
||||
* Render a source-plane catalog with no package imports or pinned runtime version.
|
||||
* @param entries - Validated metadata in catalog order.
|
||||
* @returns Deterministic TypeScript source for app-boot's generated module.
|
||||
* @returns Deterministic TypeScript source for the official-bundles generated module.
|
||||
*/
|
||||
export function renderOfficialBundleCatalog(entries: readonly OfficialBundleCatalogEntry[]): string {
|
||||
return '/** Generated offline Official bundle metadata. Run pnpm gen-official-bundle-catalog; do not edit. */\n\n'
|
||||
+ "import type { OfficialBundleCatalogEntry } from './official-bundles.ts'\n\n"
|
||||
+ "import type { OfficialBundleCatalogEntry } from './index.ts'\n\n"
|
||||
+ '/** Official on-demand entries, independent of installed packages and registry availability. */\n'
|
||||
+ 'export const OFFICIAL_ON_DEMAND_CATALOG: readonly OfficialBundleCatalogEntry[] = [\n'
|
||||
+ entries.map(entry => ' {\n'
|
||||
|
||||
+33
-23
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import { globSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { basename, resolve } from 'node:path'
|
||||
import { basename, dirname, resolve } from 'node:path'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import LlmRuntime from '@deepseek-ai/dsh-llm'
|
||||
import type { ToolSchema } from '@deepseek-ai/dsh-llm'
|
||||
@@ -19,7 +19,11 @@ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
||||
import SqliteSessionQueryEngine from '@deepseek-ai/dsh-session-query-sqlite'
|
||||
import GoalService from '@deepseek-ai/dsh-goal'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRuntime, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRuntime from '@deepseek-ai/dsh-tools'
|
||||
import { NativeToolExecutor } from '@deepseek-ai/dsh-tool-executor-native'
|
||||
import { PtcExecutor } from '@deepseek-ai/dsh-tool-executor-ptc'
|
||||
import NodePtcRuntime from '@deepseek-ai/dsh-ptc-runtime-node'
|
||||
import LocalSandboxProvider from '@deepseek-ai/dsh-sandbox-local'
|
||||
import LocalBashExecutor from '@deepseek-ai/dsh-bash-local'
|
||||
import * as BashEnvPlugin from '@deepseek-ai/dsh-shell-env'
|
||||
import { PwshLocalExecutor } from '@deepseek-ai/dsh-pwsh-local'
|
||||
@@ -165,13 +169,8 @@ export interface ToolPackage {
|
||||
mount: (ctx: Context) => Promise<void>
|
||||
/** Agent-like scope key whose tool view is catalogued instead of the global view. */
|
||||
scope?: (ctx: Context) => Agent
|
||||
/**
|
||||
* Config for the caller's `ToolRuntime` mount. The registry itself ships a
|
||||
* model-facing tool (`run_code`, registered under a non-native `mode`), so
|
||||
* ITS catalog entry boots the registry in the mode that exposes it;
|
||||
* every other entry uses the default (native) registry.
|
||||
*/
|
||||
toolsConfig?: ToolsConfig
|
||||
/** Select the program transport when harvesting the PTC package. */
|
||||
executor?: 'ptc'
|
||||
/**
|
||||
* A deployment note rendered after the package's tools, for a fact that
|
||||
* booting the package alone cannot show. The registered tool NAME can be a
|
||||
@@ -254,18 +253,20 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
'ask_user_question keeps the original blocking behavior by default; set `mode: timed` to opt into a foreground timeout and pending result while the question remains answerable. In timed mode, `timeout: -1` keeps that call blocking indefinitely.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tools',
|
||||
dir: 'tools',
|
||||
source: 'packages/core/tools/src/ptc.ts',
|
||||
requires: ['ctx.workingDirectory (Agent executions)', 'ctx.tools', 'ctx.ptcRuntime (execution time)', 'ctx.systemPrompt'],
|
||||
pkg: '@deepseek-ai/dsh-tool-executor-ptc',
|
||||
dir: 'tool-executor-ptc',
|
||||
source: 'packages/core/tool-executor-ptc/src/ptc.ts',
|
||||
requires: ['ctx.workingDirectory (Agent executions)', 'ctx.tools', 'ctx.ptcRuntime', 'ctx.systemPrompt'],
|
||||
writes: ['tool/call', 'one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call', 'tool/result'],
|
||||
// The registry's OWN tool: run_code exists only under a non-native mode
|
||||
// (the registry registers it in its constructor; the PTC runtime is read
|
||||
// at assembly/execution time, so the schema harvest needs none mounted).
|
||||
toolsConfig: { mode: 'ptc' },
|
||||
async mount() {},
|
||||
executor: 'ptc',
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(LocalSubprocessRuntime)
|
||||
await ctx.plugin(LocalSandboxProvider)
|
||||
await ctx.plugin(SandboxPolicy)
|
||||
await ctx.plugin(NodePtcRuntime)
|
||||
},
|
||||
note:
|
||||
'Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: ptc` (see the PTC mode Agent Note). Under `ptc` it is the registry\'s only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime\'s language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.',
|
||||
'The PTC executor exposes only run_code to the model and declares other visible capabilities in a generated SDK in the runtime’s language. Program bindings enter the shared guarded tool pipeline in submission order; concurrency-safe bodies overlap up to maxParallelSubCalls, and nested results remain linked to the outer program.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-plan-mode',
|
||||
@@ -721,7 +722,7 @@ export type ToolCatalog = CatalogPackage[]
|
||||
|
||||
/**
|
||||
* Assert the boot manifest covers every shipped tool package on disk (a
|
||||
* `tool-*` leaf under `packages/`).
|
||||
* `tool-*` leaf under `packages/`, excluding test support).
|
||||
* Booting has no source declaration to enumerate, so this glob restores the
|
||||
* "a new tool cannot be silently undocumented" guarantee: an unlisted package
|
||||
* fails the generator (and the freshness gate) until it is added to
|
||||
@@ -730,7 +731,11 @@ export type ToolCatalog = CatalogPackage[]
|
||||
* `scanRoot` defaults to the repo root; a test may point it at a fixture tree.
|
||||
*/
|
||||
export function assertManifestComplete(packages: ToolPackage[] = TOOL_PACKAGES, scanRoot: string = root): void {
|
||||
const onDisk = globSync('packages/*/tool-*', { cwd: scanRoot }).map(p => basename(p)).sort()
|
||||
// Native execution owns no tool definitions; PTC contributes run_code.
|
||||
const onDisk = globSync('packages/*/tool-*', { cwd: scanRoot })
|
||||
.filter(path => basename(dirname(path)) !== 'test-support')
|
||||
.map(p => basename(p))
|
||||
.filter(dir => dir !== 'tool-executor-native').sort()
|
||||
const listed = new Set(packages.map(p => p.dir))
|
||||
const missing = onDisk.filter(dir => !listed.has(dir))
|
||||
if (missing.length > 0) {
|
||||
@@ -783,9 +788,14 @@ export async function collectToolCatalog(packages: ToolPackage[] = TOOL_PACKAGES
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(LocalFileSystem)
|
||||
await ctx.plugin(WorkingDirectory)
|
||||
await ctx.plugin(ToolRuntime, entry.toolsConfig ?? {})
|
||||
await ctx.plugin(ToolRuntime)
|
||||
const executor = entry.executor === 'ptc' ? new PtcExecutor(ctx) : new NativeToolExecutor(ctx)
|
||||
ctx.tools.setExecutor(executor)
|
||||
await entry.mount(ctx)
|
||||
const schemas = ctx.tools.schemas(entry.scope?.(ctx)).sort((a, b) => a.name.localeCompare(b.name))
|
||||
const scope = entry.scope?.(ctx)
|
||||
const described = executor.describe(ctx.tools.definitions(scope))
|
||||
const schemas = [...new Map([...ctx.tools.schemas(scope), ...described.schemas].map(schema => [schema.name, schema])).values()]
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
assertToolsHarvested(entry, schemas.length)
|
||||
catalog.push({
|
||||
pkg: entry.pkg,
|
||||
|
||||
@@ -47,9 +47,9 @@ describe('volatile Loader updates', () => {
|
||||
const fiber = await mountEntry(ctx, { Config, apply(owner: Context) {
|
||||
owner.on('loader/volatile-update', events)
|
||||
} }, { title: 'first' })
|
||||
const reference = fiber.config as ReturnType<typeof Config>
|
||||
const reference = fiber.effectiveConfig as ReturnType<typeof Config>
|
||||
await update(fiber, { title: 'second' })
|
||||
expect(fiber.config).toBe(reference)
|
||||
expect(fiber.effectiveConfig).toBe(reference)
|
||||
expect(reference.get()).toEqual({ title: 'second' })
|
||||
expect(events).toHaveBeenCalledExactlyOnceWith([[]])
|
||||
})
|
||||
@@ -70,7 +70,7 @@ describe('volatile Loader updates', () => {
|
||||
await update(first, { title: 'next', nested: { count: 2 } })
|
||||
await first.await()
|
||||
expect(mount).toHaveBeenCalledTimes(2)
|
||||
expect(first.config).toBe(initial)
|
||||
expect(first.effectiveConfig).toBe(initial)
|
||||
expect(initial.title).toBe(title)
|
||||
expect(configs[1]!.title.get()).toBe('hello')
|
||||
expect(observed).toEqual([{ paths: [['title'], ['nested', 'count']], title: 'next', count: 2 }])
|
||||
@@ -117,9 +117,9 @@ describe('volatile Loader updates', () => {
|
||||
hooks.push(config.title.get())
|
||||
})
|
||||
} }, {})
|
||||
const initial = fiber.config as ReturnType<typeof Config>
|
||||
const initial = fiber.effectiveConfig as ReturnType<typeof Config>
|
||||
await update(fiber, { title: 'accepted' })
|
||||
expect(fiber.config).toBe(initial)
|
||||
expect(fiber.effectiveConfig).toBe(initial)
|
||||
expect(initial.title.get()).toBe('accepted')
|
||||
await update(fiber, { title: 'accepted' })
|
||||
expect(hooks).toEqual([])
|
||||
@@ -180,11 +180,11 @@ describe('volatile Loader updates', () => {
|
||||
const oldUrl = 'https://a.example/'
|
||||
const newUrl = 'https://b.example/'
|
||||
const fiber = await mountEntry(ctx, { Config: schema, apply }, { live: 'a', url: oldUrl })
|
||||
const before = fiber.config as ReturnType<typeof schema>
|
||||
const before = fiber.effectiveConfig as ReturnType<typeof schema>
|
||||
await update(fiber, { live: 'b', url: newUrl })
|
||||
await fiber.await()
|
||||
expect(apply).toHaveBeenCalledTimes(2)
|
||||
expect((fiber.config as ReturnType<typeof schema>).url.href).toBe(newUrl)
|
||||
expect((fiber.effectiveConfig as ReturnType<typeof schema>).url.href).toBe(newUrl)
|
||||
expect(before.live.get()).toBe('a')
|
||||
await update(fiber, { live: 'c', url: newUrl, data: 'first' })
|
||||
await update(fiber, { live: 'd', url: newUrl, data: 'second' })
|
||||
@@ -199,7 +199,7 @@ describe('volatile Loader updates', () => {
|
||||
owner.on('loader/volatile-update', events)
|
||||
} }, { values: [undefined] })
|
||||
await update(fiber, { values: [null] })
|
||||
expect((fiber.config as ReturnType<typeof schema>).values.get()).toEqual([null])
|
||||
expect((fiber.effectiveConfig as ReturnType<typeof schema>).values.get()).toEqual([null])
|
||||
expect(events).toHaveBeenCalledExactlyOnceWith([['values']])
|
||||
})
|
||||
})
|
||||
@@ -308,7 +308,7 @@ it('updates volatile children through Group and Include custom updates and saves
|
||||
expect(includeEntry.fiber).toBe(originalInclude)
|
||||
expect(group.fiber).toBe(originalGroup)
|
||||
expect(consumer.fiber).toBe(originalConsumer)
|
||||
expect(consumer.fiber!.config).toBe(initial)
|
||||
expect(consumer.fiber!.effectiveConfig).toBe(initial)
|
||||
expect(configs).toHaveLength(1)
|
||||
expect(notifications).toHaveBeenCalledTimes(2)
|
||||
expect(childUpdates).not.toHaveBeenCalled()
|
||||
@@ -318,7 +318,7 @@ it('updates volatile children through Group and Include custom updates and saves
|
||||
entry.fiber!.update({ title: 'custom-update', fixed: 'ordinary' })
|
||||
expect(childUpdates).toHaveBeenCalledOnce()
|
||||
expect(entry.options.config).toEqual({ title: 'custom-update', fixed: 'ordinary' })
|
||||
expect((entry.fiber!.config as ReturnType<typeof Config>).title.get()).toBe('hello')
|
||||
expect((entry.fiber!.effectiveConfig as ReturnType<typeof Config>).title.get()).toBe('hello')
|
||||
entry.fiber!.update({ title: 'ephemeral', fixed: 'ordinary' }, true)
|
||||
expect(childUpdates).toHaveBeenCalledTimes(2)
|
||||
expect(entry.options.config).toEqual({ title: 'custom-update', fixed: 'ordinary' })
|
||||
@@ -332,11 +332,11 @@ it('commits volatile edits in place and retains direct Fiber updates', async ()
|
||||
const notifications = vi.fn()
|
||||
const apply = vi.fn(function (owner: Context) { owner.on('loader/volatile-update', notifications) })
|
||||
const fiber = await mountEntry(ctx, { Config, apply }, {})
|
||||
const original = fiber.config as ReturnType<typeof Config>
|
||||
const original = fiber.effectiveConfig as ReturnType<typeof Config>
|
||||
await ctx.loader.create({ name: 'cordis:disabled', disabled: true })
|
||||
await update(fiber, { title: 'enabled' })
|
||||
expect(apply).toHaveBeenCalledOnce()
|
||||
expect(fiber.config).toBe(original)
|
||||
expect(fiber.effectiveConfig).toBe(original)
|
||||
expect(original.title.get()).toBe('enabled')
|
||||
expect(notifications).toHaveBeenCalledExactlyOnceWith([['title']])
|
||||
await update(fiber, { title: 'invalid', nested: { count: -1 } })
|
||||
@@ -344,7 +344,7 @@ it('commits volatile edits in place and retains direct Fiber updates', async ()
|
||||
await update(fiber, { title: 'mixed', fixed: 'changed' })
|
||||
expect(apply).toHaveBeenCalledTimes(2)
|
||||
expect(original.title.get()).toBe('enabled')
|
||||
const current = fiber.config as ReturnType<typeof Config>
|
||||
const current = fiber.effectiveConfig as ReturnType<typeof Config>
|
||||
expect(current.title.get()).toBe('mixed')
|
||||
|
||||
const hook = vi.fn((_config: unknown, _noSave: boolean, next: () => void) => { next() })
|
||||
@@ -365,7 +365,7 @@ it('parses a volatile-only update through the config hook without patching the e
|
||||
const fiber = await mountEntry(ctx, { Config, apply(owner: Context) {
|
||||
owner.on('loader/volatile-update', () => { trace.push('notify') })
|
||||
} }, { block: { title: 'initial' } })
|
||||
const current = fiber.config as ReturnType<typeof Config>
|
||||
const current = fiber.effectiveConfig as ReturnType<typeof Config>
|
||||
const resolveHook = vi.fn((_raw: unknown, next: () => unknown) => next())
|
||||
fiber.ctx.on('internal/config', resolveHook)
|
||||
const contextPatch = vi.fn((_entry: unknown, next: () => void) => { next() })
|
||||
@@ -379,7 +379,7 @@ it('parses a volatile-only update through the config hook without patching the e
|
||||
expect(resolveHook).toHaveBeenCalledOnce()
|
||||
expect(trace).toEqual(['validate', 'notify'])
|
||||
expect(current.block.get()).toEqual({ title: 'changed' })
|
||||
expect(fiber._config).toBe(raw)
|
||||
expect(fiber.rawConfig).toBe(raw)
|
||||
})
|
||||
|
||||
it.each([true, false])('keeps old references untouched when isolation replaces a dependency (available: %s)', async (available) => {
|
||||
@@ -452,7 +452,7 @@ it.each(['group', 'include'] as const)('applies profile patches to a %s subtree
|
||||
await reconcileProfilePatches(ctx, patch('second'), 'volatile-test')
|
||||
expect(parent.fiber).toBe(parentFiber)
|
||||
expect(entry.fiber).toBe(childFiber)
|
||||
expect(entry.fiber!.config).toBe(original)
|
||||
expect(entry.fiber!.effectiveConfig).toBe(original)
|
||||
expect(original.title).toBe(reference)
|
||||
expect(reference.get()).toBe('second')
|
||||
expect(configs).toHaveLength(1)
|
||||
@@ -481,9 +481,9 @@ it('logs an invalid candidate and keeps its references while other entries commi
|
||||
await invalid.entry!.update({ config: { title: 'invalid', nested: { count: -1 } } })
|
||||
await healthy.entry!.update({ config: { title: 'healthy' } })
|
||||
expect(warning).toHaveBeenCalledTimes(2)
|
||||
expect((invalid.config as ReturnType<typeof Config>).title.get()).toBe('hello')
|
||||
expect(invalid._config).toEqual({ title: 'invalid', nested: { count: -1 } })
|
||||
expect((healthy.config as ReturnType<typeof Config>).title.get()).toBe('healthy')
|
||||
expect((invalid.effectiveConfig as ReturnType<typeof Config>).title.get()).toBe('hello')
|
||||
expect(invalid.rawConfig).toEqual({ title: 'invalid', nested: { count: -1 } })
|
||||
expect((healthy.effectiveConfig as ReturnType<typeof Config>).title.get()).toBe('healthy')
|
||||
expect(events).toHaveBeenCalledExactlyOnceWith([['title']])
|
||||
})
|
||||
|
||||
@@ -500,7 +500,7 @@ it.each([false, true])('retains raw expression edits for dependency restoration
|
||||
await fiber.entry!.update({ config: raw })
|
||||
await fiber.await()
|
||||
expect(values).toEqual(volatile ? ['same'] : ['same', 'same'])
|
||||
expect(fiber._config).toBe(raw)
|
||||
expect(fiber.rawConfig).toBe(raw)
|
||||
await provider.dispose()
|
||||
await provide('old-route', 'new-route')
|
||||
await fiber.await()
|
||||
@@ -540,10 +540,10 @@ it('compares ordinary URL transforms by value and remounts when an expression re
|
||||
const fixed = { __jsExpr: "ctx.get('volatile-transform-source').value" }
|
||||
const raw = { url: 'https://example.com/', fixed, live: 'old' }
|
||||
const fiber = await mountEntry(ctx, { Config: schema, apply: mount }, raw)
|
||||
const current = fiber.config as ReturnType<typeof schema>
|
||||
const current = fiber.effectiveConfig as ReturnType<typeof schema>
|
||||
await update(fiber, { ...raw, live: 'new' })
|
||||
expect(mount).toHaveBeenCalledOnce()
|
||||
expect(fiber.config).toBe(current)
|
||||
expect(fiber.effectiveConfig).toBe(current)
|
||||
expect(current.url.href).toBe(raw.url)
|
||||
expect(current.fixed).toBe('initial')
|
||||
expect(current.live.get()).toBe('new')
|
||||
@@ -554,7 +554,7 @@ it('compares ordinary URL transforms by value and remounts when an expression re
|
||||
expect(current.fixed).toBe('initial')
|
||||
expect(current.live.get()).toBe('new')
|
||||
expect(events).toHaveBeenCalledOnce()
|
||||
const replaced = fiber.config as ReturnType<typeof schema>
|
||||
const replaced = fiber.effectiveConfig as ReturnType<typeof schema>
|
||||
expect(replaced.fixed).toBe('changed')
|
||||
expect(replaced.live.get()).toBe('remounted')
|
||||
})
|
||||
@@ -568,13 +568,13 @@ it('remounts when an ordinary parent expression removes a live reference path',
|
||||
const mount = vi.fn(function (owner: Context) { owner.on('loader/volatile-update', events) })
|
||||
const raw = { block: { __jsExpr: "ctx.get('volatile-path-source').block" }, live: 'old' }
|
||||
const fiber = await mountEntry(ctx, { Config: schema, apply: mount }, raw)
|
||||
const current = fiber.config as ReturnType<typeof schema>
|
||||
const current = fiber.effectiveConfig as ReturnType<typeof schema>
|
||||
source.block = undefined
|
||||
await update(fiber, { ...raw, live: 'new' })
|
||||
expect(mount).toHaveBeenCalledTimes(2)
|
||||
expect(current.block.value.get()).toBe('old')
|
||||
expect(events).not.toHaveBeenCalled()
|
||||
expect((fiber.config as ReturnType<typeof schema>).block).toBeUndefined()
|
||||
expect((fiber.effectiveConfig as ReturnType<typeof schema>).block).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps an ordinary plugin mounted when an absent object becomes its explicit default', async () => {
|
||||
@@ -582,13 +582,13 @@ it('keeps an ordinary plugin mounted when an absent object becomes its explicit
|
||||
const Config = z.object({ nested: z.object({ fixed: z.string() }).default({ fixed: 'default' }) })
|
||||
const mount = vi.fn()
|
||||
const fiber = await mountEntry(ctx, { Config, apply: mount }, {})
|
||||
const config: unknown = fiber.config
|
||||
const config: unknown = fiber.effectiveConfig
|
||||
await update(fiber, { nested: { fixed: 'default' } })
|
||||
expect(fiber.config).toBe(config)
|
||||
expect(fiber.effectiveConfig).toBe(config)
|
||||
expect(mount).toHaveBeenCalledTimes(1)
|
||||
await update(fiber, { nested: { fixed: 'changed' } })
|
||||
expect(mount).toHaveBeenCalledTimes(2)
|
||||
expect(fiber.config).toEqual({ nested: { fixed: 'changed' } })
|
||||
expect(fiber.effectiveConfig).toEqual({ nested: { fixed: 'changed' } })
|
||||
})
|
||||
|
||||
it('keeps a noSave volatile value as the pending raw config across a no-op entry update', async () => {
|
||||
@@ -596,11 +596,11 @@ it('keeps a noSave volatile value as the pending raw config across a no-op entry
|
||||
const fiber = await mountEntry(ctx, { Config, apply() {} }, { title: 'saved' })
|
||||
fiber.update({ title: 'ephemeral' }, true)
|
||||
await fiber.await()
|
||||
expect((fiber.config as ReturnType<typeof Config>).title.get()).toBe('ephemeral')
|
||||
expect((fiber.effectiveConfig as ReturnType<typeof Config>).title.get()).toBe('ephemeral')
|
||||
expect(fiber.entry!.options.config).toEqual({ title: 'saved' })
|
||||
await fiber.entry!.update({ config: { title: 'saved' } })
|
||||
expect(fiber._config).toEqual({ title: 'ephemeral' })
|
||||
expect((fiber.config as ReturnType<typeof Config>).title.get()).toBe('ephemeral')
|
||||
expect(fiber.rawConfig).toEqual({ title: 'ephemeral' })
|
||||
expect((fiber.effectiveConfig as ReturnType<typeof Config>).title.get()).toBe('ephemeral')
|
||||
})
|
||||
|
||||
it('isolates a failing notification listener and dispatches with a Context receiver', async () => {
|
||||
@@ -613,11 +613,11 @@ it('isolates a failing notification listener and dispatches with a Context recei
|
||||
const warning = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})
|
||||
onTestFinished(() => { warning.mockRestore() })
|
||||
await update(fiber, { title: 'committed' })
|
||||
expect((fiber.config as ReturnType<typeof Config>).title.get()).toBe('committed')
|
||||
expect((fiber.effectiveConfig as ReturnType<typeof Config>).title.get()).toBe('committed')
|
||||
expect(receivers).toEqual([fiber])
|
||||
expect(warning).toHaveBeenCalledWith(expect.objectContaining({ message: 'listener boom' }))
|
||||
await update(other, { title: 'unaffected' })
|
||||
expect((other.config as ReturnType<typeof Config>).title.get()).toBe('unaffected')
|
||||
expect((other.effectiveConfig as ReturnType<typeof Config>).title.get()).toBe('unaffected')
|
||||
})
|
||||
|
||||
it('remounts instead of committing when an ordinary field parses into a fresh class instance', async () => {
|
||||
@@ -626,9 +626,9 @@ it('remounts instead of committing when an ordinary field parses into a fresh cl
|
||||
const schema = z.object({ handle: z.transform(z.string(), value => new Handle(value)), live: z.string().volatile() })
|
||||
const mount = vi.fn()
|
||||
const fiber = await mountEntry(ctx, { Config: schema, apply: mount }, { handle: 'h', live: 'old' })
|
||||
const before = fiber.config as ReturnType<typeof schema>
|
||||
const before = fiber.effectiveConfig as ReturnType<typeof schema>
|
||||
await update(fiber, { handle: 'h', live: 'new' })
|
||||
expect(mount).toHaveBeenCalledTimes(2)
|
||||
expect(before.live.get()).toBe('old')
|
||||
expect((fiber.config as ReturnType<typeof schema>).live.get()).toBe('new')
|
||||
expect((fiber.effectiveConfig as ReturnType<typeof schema>).live.get()).toBe('new')
|
||||
})
|
||||
@@ -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/app-boot/src/official-bundle-packages.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../packages/boot/official-bundles/src/bundles.ts'
|
||||
import { generateOfficialBundleCatalog } from './gen-official-bundle-catalog.ts'
|
||||
import {
|
||||
createRegistryManifest,
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
"187ad4724635a2d845592799c3a2600437c0d2fe17e0d1c3018018d5e8b0b863": 1
|
||||
},
|
||||
"apps/desktop/tests/update-dialog.spec.ts": {
|
||||
"3226b936a41e00f2cfd8b9563d3bfcc8eaef7cf04996abe74fb981169a07b91b": 1,
|
||||
"4000017f1f3dabd4bdf8eb91120cf4ff0dd09bad89d4683151f4785974d204a5": 1
|
||||
},
|
||||
"apps/desktop/tests/update-http-executor.spec.ts": {
|
||||
@@ -406,7 +405,6 @@
|
||||
"packages/client/modules/tests/node-half.client.spec.ts": {
|
||||
"621628e4e895b24171e82d1b3fa3ecb12970e8eb0dfccab8bacc8c5784188a28": 2,
|
||||
"6c4ace8d135ab45685d0152eefb0f8156bbf95f66cefedd9a1a8da081d3cb180": 1,
|
||||
"6d4d3e6035bfeaac7068a832f0d5c89f1112961164709c0eeb18dd8937d74cac": 1,
|
||||
"8eb8ea3592cbd4e828a33b2cbe2876089d3923738996fb2259ded563811a1a85": 1,
|
||||
"c4f0b7f87a16d26a1e4e7ef7a07991e789f0bbcfdbe2240a97fcd879668b5b3f": 1,
|
||||
"f549d88ac2b1db67fb33f18a024c116051ff83d3f8ad0433ba4ceedc679e9044": 2
|
||||
@@ -427,9 +425,6 @@
|
||||
"packages/client/ui-attachment/tests/attachment-rail.client.spec.tsx": {
|
||||
"b42ab24a43e0f7542bf1deb8f7ac3a76698d29e098f8dc575af6c08774987c32": 1
|
||||
},
|
||||
"packages/client/ui-attachment/tests/composer-attachments.client.spec.tsx": {
|
||||
"ab22ebe23f59b5aa76d42502a0421f3953d1d170e8a4f900a0c442daf6f1397d": 1
|
||||
},
|
||||
"packages/client/ui-chat/src/client/conversation-nodes/turn-process.ts": {
|
||||
"a7d8cc8d42e37ad5cc4882241492ac572ed8163c7fb8663180e893c7178e906f": 1
|
||||
},
|
||||
@@ -751,9 +746,6 @@
|
||||
"packages/client/ui-workflow-run/tests/workflow-run.client.spec.tsx": {
|
||||
"9f5585ad6cebba08252165f2dcc63c1e16101e0306fab754afb641b417e7789d": 1
|
||||
},
|
||||
"packages/client/ui-workspace/tests/animated-rows.client.spec.tsx": {
|
||||
"a898fd26a24bbfa72e00b9b825779a95c1d1c2bd447ebf8f21e8b2d38c3fb08c": 1
|
||||
},
|
||||
"packages/client/ui-workspace/tests/apply.client.spec.ts": {
|
||||
"b467270b0c2a95d6b29939f6baf4d5c8e5031e99a1d15437a3060f35735d8d37": 1
|
||||
},
|
||||
@@ -913,16 +905,29 @@
|
||||
"packages/core/system-prompt/src/index.ts": {
|
||||
"7fe58280287ff63d40e979e72329f975e11015a95e9a8eec207a5a78748d3c89": 1
|
||||
},
|
||||
"packages/core/tools/src/index.ts": {
|
||||
"6061de0a1e01547eb1abf9d4e1b5fa79fda112fa35dda980775491123413dcce": 1
|
||||
"packages/core/tool-executor-ptc/tests/ptc.spec.ts": {
|
||||
"52faf4558fcd91bd27dbdfa7e497843e8eeb75ea5f68ee231335018d5b3f2053": 1
|
||||
},
|
||||
"packages/core/tool-executor-ptc/tests/py-types.spec.ts": {
|
||||
"0d733b2c7c458f6a7c4d287ed741c321842339cbe5908a8720660883423474c0": 9,
|
||||
"23ebbf16be06365be256c1ceb77869576a026644f3514e1737030c4556849f4a": 1,
|
||||
"3ec1a0855f2cf4a5b3646725ea95c07addd33d5957ba6af530b944cd26f1ea8f": 1,
|
||||
"43155833aa226b066cf374829e013176403ddbd6acd313ccf90f6cfb92ceec06": 1,
|
||||
"5d1224b16a64bb138a708eb3e3e130df2ddf1f8f656617e3f1b3f2583250cf63": 1,
|
||||
"6aecd519658d0d2fb040f0289d6db823e3f6bbb26fcb2b118dc7f7f2c1d3eb06": 1,
|
||||
"9b2b04bd2ecde0c56b00ae050b721d56b3c29a1c834b982c9c2f6e62dbcd0a04": 1,
|
||||
"d334b0b55263b6cb2125317ce96b766155b11455d40d600590f5302f5e0543cc": 1,
|
||||
"d6650a0b52b5f7311afff190373b4a1cbd31ba44f9fa0dff09395f6f2901e32d": 1
|
||||
},
|
||||
"packages/core/tool-executor-ptc/tests/ts-types.spec.ts": {
|
||||
"054c87ca977d98749baf70bb5dcad1bb8d9e725d08a480b7804a181434633873": 1,
|
||||
"0d733b2c7c458f6a7c4d287ed741c321842339cbe5908a8720660883423474c0": 1,
|
||||
"84a5877f154b369cc0528281904d1dc18e20fd3ad74a26c939301ba0db20ae02": 1,
|
||||
"d80a3368311f07e812a76ff265125b072175f58b6003d9f96a37d4f3e70e3dec": 1
|
||||
},
|
||||
"packages/core/tools/src/schema.ts": {
|
||||
"62870121f1e1644b67c62606e94a375bb40f880cc88edf7cd6baab0249e9da46": 1
|
||||
},
|
||||
"packages/core/tools/src/testing.ts": {
|
||||
"5be3dd49608bd752ae1371aae8c8d748a07158f7a597e2506af79a621f74f0b3": 1,
|
||||
"6fe9024d30e0d4757a348b83d246925b3f5aeaf2e79a0eff4bf12f3c9fb5826b": 1
|
||||
},
|
||||
"packages/core/tools/tests/execution-mode.spec.ts": {
|
||||
"33dec0693f265044c830377981b51852b773e1a8a765e0b3398a4542c646537b": 1,
|
||||
"f9c9afc4ff66b96c59303ffa5475e26719ef101dc45504fee4b56145ff4be02c": 1
|
||||
@@ -937,20 +942,6 @@
|
||||
"c21022b1c0c54a213857769a5d72bc249dce83dd63c9d6f2370139bbea3288c9": 1,
|
||||
"d1250fc465aac30c8474c1ee43ecbafc0a8c072d5e607961da1295acc710a257": 1
|
||||
},
|
||||
"packages/core/tools/tests/ptc.spec.ts": {
|
||||
"52faf4558fcd91bd27dbdfa7e497843e8eeb75ea5f68ee231335018d5b3f2053": 1
|
||||
},
|
||||
"packages/core/tools/tests/py-types.spec.ts": {
|
||||
"0d733b2c7c458f6a7c4d287ed741c321842339cbe5908a8720660883423474c0": 9,
|
||||
"23ebbf16be06365be256c1ceb77869576a026644f3514e1737030c4556849f4a": 1,
|
||||
"3ec1a0855f2cf4a5b3646725ea95c07addd33d5957ba6af530b944cd26f1ea8f": 1,
|
||||
"43155833aa226b066cf374829e013176403ddbd6acd313ccf90f6cfb92ceec06": 1,
|
||||
"5d1224b16a64bb138a708eb3e3e130df2ddf1f8f656617e3f1b3f2583250cf63": 1,
|
||||
"6aecd519658d0d2fb040f0289d6db823e3f6bbb26fcb2b118dc7f7f2c1d3eb06": 1,
|
||||
"9b2b04bd2ecde0c56b00ae050b721d56b3c29a1c834b982c9c2f6e62dbcd0a04": 1,
|
||||
"d334b0b55263b6cb2125317ce96b766155b11455d40d600590f5302f5e0543cc": 1,
|
||||
"d6650a0b52b5f7311afff190373b4a1cbd31ba44f9fa0dff09395f6f2901e32d": 1
|
||||
},
|
||||
"packages/core/tools/tests/schema.spec.ts": {
|
||||
"2994ddac72096564a65b1d11cfe50c5b5f21de0cca775ed7b758d031b9a4df99": 1,
|
||||
"7724a969f2b794fc521b1a246d444c75616cf27e6ce073b00e8c8956b626634d": 1,
|
||||
@@ -974,12 +965,6 @@
|
||||
"d3478c9f3a88906a17d5daa6d19c3694a33618cfc000c7c0ee8f3e16f2697322": 1,
|
||||
"f9c9afc4ff66b96c59303ffa5475e26719ef101dc45504fee4b56145ff4be02c": 2
|
||||
},
|
||||
"packages/core/tools/tests/ts-types.spec.ts": {
|
||||
"054c87ca977d98749baf70bb5dcad1bb8d9e725d08a480b7804a181434633873": 1,
|
||||
"0d733b2c7c458f6a7c4d287ed741c321842339cbe5908a8720660883423474c0": 1,
|
||||
"84a5877f154b369cc0528281904d1dc18e20fd3ad74a26c939301ba0db20ae02": 1,
|
||||
"d80a3368311f07e812a76ff265125b072175f58b6003d9f96a37d4f3e70e3dec": 1
|
||||
},
|
||||
"packages/credentials/credentials-local/tests/drain.spec.ts": {
|
||||
"a96f4a3497a1c661f76e47ef5fda3c32d2759a7a5f9c519527fa46801a91bf79": 1
|
||||
},
|
||||
@@ -1505,10 +1490,6 @@
|
||||
"cb524acd0159b942e7974d76b581b1f419b8653d1734946a3f154dda8e8f4762": 1,
|
||||
"f10c6a0a3a762be54c2de87ab5d787e37a21a89c9d468bf81d0658c2bf15d581": 1
|
||||
},
|
||||
"packages/session-query/session-log-export/tests/client-apply.client.spec.tsx": {
|
||||
"04984a2ce1486250236e638e0197da0a1a840855ed3975c59130a85358c7e5fe": 1,
|
||||
"eee35b4d8641824c6eaacfe03ed60d353d81d5257d1111132f34d46a7b13ef35": 1
|
||||
},
|
||||
"packages/session-query/session-log-export/tests/controller.client.spec.ts": {
|
||||
"746ebc5aade01a50cc9a74fc07b1023addf021aef80b62c069a437ac140c9e64": 1,
|
||||
"c920d88f6eb309ef5991608642a1ab0fd972690538b33c34d86c68d07ddd6edf": 1
|
||||
@@ -1516,10 +1497,6 @@
|
||||
"packages/session-query/session-log-export/tests/dialog.client.spec.tsx": {
|
||||
"a9b1969d76db0b05f87c862956f8ff45c52f1f4a5b32a206339de0c9fee99a77": 1
|
||||
},
|
||||
"packages/session-query/session-log-export/tests/header-action.client.spec.tsx": {
|
||||
"238a292bbe3a61acbc4f2d96fff474df5fba1577357219a88c914813786b56a7": 1,
|
||||
"47d3a14a42ef7142ea22071677b42265f02fba5a2a0cf4210f4f9c7c5a876d10": 1
|
||||
},
|
||||
"packages/session-query/session-log-export/tests/loader-composition.host.spec.ts": {
|
||||
"59c63f734e8bba862325f776e3fcbebd78a84cfb4161f8df41c216950d9bf351": 1,
|
||||
"e12d49327a6708ae3ace371e28ed0deb471dc8895c584ba94a86af344c15cec3": 1
|
||||
|
||||
@@ -8,7 +8,8 @@ import { describe, expect, it, onTestFinished } from 'vitest'
|
||||
import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import { loadOverlayPatches } from '../packages/boot/app-boot/src/index.ts'
|
||||
import { readPluginMeta } from '../packages/boot/app-boot/src/package-meta.ts'
|
||||
import { OPTIONAL_BUNDLES, PROFILE_TEMPLATES, bundlePatchPaths, composeEntries, initProfile, readProfileManifest } from '../packages/boot/app-boot/src/profile.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 type { DshBundleManifest } from '../packages/util/package-manifest/src/types.ts'
|
||||
import { createPluginProfile } from '../apps/desktop/src/project-manager.ts'
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface ProductPackagePolicy {
|
||||
* Optional integrations may be reached by an optional bundle without becoming default capabilities.
|
||||
*/
|
||||
export const PRODUCT_PACKAGE_POLICY: Readonly<Record<string, ProductPackagePolicy>> = {
|
||||
'packages/core/agent-tool-presentation': { category: 'optional', reason: 'Deprecated selector retained for existing executor configurations.' },
|
||||
'packages/browser-use/browser-use': { category: 'optional', reason: 'Service definition used by explicitly selected browser providers.' },
|
||||
'packages/computer-use/computer-use': { category: 'optional', reason: 'Service definition used by explicitly selected computer providers.' },
|
||||
'packages/lsp/lsp': { category: 'optional', reason: 'Service definition for deployment-configured language servers.' },
|
||||
@@ -38,6 +39,7 @@ export const PRODUCT_PACKAGE_POLICY: Readonly<Record<string, ProductPackagePolic
|
||||
'packages/sdk/client': { category: 'sdk', reason: 'Published TypeScript embedding client runs in the consumer process.' },
|
||||
'packages/typert/generator': { category: 'build', reason: 'Build-time generation of type metadata.' },
|
||||
'packages/test-support/agent-loop-testkit': { category: 'test', reason: 'Published agent-loop test harness.' },
|
||||
'packages/test-support/tool-testkit': { category: 'test', reason: 'Shared tool definition and execution fixtures for tests and benchmarks.' },
|
||||
'packages/test-support/client-runtime': { category: 'test', reason: 'Published browser plugin test harness.' },
|
||||
'packages/test-support/llm-mock-server': { category: 'test', reason: 'Published model-provider test server.' },
|
||||
'packages/test-support/llm-replay': { category: 'test', reason: 'Published recorded-model replay harness.' },
|
||||
|
||||
@@ -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/app-boot/src/official-bundles.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../../packages/boot/official-bundles/src/bundles.ts'
|
||||
import { releaseFamily, type ReleaseMember } from './families.ts'
|
||||
import { compareVersions, nextVendorVersion, planShared, reachesPayload } from './bump.ts'
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
readClientBuildRecord,
|
||||
} from '../client-build-environment.ts'
|
||||
import { validateTarballPayload } from '../publication-payload.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../../packages/boot/app-boot/src/official-bundle-packages.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../../packages/boot/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/app-boot/src/profile.ts'
|
||||
import { OPTIONAL_BUNDLES } from '../../packages/boot/official-bundles/src/bundles.ts'
|
||||
|
||||
interface InstalledManifest {
|
||||
name: string
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
/** Recorded npm evidence stays intact while authored files and exact edits remain checked. */
|
||||
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { exactEditState, isRescopeExcluded } from './rescope-vendor.ts'
|
||||
import { exactEditState, isRescopeExcluded, rescopeText } from './rescope-vendor.ts'
|
||||
|
||||
const ANCHOR = '\n## Sync procedure'
|
||||
const INSERTED = `\n15. **rescope**: one log entry.\n${ANCHOR}`
|
||||
|
||||
function expectRoundTrip(file: string, text: string, scoped: string, lines: number): void {
|
||||
expect(rescopeText(text, file)).toEqual({ text: scoped, lines })
|
||||
expect(rescopeText(scoped, file)).toEqual({ text: scoped, lines: 0 })
|
||||
expect(rescopeText(scoped, file, true)).toEqual({ text, lines })
|
||||
}
|
||||
|
||||
describe('rescope file selection', () => {
|
||||
it('preserves the recorded npm resolution', () => {
|
||||
expect(isRescopeExcluded('scripts/dependency-catalog/package-lock.json')).toBe(true)
|
||||
@@ -51,3 +58,141 @@ describe('exactEditState', () => {
|
||||
expect(exactEditState('x\n', 'a = 1', 'b = 2', 1)).toBe('invalid')
|
||||
})
|
||||
})
|
||||
|
||||
describe('product identifiers', () => {
|
||||
it.each([
|
||||
'apps/cli/tests/optional-bundle-transitions.e2e.ts',
|
||||
'apps/web/tests/agent-preset-selection.e2e.ts',
|
||||
'apps/web/tests/developer-tools-settings.e2e.ts',
|
||||
'docs/subsystems/schedule.md',
|
||||
'docs/subsystems/schedule.zh.md',
|
||||
'docs/upgrade-guide/v0.2.0-rc.2/schedule-bundle-retired/guide.md',
|
||||
'docs/upgrade-guide/v0.2.0-rc.2/schedule-bundle-retired/guide.zh.md',
|
||||
'docs/upgrade-guide/v0.2.1-alpha.1/subagent-preset-composition/guide.md',
|
||||
'docs/upgrade-guide/v0.2.1-alpha.1/subagent-preset-composition/guide.zh.md',
|
||||
'docs/user/guide/schedule.md',
|
||||
'docs/user/guide/schedule.zh.md',
|
||||
'packages/bundle/web-app/cordis.patch.yml',
|
||||
'packages/client/ui-agent-preset/src/client/CreatePluginMenuItem.tsx',
|
||||
'packages/client/ui-agent-preset/src/client/analytics.ts',
|
||||
'packages/client/ui-agent-preset/tests/analytics.client.spec.ts',
|
||||
'packages/client/ui-agent-preset/tests/components.client.spec.tsx',
|
||||
'packages/client/ui-agent-preset/tests/create-plugin-menu-item.client.spec.tsx',
|
||||
'packages/client/ui-agent-preset/tests/section-store.client.spec.ts',
|
||||
'packages/client/ui-conversation/src/client/input/submission-analytics.ts',
|
||||
'packages/client/ui-conversation/tests/service-orchestration.client.spec.ts',
|
||||
'packages/client/ui-entry-customization/src/client/index.ts',
|
||||
'packages/client/ui-entry-customization/tests/menu.client.spec.tsx',
|
||||
'packages/client/ui-workspace/tests/workspaces-service.client.spec.ts',
|
||||
'packages/extensions/cordis-host-runner/tests/inspect-registry.spec.ts',
|
||||
'snapshots/session/cordis-inspect-liveness/client-fixture.mjs',
|
||||
'snapshots/session/cordis-inspect-timeout/client-fixture.mjs',
|
||||
])('preserves the product values in %s', (file) => {
|
||||
const text = readFileSync(new URL(`../${file}`, import.meta.url), 'utf8')
|
||||
expect(text).toMatch(/(['"`])cordis(?:\/inspect-query(?:-resolved)?)?\1/)
|
||||
expect(isRescopeExcluded(file)).toBe(false)
|
||||
expect(rescopeText(text, file)).toEqual({ text, lines: 0 })
|
||||
})
|
||||
|
||||
it('still renames other libraries in a preset consumer and reverses them', () => {
|
||||
const file = 'packages/client/ui-agent-preset/src/client/CreatePluginMenuItem.tsx'
|
||||
const text = "const preset = 'cordis'\nimport utils from 'cosmokit'\nimport Schema from 'schemastery'\n"
|
||||
const scoped = "const preset = 'cordis'\nimport utils from '@deepseek-ai/cosmokit'\nimport Schema from '@deepseek-ai/schemastery'\n"
|
||||
expect(rescopeText(text, file)).toEqual({ text: scoped, lines: 2 })
|
||||
expect(rescopeText(scoped, file)).toEqual({ text: scoped, lines: 0 })
|
||||
expect(rescopeText(scoped, file, true)).toEqual({ text, lines: 2 })
|
||||
})
|
||||
|
||||
it('keeps unlisted package references subject to residue detection', () => {
|
||||
const file = 'packages/example/src/index.ts'
|
||||
const text = "import { Context } from 'cordis'\nimport type Schema from 'schemastery'\n"
|
||||
const scoped = "import { Context } from '@deepseek-ai/cordis'\nimport type Schema from '@deepseek-ai/schemastery'\n"
|
||||
expect(rescopeText(text, file)).toEqual({ text: scoped, lines: 2 })
|
||||
expect(rescopeText(scoped, file)).toEqual({ text: scoped, lines: 0 })
|
||||
expect(rescopeText(scoped, file, true)).toEqual({ text, lines: 2 })
|
||||
expect(rescopeText("ctx.emit('cordis/inspect-query')", file).text).toBe("ctx.emit('@deepseek-ai/cordis/inspect-query')")
|
||||
})
|
||||
|
||||
it.each([
|
||||
['import', "import { Context } from 'cordis'", "import { Context } from '@deepseek-ai/cordis'"],
|
||||
['multiline import', "import {\n Context,\n} from 'cordis'", "import {\n Context,\n} from '@deepseek-ai/cordis'"],
|
||||
['side-effect import', "import 'cordis'", "import '@deepseek-ai/cordis'"],
|
||||
['export', "export { Context } from 'cordis/subpath'", "export { Context } from '@deepseek-ai/cordis/subpath'"],
|
||||
['import equals', "import Cordis = require('cordis')", "import Cordis = require('@deepseek-ai/cordis')"],
|
||||
['dynamic import', 'const cordis = import(`cordis`)', 'const cordis = import(`@deepseek-ai/cordis`)'],
|
||||
['require', 'const cordis = require("cordis/subpath")', 'const cordis = require("@deepseek-ai/cordis/subpath")'],
|
||||
['import type', "type Context = import('cordis').Context", "type Context = import('@deepseek-ai/cordis').Context"],
|
||||
['module declaration', "declare module 'cordis' {}", "declare module '@deepseek-ai/cordis' {}"],
|
||||
['JSDoc type', "/** @param {import('cordis').Context} ctx */\nfunction apply(ctx) {}", "/** @param {import('@deepseek-ai/cordis').Context} ctx */\nfunction apply(ctx) {}"],
|
||||
['JSDoc import', "/** @import { Context } from 'cordis' */\nfunction apply(ctx) {}", "/** @import { Context } from '@deepseek-ai/cordis' */\nfunction apply(ctx) {}"],
|
||||
['same-line imports', "import 'cordis'; import 'cordis/subpath'", "import '@deepseek-ai/cordis'; import '@deepseek-ai/cordis/subpath'"],
|
||||
])('rewrites %s without changing same-file product data', (_label, source, target) => {
|
||||
const file = 'packages/client/ui-agent-preset/src/client/CreatePluginMenuItem.tsx'
|
||||
const product = "const preset = 'cordis'\nctx.emit('cordis/inspect-query')\nconst fixture = \"import 'cordis'\"\n"
|
||||
const text = `${product}${source}\n`
|
||||
const scoped = `${product}${target}\n`
|
||||
expectRoundTrip(file, text, scoped, 1)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['apps/cli/tests/optional-bundle-transitions.e2e.ts', 1],
|
||||
['packages/client/ui-conversation/src/client/input/submission-analytics.ts', 1],
|
||||
['packages/client/ui-conversation/tests/service-orchestration.client.spec.ts', 1],
|
||||
['packages/client/ui-entry-customization/src/client/index.ts', 1],
|
||||
['packages/client/ui-entry-customization/tests/menu.client.spec.tsx', 1],
|
||||
['packages/client/ui-workspace/tests/workspaces-service.client.spec.ts', 1],
|
||||
['packages/extensions/cordis-host-runner/tests/inspect-registry.spec.ts', 2],
|
||||
['snapshots/session/cordis-inspect-liveness/client-fixture.mjs', 1],
|
||||
['snapshots/session/cordis-inspect-timeout/client-fixture.mjs', 2],
|
||||
] as const)('reverses real package references in %s and restores them', (file, lines) => {
|
||||
const scoped = readFileSync(new URL(`../${file}`, import.meta.url), 'utf8')
|
||||
const text = scoped.replaceAll("'@deepseek-ai/cordis'", "'cordis'")
|
||||
expect(text).not.toBe(scoped)
|
||||
expectRoundTrip(file, text, scoped, lines)
|
||||
expect(rescopeText(text, file, true)).toEqual({ text, lines: 0 })
|
||||
})
|
||||
|
||||
it('rewrites multiline fenced imports while preserving prose and fenced product data', () => {
|
||||
const file = 'docs/subsystems/schedule.md'
|
||||
const text = "The `cordis` preset.\n```ts\nconst preset = 'cordis'\nimport {\n Context,\n} from 'cordis'\n```\n"
|
||||
const scoped = "The `cordis` preset.\n```ts\nconst preset = 'cordis'\nimport {\n Context,\n} from '@deepseek-ai/cordis'\n```\n"
|
||||
expectRoundTrip(file, text, scoped, 1)
|
||||
})
|
||||
|
||||
it.each(['cordis', "'cordis'", '"cordis/subpath"'])('rewrites the YAML name %s without changing product comments', (name) => {
|
||||
const text = `# The 'cordis' preset.\n- name: ${name} # 'cordis' stays product data\n`
|
||||
const scoped = `# The 'cordis' preset.\n- name: ${name.replace('cordis', '@deepseek-ai/cordis')} # 'cordis' stays product data\n`
|
||||
for (const [file, wrap] of [
|
||||
['packages/bundle/web-app/cordis.patch.yml', (value: string) => value],
|
||||
['docs/subsystems/schedule.md', (value: string) => `The \`cordis\` preset.\n\`\`\`yaml\n${value}\`\`\`\n`],
|
||||
] as const) {
|
||||
expectRoundTrip(file, wrap(text), wrap(scoped), 1)
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['scripts/rescope-vendor.spec.ts', 'scripts/gen-third-party-notices.spec.ts'])('keeps both package spellings in the fixture owner %s', (file) => {
|
||||
const text = "import 'cordis'\nimport '@deepseek-ai/cordis'\nconst fixture = \"import 'cosmokit'\"\n"
|
||||
expect(rescopeText(text, file)).toEqual({ text, lines: 0 })
|
||||
expect(rescopeText(text, file, true)).toEqual({ text, lines: 0 })
|
||||
})
|
||||
|
||||
it('keeps schema runtime identifiers while rewriting their same-file import', () => {
|
||||
const file = 'vendor/loader/src/config/diff.ts'
|
||||
const text = "const key = Symbol.for('schemastery')\nconst metadata = { vendor: 'schemastery' }\nimport Schema from 'schemastery'\n"
|
||||
const scoped = "const key = Symbol.for('schemastery')\nconst metadata = { vendor: 'schemastery' }\nimport Schema from '@deepseek-ai/schemastery'\n"
|
||||
expectRoundTrip(file, text, scoped, 1)
|
||||
})
|
||||
|
||||
it('rewrites declarations in an older event-name exception', () => {
|
||||
const file = 'packages/extensions/cordis-host-runner/src/index.ts'
|
||||
const text = "ctx.emit('cordis/inspect-query')\nimport { Context } from 'cordis'\ndeclare module 'cordis' {}\n"
|
||||
const scoped = "ctx.emit('cordis/inspect-query')\nimport { Context } from '@deepseek-ai/cordis'\ndeclare module '@deepseek-ai/cordis' {}\n"
|
||||
expectRoundTrip(file, text, scoped, 2)
|
||||
})
|
||||
|
||||
it.each(['```ts\n```\n', "```ts\nimport 'cordis'\n"])('keeps fence line counts without requiring a closing fence', (text) => {
|
||||
const file = 'docs/subsystems/schedule.md'
|
||||
const scoped = text.replace("import 'cordis'", "import '@deepseek-ai/cordis'")
|
||||
expectRoundTrip(file, text, scoped, scoped === text ? 0 : 1)
|
||||
})
|
||||
})
|
||||
+129
-33
@@ -12,9 +12,9 @@
|
||||
* `cordis.yml`, the Loader's `cordis:` builtin prefix, `cordis-config-entry`,
|
||||
* `@deepseek-ai/dsh-tool-cordis`, and `cordiverse/cordis`, and makes the
|
||||
* rewrite idempotent because the scoped name's `cordis` is preceded by `/`.
|
||||
* Markdown follows the rename inside every fence, and in `docs/` prose too:
|
||||
* a tutorial that teaches an unresolvable name is wrong, while prose elsewhere
|
||||
* records what was true when it was written.
|
||||
* Markdown follows the token rule in fences and `docs/` prose. Audited product
|
||||
* names stay intact; their source module references and YAML plugin names still
|
||||
* follow the rename. Prose elsewhere records what was true when it was written.
|
||||
*
|
||||
* Sites the token rule cannot express (dot-notation access, unquoted object
|
||||
* keys, regex literals, the vendored-manifest table) are listed in
|
||||
@@ -31,6 +31,7 @@ import { execFileSync } from 'node:child_process'
|
||||
import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import ts from 'typescript'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
@@ -66,13 +67,14 @@ interface ExactEdit {
|
||||
}
|
||||
|
||||
/**
|
||||
* A file where an upstream name also appears as a vendor DIRECTORY name or an
|
||||
* upstream runtime identifier: the generic pass is disabled for the listed
|
||||
* names and {@link EXACT_EDITS} renames the real package-name occurrences.
|
||||
* A file where an upstream name also identifies runtime metadata, directories,
|
||||
* presets, events or locale keys. Its module references and YAML plugin names
|
||||
* still change; fixture owners retain both package spellings throughout.
|
||||
*/
|
||||
interface GenericSkip {
|
||||
readonly file: string
|
||||
readonly upstream: readonly string[]
|
||||
readonly fixture?: true
|
||||
}
|
||||
|
||||
const GENERIC_SKIPS: readonly GenericSkip[] = [
|
||||
@@ -83,8 +85,10 @@ const GENERIC_SKIPS: readonly GenericSkip[] = [
|
||||
// Native schema detection and its fixture share the vendored runtime's Symbol.for identifier.
|
||||
{ file: 'packages/boot/app-boot/src/config-schema/native.ts', upstream: ['schemastery'] },
|
||||
{ file: 'packages/boot/app-boot/tests/config-schema.spec.ts', upstream: ['schemastery'] },
|
||||
// The codemod's positive and negative fixtures contain both package spellings.
|
||||
{ file: 'scripts/rescope-vendor.spec.ts', upstream: RENAMES.map(rename => rename.upstream), fixture: true },
|
||||
// Asserts the vendored-manifest table, which gains an upstream-name column.
|
||||
{ file: 'scripts/gen-third-party-notices.spec.ts', upstream: RENAMES.map(rename => rename.upstream) },
|
||||
{ file: 'scripts/gen-third-party-notices.spec.ts', upstream: RENAMES.map(rename => rename.upstream), fixture: true },
|
||||
// `cordis` is also an agent-preset id, so in these files the bare name is
|
||||
// product data, not a package reference. Renaming it changed which preset
|
||||
// the creator flow stages and which id the roster reports.
|
||||
@@ -98,6 +102,30 @@ 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'] },
|
||||
// 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'] },
|
||||
{ file: 'apps/web/tests/developer-tools-settings.e2e.ts', upstream: ['cordis'] },
|
||||
{ file: 'docs/subsystems/schedule.md', upstream: ['cordis'] },
|
||||
{ file: 'docs/subsystems/schedule.zh.md', upstream: ['cordis'] },
|
||||
{ file: 'docs/upgrade-guide/v0.2.0-rc.2/schedule-bundle-retired/guide.md', upstream: ['cordis'] },
|
||||
{ file: 'docs/upgrade-guide/v0.2.0-rc.2/schedule-bundle-retired/guide.zh.md', upstream: ['cordis'] },
|
||||
{ file: 'docs/upgrade-guide/v0.2.1-alpha.1/subagent-preset-composition/guide.md', upstream: ['cordis'] },
|
||||
{ file: 'docs/upgrade-guide/v0.2.1-alpha.1/subagent-preset-composition/guide.zh.md', upstream: ['cordis'] },
|
||||
{ file: 'docs/user/guide/schedule.md', upstream: ['cordis'] },
|
||||
{ file: 'docs/user/guide/schedule.zh.md', upstream: ['cordis'] },
|
||||
{ file: 'packages/bundle/web-app/cordis.patch.yml', upstream: ['cordis'] },
|
||||
{ file: 'packages/client/ui-agent-preset/src/client/CreatePluginMenuItem.tsx', upstream: ['cordis'] },
|
||||
{ file: 'packages/client/ui-agent-preset/src/client/analytics.ts', upstream: ['cordis'] },
|
||||
{ file: 'packages/client/ui-agent-preset/tests/analytics.client.spec.ts', upstream: ['cordis'] },
|
||||
{ file: 'packages/client/ui-agent-preset/tests/components.client.spec.tsx', upstream: ['cordis'] },
|
||||
{ file: 'packages/client/ui-agent-preset/tests/create-plugin-menu-item.client.spec.tsx', upstream: ['cordis'] },
|
||||
{ file: 'packages/client/ui-agent-preset/tests/section-store.client.spec.ts', upstream: ['cordis'] },
|
||||
{ file: 'packages/client/ui-conversation/src/client/input/submission-analytics.ts', upstream: ['cordis'] },
|
||||
{ file: 'packages/client/ui-conversation/tests/service-orchestration.client.spec.ts', upstream: ['cordis'] },
|
||||
{ file: 'packages/client/ui-entry-customization/src/client/index.ts', upstream: ['cordis'] },
|
||||
{ file: 'packages/client/ui-entry-customization/tests/menu.client.spec.tsx', upstream: ['cordis'] },
|
||||
{ file: 'packages/client/ui-workspace/tests/workspaces-service.client.spec.ts', upstream: ['cordis'] },
|
||||
// The preset-roster loop names the `cordis` preset id, not a package.
|
||||
{ file: 'apps/cli/tests/windows-shell.spec.ts', upstream: ['cordis'] },
|
||||
// GROUP_ORDER holds `packages/<group>/` directory names, not package names.
|
||||
@@ -124,6 +152,10 @@ const GENERIC_SKIPS: readonly GenericSkip[] = [
|
||||
{ file: 'packages/extensions/tool-cordis/src/providers.ts', upstream: ['cordis'] },
|
||||
{ file: 'packages/extensions/ui-cordis/src/client/index.ts', upstream: ['cordis'] },
|
||||
{ file: 'packages/extensions/ui-cordis/src/client/inventory.ts', upstream: ['cordis'] },
|
||||
// Inspector fixtures use the same extension event domain.
|
||||
{ file: 'packages/extensions/cordis-host-runner/tests/inspect-registry.spec.ts', upstream: ['cordis'] },
|
||||
{ file: 'snapshots/session/cordis-inspect-liveness/client-fixture.mjs', upstream: ['cordis'] },
|
||||
{ file: 'snapshots/session/cordis-inspect-timeout/client-fixture.mjs', upstream: ['cordis'] },
|
||||
// `cordis/tree` is an Inspector observation topic, not a package subpath.
|
||||
{ file: 'packages/experimental/inspector/src/shared/bridge/messages/cordis.ts', upstream: ['cordis'] },
|
||||
{ file: 'packages/experimental/inspector/tests/cordis-query.host.spec.ts', upstream: ['cordis'] },
|
||||
@@ -470,7 +502,7 @@ function patterns(reverse: boolean): Pattern[] {
|
||||
.map(rename => ({
|
||||
...rename,
|
||||
token: new RegExp(`(['"\`])${escapeRegExp(rename.from)}((?:/[^'"\`\\s]*)?)\\1`, 'g'),
|
||||
yamlName: new RegExp(`^(\\s*(?:-\\s*)?name:[ \\t]+)${escapeRegExp(rename.from)}([ \\t]*(?:#.*)?)$`, 'gm'),
|
||||
yamlName: new RegExp(`^(\\s*(?:-\\s*)?name:[ \\t]+)(['"]?)${escapeRegExp(rename.from)}((?:/[^\\s'"]*)?)\\2([ \\t]*(?:#.*)?)$`, 'gm'),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -478,45 +510,109 @@ function skipped(file: string, pattern: Pattern): boolean {
|
||||
return GENERIC_SKIPS.some(skip => skip.file === file && skip.upstream.includes(pattern.upstream))
|
||||
}
|
||||
|
||||
function rewriteLine(line: string, file: string, all: readonly Pattern[]): string {
|
||||
function rewriteLine(line: string, file: string, all: readonly Pattern[], yaml = false): string {
|
||||
let out = line
|
||||
for (const pattern of all) {
|
||||
if (skipped(file, pattern)) continue
|
||||
out = out.replace(pattern.token, (_match, quote: string, subpath: string) => `${quote}${pattern.to}${subpath}${quote}`)
|
||||
out = out.replace(pattern.yamlName, (_match, prefix: string, suffix: string) => `${prefix}${pattern.to}${suffix}`)
|
||||
const preserve = skipped(file, pattern)
|
||||
if (!preserve) out = out.replace(pattern.token, (_match, quote: string, subpath: string) => `${quote}${pattern.to}${subpath}${quote}`)
|
||||
if (!preserve || yaml) {
|
||||
out = out.replace(pattern.yamlName, (_match, prefix: string, quote: string, subpath: string, suffix: string) => `${prefix}${quote}${pattern.to}${subpath}${quote}${suffix}`)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** Rewrite only module literals whose other same-file uses are product data. */
|
||||
function rewriteModules(text: string, file: string, all: readonly Pattern[], sourceName = file): string {
|
||||
const skip = GENERIC_SKIPS.find(entry => entry.file === file)
|
||||
if (!skip || skip.fixture) return text
|
||||
const active = all.filter(pattern => skip.upstream.includes(pattern.upstream))
|
||||
const source = ts.createSourceFile(sourceName, text, ts.ScriptTarget.Latest, true)
|
||||
const edits = new Map<number, { end: number; value: string }>()
|
||||
const visited = new Set<ts.Node>()
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (visited.has(node)) return
|
||||
visited.add(node)
|
||||
let literal: ts.Node | undefined
|
||||
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node) || ts.isJSDocImportTag(node)) {
|
||||
literal = node.moduleSpecifier
|
||||
} else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) {
|
||||
literal = node.moduleReference.expression
|
||||
} else if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
|
||||
literal = node.argument.literal
|
||||
} else if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(node.expression) && node.expression.text === 'require'))) {
|
||||
literal = node.arguments[0]
|
||||
} else if (ts.isModuleDeclaration(node)) {
|
||||
literal = node.name
|
||||
}
|
||||
if (literal && ts.isStringLiteralLike(literal)) {
|
||||
const start = literal.getStart(source) + 1
|
||||
const end = literal.getEnd() - 1
|
||||
const value = text.slice(start, end)
|
||||
const pattern = active.find(entry => value === entry.from || value.startsWith(`${entry.from}/`))
|
||||
if (pattern) edits.set(start, { end, value: pattern.to + value.slice(pattern.from.length) })
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
for (const doc of ts.getJSDocCommentsAndTags(node)) visit(doc)
|
||||
}
|
||||
visit(source)
|
||||
let out = text
|
||||
for (const [start, edit] of [...edits].sort(([left], [right]) => right - left)) {
|
||||
out = out.slice(0, start) + edit.value + out.slice(edit.end)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrite a file's eligible lines.
|
||||
*
|
||||
* Markdown splits in two. Every fence is code a reader copies or a
|
||||
* configuration they mount, so every fence follows the rename regardless of its
|
||||
* info string. Prose follows it only under `docs/`, where a sentence quoting
|
||||
* `` `cordis` `` teaches an unresolved package name; elsewhere
|
||||
* prose is a record of what was true when it was written, and the same spelling
|
||||
* can mean something else entirely — the Python SDK's `cordis` option, or the
|
||||
* unvendored `@cordisjs/plugin-http`.
|
||||
* Rewrite eligible source, fences and docs prose. Audited names in source and
|
||||
* TypeScript/JavaScript fences change only in module syntax; YAML names remain
|
||||
* eligible too. Other names retain the token rule. Prose outside docs is kept.
|
||||
*/
|
||||
function rewrite(text: string, file: string, all: readonly Pattern[]): { text: string; lines: number } {
|
||||
const markdown = file.endsWith('.md')
|
||||
const prose = markdown && file.startsWith('docs/')
|
||||
let insideFence = false
|
||||
let lines = 0
|
||||
const out = text.split('\n').map((line) => {
|
||||
const original = text.split('\n')
|
||||
let source = text
|
||||
if (!markdown && /\.(ts|tsx|js|mjs|cjs|tpl)$/.test(file)) source = rewriteModules(text, file, all)
|
||||
let fenceStart: number | undefined
|
||||
let language = ''
|
||||
const out = source.split('\n')
|
||||
const finishFence = (end: number): void => {
|
||||
if (fenceStart === undefined || fenceStart === end || !/^(?:[cm]?[jt]sx?|typescript|javascript)?$/.test(language)) return
|
||||
const extension = language === 'jsx' || language === 'tsx' ? 'tsx' : language === 'js' || language === 'javascript' || language === 'mjs' || language === 'cjs' ? 'js' : 'ts'
|
||||
const body = out.slice(fenceStart, end).join('\n')
|
||||
out.splice(fenceStart, end - fenceStart, ...rewriteModules(body, file, all, `fence.${extension}`).split('\n'))
|
||||
}
|
||||
for (const [index, line] of out.entries()) {
|
||||
if (markdown) {
|
||||
if (/^\s*```/.test(line)) {
|
||||
insideFence = !insideFence
|
||||
return line
|
||||
const fence = /^\s*```(\S*)/.exec(line)
|
||||
if (fence) {
|
||||
if (fenceStart === undefined) {
|
||||
fenceStart = index + 1
|
||||
language = fence[1] ?? ''
|
||||
} else {
|
||||
finishFence(index)
|
||||
fenceStart = undefined
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (!insideFence && !prose) return line
|
||||
if (fenceStart === undefined && !prose) continue
|
||||
}
|
||||
const next = rewriteLine(line, file, all)
|
||||
if (next !== line) lines += 1
|
||||
return next
|
||||
})
|
||||
return { text: out.join('\n'), lines }
|
||||
out[index] = rewriteLine(line, file, all, /\.ya?ml$/.test(file) || (fenceStart !== undefined && (language === 'yaml' || language === 'yml')))
|
||||
}
|
||||
if (fenceStart !== undefined) finishFence(out.length)
|
||||
return { text: out.join('\n'), lines: out.filter((line, index) => line !== original[index]).length }
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the CLI's generic package rename with the audited name exceptions.
|
||||
* @param text - Complete source text.
|
||||
* @param file - Repository-relative path with forward slash separators.
|
||||
* @param reverse - Restore upstream package names when true.
|
||||
* @returns Rewritten text and the number of changed lines.
|
||||
*/
|
||||
export function rescopeText(text: string, file: string, reverse = false): { text: string; lines: number } {
|
||||
return rewrite(text, file, patterns(reverse))
|
||||
}
|
||||
|
||||
function classify(file: string): string {
|
||||
|
||||
@@ -258,7 +258,8 @@ def write_advanced_profile_patch(
|
||||
) -> Path:
|
||||
"""Write the shared custom, snapshot, and restart profile patch."""
|
||||
return write_profile_patch(root, name, sessions, [
|
||||
{"id": "tools", "config": {"mode": mode}},
|
||||
{"id": "tool-executor-native", "disabled": mode != "native"},
|
||||
{"id": "tool-executor-ptc", "disabled": mode != "ptc"},
|
||||
{
|
||||
"id": "system-prompt",
|
||||
"config": {
|
||||
|
||||
@@ -2580,6 +2580,70 @@
|
||||
"childSessionId": "{{child-1}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "session.event",
|
||||
"payload": {
|
||||
"sessionId": "{{child-1}}",
|
||||
"event": {
|
||||
"type": "subagent/descriptor",
|
||||
"seq": 0,
|
||||
"time": 0,
|
||||
"data": {
|
||||
"version": 3,
|
||||
"mode": "continuable",
|
||||
"provider": "spawn",
|
||||
"label": "Check direct child",
|
||||
"agentProvider": "deepseek-official",
|
||||
"agentModel": "smoke-model",
|
||||
"agentReasoningEffort": "high"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "session.event",
|
||||
"payload": {
|
||||
"sessionId": "{{child-1}}",
|
||||
"event": {
|
||||
"type": "sandbox/mode",
|
||||
"seq": 1,
|
||||
"time": 0,
|
||||
"data": {
|
||||
"mode": "danger-full-access",
|
||||
"source": "delegation"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "session.event",
|
||||
"payload": {
|
||||
"sessionId": "{{child-1}}",
|
||||
"event": {
|
||||
"type": "approval/policy",
|
||||
"seq": 2,
|
||||
"time": 0,
|
||||
"data": {
|
||||
"policy": "never",
|
||||
"source": "delegation"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "session.event",
|
||||
"payload": {
|
||||
"sessionId": "{{child-1}}",
|
||||
"event": {
|
||||
"type": "permission/preset",
|
||||
"seq": 3,
|
||||
"time": 0,
|
||||
"data": {
|
||||
"preset": "danger-full-access"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "session.event",
|
||||
"payload": {
|
||||
@@ -3405,6 +3469,70 @@
|
||||
"childSessionId": "{{child-2}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "session.event",
|
||||
"payload": {
|
||||
"sessionId": "{{child-2}}",
|
||||
"event": {
|
||||
"type": "subagent/descriptor",
|
||||
"seq": 0,
|
||||
"time": 0,
|
||||
"data": {
|
||||
"version": 3,
|
||||
"mode": "continuable",
|
||||
"provider": "spawn",
|
||||
"label": "advanced-exe-snapshot child 1",
|
||||
"agentProvider": "deepseek-official",
|
||||
"agentModel": "smoke-model",
|
||||
"agentReasoningEffort": "high"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "session.event",
|
||||
"payload": {
|
||||
"sessionId": "{{child-2}}",
|
||||
"event": {
|
||||
"type": "sandbox/mode",
|
||||
"seq": 1,
|
||||
"time": 0,
|
||||
"data": {
|
||||
"mode": "danger-full-access",
|
||||
"source": "delegation"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "session.event",
|
||||
"payload": {
|
||||
"sessionId": "{{child-2}}",
|
||||
"event": {
|
||||
"type": "approval/policy",
|
||||
"seq": 2,
|
||||
"time": 0,
|
||||
"data": {
|
||||
"policy": "never",
|
||||
"source": "delegation"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "session.event",
|
||||
"payload": {
|
||||
"sessionId": "{{child-2}}",
|
||||
"event": {
|
||||
"type": "permission/preset",
|
||||
"seq": 3,
|
||||
"time": 0,
|
||||
"data": {
|
||||
"preset": "danger-full-access"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "session.event",
|
||||
"payload": {
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
"ask_user_question",
|
||||
"create_goal",
|
||||
"edit",
|
||||
"exit_plan_mode",
|
||||
"get_goal",
|
||||
"glob",
|
||||
"grep",
|
||||
@@ -110,7 +109,6 @@
|
||||
"ask_user_question",
|
||||
"create_goal",
|
||||
"edit",
|
||||
"exit_plan_mode",
|
||||
"get_goal",
|
||||
"glob",
|
||||
"grep",
|
||||
@@ -187,7 +185,6 @@
|
||||
"ask_user_question",
|
||||
"create_goal",
|
||||
"edit",
|
||||
"exit_plan_mode",
|
||||
"get_goal",
|
||||
"glob",
|
||||
"grep",
|
||||
|
||||
@@ -49,10 +49,12 @@ function evaluateCondition(expression: string, cancelled: boolean, results: stri
|
||||
}, { timeout: 1000 }) as boolean
|
||||
}
|
||||
|
||||
const verdicts = [['ci.yml', 'all-checks-passed'], ['ci-windows.yml', 'windows-checks-passed'], ['ci-python.yml', 'python-checks-passed']] as const
|
||||
|
||||
describe('master-only platform scheduling', () => {
|
||||
it.each(['success', 'failure', 'skipped', 'cancelled'])(
|
||||
'reports %s dependencies in active runs but never starts a cancelled-run verdict', (result) => {
|
||||
const aggregate = workflow('ci.yml').jobs['all-checks-passed']!
|
||||
it.each(verdicts.flatMap(([file, job]) => ['success', 'failure', 'skipped', 'cancelled'].map(result => [file, job, result] as const)))(
|
||||
'%s %s reports %s dependencies in active runs but never starts a cancelled-run verdict', (file, job, result) => {
|
||||
const aggregate = workflow(file).jobs[job]!
|
||||
const results = aggregate.needs!.map(() => 'success')
|
||||
results[0] = result
|
||||
const condition = aggregate.if as string
|
||||
@@ -72,8 +74,8 @@ describe('master-only platform scheduling', () => {
|
||||
expect(evaluateCondition(workflow('ci.yml').jobs['all-checks-passed']!.if as string, true, ['success'])).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps only Linux and Windows x64 runtimes in required PR CI', () => {
|
||||
const pr = workflow('ci.yml')
|
||||
it('keeps the required Python PR verdict and its two x64 targets in CI Python', () => {
|
||||
const pr = workflow('ci-python.yml')
|
||||
expect(Object.keys(pr.on)).toEqual(['pull_request'])
|
||||
expect(pr.jobs['python-runtime']).toMatchObject({
|
||||
if: "github.event_name == 'pull_request'",
|
||||
@@ -82,7 +84,7 @@ describe('master-only platform scheduling', () => {
|
||||
})
|
||||
expect(pr.jobs.windows).toBeUndefined()
|
||||
expect(JSON.stringify(pr.jobs)).not.toMatch(/wine-windows-gates|check:windows-wine/)
|
||||
const aggregate = pr.jobs['all-checks-passed']!
|
||||
const aggregate = pr.jobs['python-checks-passed']!
|
||||
expect(aggregate.needs).toContain('python-runtime')
|
||||
expect(aggregate.needs).not.toContain('windows')
|
||||
expect(aggregate.needs!.every(id => id in pr.jobs)).toBe(true)
|
||||
@@ -90,6 +92,9 @@ describe('master-only platform scheduling', () => {
|
||||
expect(aggregate.steps).toContainEqual(expect.objectContaining({
|
||||
if: "contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped')",
|
||||
}))
|
||||
for (const file of ['ci.yml', 'ci-windows.yml']) {
|
||||
expect(workflow(file).jobs['python-runtime']).toBeUndefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('runs all three deferred carriers on master pushes with fail-loud API credentials', () => {
|
||||
|
||||
@@ -924,12 +924,12 @@
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolOutputDefinition",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolDefinition",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
@@ -959,82 +959,97 @@
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolExecutionToken",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolExecutionInput",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolExecution",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolDispatchExecution",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolExecutionMode",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "PtcDispatchLog",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
"source": "packages/core/tool-executor-ptc/src/runtime-types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolRunContext",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolGuard",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolErrorInfo",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolRestriction",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolActivationContext",
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolRegistrationOptions",
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ResolvedToolSet",
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolFailure",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolExecutionSuccess",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolExecutionFailure",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolExecutionResult",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "PreToolDecision",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "PostToolDecision",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
"source": "packages/core/tools/src/contracts.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
@@ -1941,6 +1956,16 @@
|
||||
"symbol": "WebRoute",
|
||||
"source": "packages/host/webserver/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/web-server.md",
|
||||
"symbol": "WebResource",
|
||||
"source": "packages/host/webserver/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/web-server.md",
|
||||
"symbol": "WebResourceRoute",
|
||||
"source": "packages/host/webserver/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/web-server.md",
|
||||
"symbol": "Config",
|
||||
@@ -2497,6 +2522,31 @@
|
||||
"symbol": "PendingModule",
|
||||
"source": "packages/boot/hmr/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolModelInput",
|
||||
"source": "packages/core/tools/src/executor.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolBatchInput",
|
||||
"source": "packages/core/tools/src/executor.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolCallResult",
|
||||
"source": "packages/core/tools/src/executor.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolBatchResult",
|
||||
"source": "packages/core/tools/src/executor.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tools.md",
|
||||
"symbol": "ToolExecutor",
|
||||
"source": "packages/core/tools/src/executor.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/core.md",
|
||||
"symbol": "ModelSelection",
|
||||
|
||||
@@ -11,6 +11,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
bundleManifestPaths,
|
||||
bundlePluginDependencyErrors,
|
||||
exclusivePlaneRowIds,
|
||||
metadataExpressionErrors,
|
||||
packageTestFixtureDependencyErrors,
|
||||
packageTestPluginDependencyErrors,
|
||||
@@ -48,6 +49,17 @@ describe('verify-cordis-config metadata expressions', () => {
|
||||
})
|
||||
|
||||
describe('workspace Bundle discovery and product dependency closures', () => {
|
||||
it('allows scoped executors on both planes while retaining other plugin rows', () => {
|
||||
expect([...exclusivePlaneRowIds([{ insert: [
|
||||
{ id: 'native', name: '@deepseek-ai/dsh-tool-executor-native' },
|
||||
{ id: 'ptc', name: '@deepseek-ai/dsh-tool-executor-ptc' },
|
||||
{ id: 'tools', name: '@deepseek-ai/dsh-tools' },
|
||||
{ id: 'group', name: 'cordis:group', group: true, config: [
|
||||
{ id: 'tool-executor-ptc', name: '@deepseek-ai/dsh-tool-fs' },
|
||||
] },
|
||||
] }])]).toEqual(['tools', 'group', 'tool-executor-ptc'])
|
||||
})
|
||||
|
||||
it('discovers a Bundle outside packages/bundle from its manifest declaration', () => {
|
||||
const fixture = mkdtempSync(join(tmpdir(), 'dsh-bundle-discovery-'))
|
||||
try {
|
||||
|
||||
@@ -119,7 +119,8 @@ function validateClientHalvesDeclared(): string[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* No shipped agent preset may repeat a row the host composition still runs.
|
||||
* No shipped agent preset may repeat an unscoped row the host composition still runs.
|
||||
* Tool executors select through the mounting scope and may appear on both planes.
|
||||
*
|
||||
* A preset contributes what ONE session adds to the host's registries. A row
|
||||
* active on both planes is therefore mounted twice — once per process and once
|
||||
@@ -157,7 +158,7 @@ function validatePresetPlaneSeparation(): string[] {
|
||||
const active = new Set([...rowIds(hostFile), ...overlayRows].filter(id => !disabled.has(id)))
|
||||
for (const file of overlayFiles) {
|
||||
for (const definition of presetDefinitions(loadEntries(file))) {
|
||||
for (const id of collectRowIds(definition.plugins)) {
|
||||
for (const id of exclusivePlaneRowIds(definition.plugins)) {
|
||||
if (active.has(id)) problems.push(`${file}#${definition.id}: row "${id}" is also active in the host composition; a row belongs to exactly one plane`)
|
||||
}
|
||||
}
|
||||
@@ -178,10 +179,15 @@ function loadEntries(file: string): unknown[] {
|
||||
* @returns the declared ids.
|
||||
*/
|
||||
function rowIds(file: string): Set<string> {
|
||||
return collectRowIds(loadEntries(file))
|
||||
return exclusivePlaneRowIds(loadEntries(file))
|
||||
}
|
||||
|
||||
function collectRowIds(rows: unknown): Set<string> {
|
||||
/**
|
||||
* Collect rows that cannot be shared between the host and a preset.
|
||||
* @param rows - parsed Loader entries, including inserts and groups.
|
||||
* @returns ids excluding the executor plugins whose registrations are scope-owned.
|
||||
*/
|
||||
export function exclusivePlaneRowIds(rows: unknown): Set<string> {
|
||||
const ids = new Set<string>()
|
||||
const walk = (value: unknown): void => {
|
||||
if (isUnknownArray(value)) {
|
||||
@@ -189,7 +195,9 @@ function collectRowIds(rows: unknown): Set<string> {
|
||||
return
|
||||
}
|
||||
if (!isRecord(value)) return
|
||||
if (typeof value.id === 'string' && typeof value.name === 'string') ids.add(value.id)
|
||||
if (typeof value.id === 'string' && typeof value.name === 'string'
|
||||
&& value.name !== '@deepseek-ai/dsh-tool-executor-native'
|
||||
&& value.name !== '@deepseek-ai/dsh-tool-executor-ptc') ids.add(value.id)
|
||||
walk(value.insert)
|
||||
if (isCordisGroupEntry(value)) walk(value.config)
|
||||
}
|
||||
|
||||
@@ -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/app-boot/src/profile.ts'
|
||||
const profile = 'packages/boot/official-bundles/src/bundles.ts'
|
||||
const preset = 'packages/bundle/web-app/presets/standard.patch.yml'
|
||||
const patch = 'packages/bundle/base/cordis.patch.yml'
|
||||
|
||||
@@ -488,6 +488,6 @@ describe('default product isolation', () => {
|
||||
])('rejects absent, empty, or uninspectable default profile definitions', (source) => {
|
||||
const root = fixture()
|
||||
write(root, profile, source)
|
||||
expect(() => verifyDefaultProductIsolation(root)).toThrow('profile')
|
||||
expect(() => verifyDefaultProductIsolation(root)).toThrow(`${profile}: `)
|
||||
})
|
||||
})
|
||||
@@ -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/app-boot/src/profile.ts'
|
||||
const PROFILE_SOURCE = 'packages/boot/official-bundles/src/bundles.ts'
|
||||
const PRESET_PATTERN = 'packages/bundle/web-app/presets/*.patch.yml'
|
||||
const RUNTIME_SECTIONS = ['dependencies', 'optionalDependencies', 'peerDependencies'] as const
|
||||
|
||||
|
||||
@@ -30,12 +30,14 @@ interface SentenceContract {
|
||||
* so an absent section cannot be mistaken for forgotten documentation.
|
||||
*/
|
||||
const NO_MODEL_EXPERIENCE_SECTION: Readonly<Record<string, string>> = {
|
||||
'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/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/code-language': 'The package only maps a filename suffix to a syntax-highlighting language id; the read consumer owns the persisted hint.',
|
||||
}
|
||||
|
||||
@@ -111,6 +113,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/experimental/inspector-profile': { kind: 'none', reason: 'The bundle mounts developer inspection plugins without contributing model context.' },
|
||||
'packages/experimental/session-inspector': { kind: 'none', reason: 'The conversation view entries add no model-facing input.' },
|
||||
'packages/client/ui-slots': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/ui-file-drag': { kind: 'none', reason: 'Browser-side file transfers and transient feedback; registers nothing model-facing.' },
|
||||
'packages/client/ui-attachment': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/ui-primitives': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/store': { kind: 'none', reason: 'Browser-side state primitives; register nothing model-facing.' },
|
||||
@@ -224,6 +227,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/spill/spill-local': { kind: 'indirect', reason: 'The storage backend delegates model rendering to spill consumers.' },
|
||||
'packages/test-support/session-snapshot': { kind: 'none', reason: 'The test harness observes and normalizes transcripts without changing live requests.' },
|
||||
'packages/test-support/agent-loop-testkit': { kind: 'none', reason: 'The test helper mounts services but neither drives nor modifies model requests.' },
|
||||
'packages/test-support/tool-testkit': { kind: 'none', reason: 'Fixture helpers define and execute test tools without making model requests.' },
|
||||
'packages/test-support/loader-smoke': { kind: 'none', reason: 'The test harness submits an ordinary user task but delegates prompt and tool composition to the loaded tree.' },
|
||||
'packages/test-support/llm-mock-server': { kind: 'none', reason: 'The test server substitutes provider wire behavior without invoking a real model.' },
|
||||
'packages/test-support/llm-replay': { kind: 'none', reason: 'The keyless adapter invokes no provider model.' },
|
||||
|
||||
@@ -13,8 +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/app-boot/src/profile.ts'
|
||||
const catalog = 'packages/boot/app-boot/src/official-bundle-packages.ts'
|
||||
const profile = 'packages/boot/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 {
|
||||
@@ -29,10 +28,10 @@ function pkg(root: string, directory: string, name: string, fields: Record<strin
|
||||
}
|
||||
|
||||
function profiles(root: string, optional: string[] = [], onDemand: string[] = []): void {
|
||||
write(root, catalog, `export const ON_DEMAND_BUNDLES = ${JSON.stringify(onDemand)}\n`)
|
||||
write(root, profile, `export const PROFILE_TEMPLATES = ${JSON.stringify(Object.fromEntries(
|
||||
['web', 'acp', 'headless', 'sdk', 'sdk-minimal'].map(name => [name, { bundles: [base] }]),
|
||||
))}\nexport const DEFAULT_PROFILE_BUNDLES = ['${base}']\nexport const OPTIONAL_BUNDLES = ${JSON.stringify(optional)}\n`)
|
||||
))}\nexport const DEFAULT_PROFILE_BUNDLES = ['${base}']\nexport const OPTIONAL_BUNDLES = ${JSON.stringify(optional)}\n`
|
||||
+ `export const ON_DEMAND_BUNDLES = ${JSON.stringify(onDemand)}\n`)
|
||||
}
|
||||
|
||||
function fixture(): string {
|
||||
@@ -142,7 +141,7 @@ describe('product package use', () => {
|
||||
const root = fixture()
|
||||
pkg(root, candidateDir, candidate)
|
||||
const children = [{ name: candidate }]
|
||||
if (!initial) write(root, 'packages/boot/app-boot/src/included.yml', children)
|
||||
if (!initial) write(root, 'packages/boot/official-bundles/src/included.yml', children)
|
||||
write(root, patch, [{ insert: [{ name: core }, {
|
||||
name: 'cordis:include', disabled: true, config: { path: './included.yml', ...(initial ? { initial: children } : {}) },
|
||||
}] }])
|
||||
@@ -162,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/app-boot/src/included.yml', [{ id: 'child', name: candidate }])
|
||||
write(root, 'packages/boot/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/app-boot/src/included.yml', [])
|
||||
write(root, 'packages/boot/official-bundles/src/included.yml', [])
|
||||
expect(failures(root)).toContain('no product runtime use')
|
||||
})
|
||||
|
||||
|
||||
@@ -14,8 +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/app-boot/src/profile.ts'
|
||||
const ON_DEMAND_SOURCE = 'packages/boot/app-boot/src/official-bundle-packages.ts'
|
||||
const PROFILE_SOURCE = 'packages/boot/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[]>> = {
|
||||
@@ -188,11 +187,9 @@ export function verifyProductUse(
|
||||
const profileFile = ts.createSourceFile(profilePath, readFileSync(profilePath, 'utf8'), ts.ScriptTarget.Latest, true)
|
||||
const templates = literalDeclaration(profileFile, 'PROFILE_TEMPLATES')
|
||||
const fallback = stringList(literalDeclaration(profileFile, 'DEFAULT_PROFILE_BUNDLES'), PROFILE_SOURCE)
|
||||
const catalogPath = resolve(root, ON_DEMAND_SOURCE)
|
||||
const catalogFile = ts.createSourceFile(catalogPath, readFileSync(catalogPath, 'utf8'), ts.ScriptTarget.Latest, true)
|
||||
const optionals = [
|
||||
...stringList(literalDeclaration(profileFile, 'OPTIONAL_BUNDLES'), PROFILE_SOURCE),
|
||||
...stringList(literalDeclaration(catalogFile, 'ON_DEMAND_BUNDLES'), ON_DEMAND_SOURCE),
|
||||
...stringList(literalDeclaration(profileFile, 'ON_DEMAND_BUNDLES'), PROFILE_SOURCE),
|
||||
]
|
||||
if (!isRecord(templates)) throw new Error(`${PROFILE_SOURCE}: PROFILE_TEMPLATES must be a literal object`)
|
||||
for (const name of PROFILES) if (!isRecord(templates[name])) failures.push(`${PROFILE_SOURCE}: missing shipped ${name} profile`)
|
||||
|
||||
Reference in new issue
Block a user