mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge remote-tracking branch 'origin/master' into fix/node26-vitest-webstorage
# Conflicts: # scripts/run-gates.spec.ts # vitest.config.ts
This commit is contained in:
55
scripts/coverage-exempt.spec.ts
Normal file
55
scripts/coverage-exempt.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Mechanical guard for the coverage-exempt roster: each entry's positional
|
||||
* filter and exclude glob must select the same non-empty file set out of the
|
||||
* repository's spec inventory, so a renamed suite cannot silently fall out of
|
||||
* the uninstrumented gate while its exclude goes stale.
|
||||
*/
|
||||
|
||||
import { globSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { coverageExemptHeavySuites } from './coverage-exempt.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
/** The spec inventory mirrored from vitest.config.ts testIncludes. */
|
||||
const allSpecs = new Set([
|
||||
...globSync('packages/*/*/tests/**/*.spec.ts', { cwd: root }),
|
||||
...globSync('packages/*/*/tests/**/*.spec.tsx', { cwd: root }),
|
||||
...globSync('apps/*/tests/**/*.spec.ts', { cwd: root }),
|
||||
...globSync('examples/*/tests/**/*.spec.ts', { cwd: root }),
|
||||
...globSync('scripts/**/*.spec.ts', { cwd: root }),
|
||||
].map(path => path.replaceAll('\\', '/')))
|
||||
|
||||
function excludeMatches(exclude: string): string[] {
|
||||
return globSync(exclude, { cwd: root })
|
||||
.map(path => path.replaceAll('\\', '/'))
|
||||
.filter(path => allSpecs.has(path))
|
||||
.sort()
|
||||
}
|
||||
|
||||
function filterMatches(filter: string): string[] {
|
||||
return [...allSpecs].filter(spec => spec.startsWith(filter)).sort()
|
||||
}
|
||||
|
||||
describe('coverage-exempt roster', () => {
|
||||
it.each(coverageExemptHeavySuites.map(suite => [suite.filter, suite] as const))(
|
||||
'filter and exclude select the same non-empty spec set for %s',
|
||||
(_filter, suite) => {
|
||||
const fromExclude = excludeMatches(suite.exclude)
|
||||
const fromFilter = filterMatches(suite.filter)
|
||||
expect(fromExclude.length).toBeGreaterThan(0)
|
||||
expect(fromFilter).toEqual(fromExclude)
|
||||
},
|
||||
)
|
||||
|
||||
it('entries never overlap, so no suite is double-run or double-excluded', () => {
|
||||
const seen = new Map<string, string>()
|
||||
for (const suite of coverageExemptHeavySuites) {
|
||||
for (const spec of excludeMatches(suite.exclude)) {
|
||||
expect(seen.get(spec), `${spec} matched by ${seen.get(spec) ?? ''} and ${suite.exclude}`).toBeUndefined()
|
||||
seen.set(spec, suite.exclude)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
41
scripts/coverage-exempt.ts
Normal file
41
scripts/coverage-exempt.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Heavy suites the coverage aggregate runs uninstrumented in a parallel gate.
|
||||
* Membership contract: a suite qualifies only when every coverage-measured
|
||||
* file it executes in-process (`coverage.include` spans package src trees;
|
||||
* typert generator src is threshold-excluded in vitest.config.ts) is already
|
||||
* fully covered by other suites, so removing it from the instrumented run
|
||||
* changes no threshold outcome. The aggregate still runs every listed suite
|
||||
* plain beside the instrumented gate, so correctness signal is unchanged —
|
||||
* only the v8 instrumentation tax on compiler- and subprocess-heavy fixtures
|
||||
* is dropped.
|
||||
*/
|
||||
|
||||
/** One coverage-exempt suite: a Vitest CLI filter and its exclude glob. */
|
||||
export interface CoverageExemptSuite {
|
||||
/** Positional file filter selecting the suite in the uninstrumented gate. */
|
||||
readonly filter: string
|
||||
/** Exclude glob removing the suite from the instrumented gate. */
|
||||
readonly exclude: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to `1` by the instrumented coverage gate; vitest.config.ts then drops
|
||||
* the exempt suites from every project. CLI `--exclude` cannot express this:
|
||||
* it does not reach per-project include resolution.
|
||||
*/
|
||||
export const COVERAGE_EXEMPT_ENV = 'DSH_COVERAGE_EXEMPT_HEAVY'
|
||||
|
||||
/** Coverage-exempt heavy suites; keep filter and exclude selecting the same files. */
|
||||
export const coverageExemptHeavySuites: readonly CoverageExemptSuite[] = [
|
||||
// Whole-workspace compiler analysis per case — the lane's longest tail.
|
||||
// Generator src is threshold-excluded; tools-catalog's registry and
|
||||
// tool-cordis imports are fully covered by those packages' own tests.
|
||||
{
|
||||
filter: 'packages/typert/generator/tests/',
|
||||
exclude: 'packages/typert/generator/tests/**',
|
||||
},
|
||||
// Real child-process fixtures over scripts/ sources, which coverage never measures.
|
||||
{ filter: 'scripts/install-lefthook.spec.ts', exclude: 'scripts/install-lefthook.spec.ts' },
|
||||
{ filter: 'scripts/oxlint-contract.spec.ts', exclude: 'scripts/oxlint-contract.spec.ts' },
|
||||
{ filter: 'scripts/change-scope.spec.ts', exclude: 'scripts/change-scope.spec.ts' },
|
||||
]
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"AGENTS.md": 1765,
|
||||
"AGENTS.md": 1775,
|
||||
"docs/AGENTS.md": 1150,
|
||||
"docs/architecture.md": 1920,
|
||||
"docs/cordis-primer.md": 600,
|
||||
@@ -7,5 +7,5 @@
|
||||
"docs/testing.md": 1100,
|
||||
"examples/AGENTS.md": 310,
|
||||
"packages/AGENTS.md": 675,
|
||||
"packages/README.md": 905
|
||||
"packages/README.md": 920
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
MessageId: 'core.md',
|
||||
HookContext: 'core.md',
|
||||
SettleReason: 'core.md',
|
||||
AdapterRegistrationHandle: 'core.md',
|
||||
LlmCallConfig: 'core.md',
|
||||
LlmModelContext: 'core.md',
|
||||
LlmModelReasoningInfo: 'core.md',
|
||||
@@ -40,6 +41,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
LlmFailure: 'llm-streaming.md',
|
||||
LlmModelInfo: 'core.md',
|
||||
LlmProviderInfo: 'core.md',
|
||||
LlmConfigurableProvider: 'core.md',
|
||||
ResolvedRetryPolicy: 'llm-streaming.md',
|
||||
Message: 'core.md',
|
||||
MessageSource: 'core.md',
|
||||
@@ -190,7 +192,12 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
SettingsRegisterOptions: 'settings.md',
|
||||
SettingsScope: 'settings.md',
|
||||
SettingsDescriptor: 'settings.md',
|
||||
SettingsPathOp: 'settings.md',
|
||||
SettingsDescribeOptions: 'settings.md',
|
||||
SettingsUpdateSource: 'settings.md',
|
||||
CredentialRef: 'credentials.md',
|
||||
CredentialInfo: 'credentials.md',
|
||||
ResolvedCredential: 'credentials.md',
|
||||
AskUserQuestionAnswer: 'user-interaction.md',
|
||||
AskUserQuestionRequest: 'user-interaction.md',
|
||||
UserInteractionProvider: 'user-interaction.md',
|
||||
|
||||
@@ -158,8 +158,17 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
title: 'User-settings seam',
|
||||
mode: 'seam',
|
||||
implementations: ['settings-local'],
|
||||
consumers: [],
|
||||
note: 'Plugins register namespace schemas and resolve layered values; providers store the raw document. No production consumer is migrated yet.',
|
||||
consumers: ['llm-deepseek', 'llm-pi-ai', 'apiproxy'],
|
||||
note: 'Plugins register namespace schemas and resolve layered values; providers store the raw document. The LLM adapters register their entry config as the composition base under the user section; the web gateway serves redacted layered descriptors and writes the user layer.',
|
||||
},
|
||||
{
|
||||
key: 'credentials',
|
||||
pkg: 'credentials',
|
||||
title: 'Credential seam',
|
||||
mode: 'seam',
|
||||
implementations: ['credentials-local'],
|
||||
consumers: ['llm-deepseek', 'llm-pi-ai', 'apiproxy'],
|
||||
note: 'Configuration carries references to secrets; providers own the values. Consumers resolve per operation, so a rotated credential reaches the very next request; the web gateway exposes value-free views and write-only storage.',
|
||||
},
|
||||
{
|
||||
key: 'telemetry',
|
||||
|
||||
@@ -273,10 +273,10 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
// never depends on the host PATH. `ctx.spillStore` is optional (read via
|
||||
// ctx.get) and does not affect the schemas, so no spill backend is mounted.
|
||||
await ctx.plugin(CatalogSearchBashExecutor)
|
||||
await ctx.plugin(ToolFsSearch)
|
||||
await ctx.plugin(ToolFsSearch, { sampleOverCapGlobResults: true })
|
||||
},
|
||||
note:
|
||||
'glob and grep are conditional bash-backed discovery tools: they register only when ctx.bash can find `rg`, then run fixed ripgrep commands through ctx.bash as ordinary foreground calls (never background tasks). Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments.',
|
||||
'glob and grep are conditional bash-backed discovery tools: they register only when ctx.bash can find `rg`, then run fixed ripgrep commands through ctx.bash as ordinary foreground calls (never background tasks). The catalog uses `sampleOverCapGlobResults: true`; deployments must choose that behavior explicitly. Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-pty',
|
||||
|
||||
@@ -197,7 +197,7 @@ describe('docsPages locale routes', () => {
|
||||
const translated = rootPages.filter(page => page.contentLocale === 'zh-CN')
|
||||
const fallbacks = rootPages.filter(page => page.contentLocale === 'en-US')
|
||||
|
||||
expect(translated).toHaveLength(19)
|
||||
expect(translated).toHaveLength(20)
|
||||
expect(translated.every(page => page.source.endsWith('.zh.md'))).toBe(true)
|
||||
expect(fallbacks.map(page => page.source).sort()).toEqual([
|
||||
'docs/core-data-structures/commands.md',
|
||||
|
||||
@@ -152,31 +152,43 @@ describe('Node compatibility graph', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Node 24 consumer graph', () => {
|
||||
it('owns the eight-command pool and orders restored-artifact consumers', () => {
|
||||
describe('Node 24 lane ownership', () => {
|
||||
it('keeps the static lane source-only', () => {
|
||||
const subject = withPnpmEntrypoint(() => gatesForMode('ci-static'))
|
||||
|
||||
expect(subject.map(item => item.id)).not.toContain('build')
|
||||
expect(subject.map(item => item.id)).not.toContain('doc-typecheck')
|
||||
})
|
||||
|
||||
it('owns the build and orders its artifact consumers', () => {
|
||||
const subject = withPnpmEntrypoint(() => gatesForMode('ci-consumers'))
|
||||
|
||||
expect(defaultConcurrency('ci-consumers', subject.length, 4)).toEqual({
|
||||
workers: 8,
|
||||
workers: 10,
|
||||
source: 'ci-consumers gate count',
|
||||
})
|
||||
expect(subject.map(item => item.id)).toEqual([
|
||||
'lint-and-duplication',
|
||||
'build',
|
||||
'node-compat',
|
||||
'publint',
|
||||
'built-package-invariants',
|
||||
'lint-and-duplication',
|
||||
'snapshot',
|
||||
'web-snapshot',
|
||||
'publint',
|
||||
'doc-typecheck',
|
||||
'node-next-types',
|
||||
'built-package-invariants',
|
||||
'built-bin-smoke',
|
||||
])
|
||||
expect(subject.find(item => item.id === 'publint')?.needs).toBeUndefined()
|
||||
expect(subject.find(item => item.id === 'publint')?.needs).toEqual(['build'])
|
||||
expect(subject.find(item => item.id === 'built-package-invariants')?.needs).toEqual(['publint'])
|
||||
expect(subject.find(item => item.id === 'lint-and-duplication')?.needs).toEqual(['built-package-invariants'])
|
||||
for (const id of ['snapshot', 'web-snapshot', 'node-next-types', 'built-bin-smoke']) {
|
||||
for (const id of ['snapshot', 'web-snapshot', 'doc-typecheck', 'node-next-types', 'built-bin-smoke']) {
|
||||
expect(subject.find(item => item.id === id)?.needs).toEqual(['built-package-invariants'])
|
||||
}
|
||||
expect(subject.find(item => item.id === 'snapshot')?.env).toEqual({ DSH_EXAMPLE_MODE: 'lib' })
|
||||
expect(subject.find(item => item.id === 'doc-typecheck')?.env).toEqual({
|
||||
DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1',
|
||||
})
|
||||
expect(subject.find(item => item.id === 'web-snapshot')).toMatchObject({
|
||||
displayCommand: 'DSH_SNAPSHOT=replay pnpm run test:web:built',
|
||||
env: { DSH_SNAPSHOT: 'replay' },
|
||||
|
||||
@@ -9,6 +9,7 @@ import { spawn } from 'node:child_process'
|
||||
import { availableParallelism } from 'node:os'
|
||||
import { resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { COVERAGE_EXEMPT_ENV, coverageExemptHeavySuites } from './coverage-exempt.ts'
|
||||
|
||||
/** A named aggregate exposed by the gate runner. */
|
||||
export type Mode =
|
||||
@@ -195,14 +196,14 @@ export function gatesForMode(selected: Mode): Gate[] {
|
||||
case 'ci-linux-primary':
|
||||
return [...ciPrimaryGates(), webSnapshotGate(['built-package-invariants'])]
|
||||
case 'ci-static':
|
||||
return ciStaticGates()
|
||||
return ciStaticGates({ ownsBuild: false })
|
||||
case 'ci-lint':
|
||||
return [
|
||||
lintGate(),
|
||||
pnpmScript('duplication', 'duplication'),
|
||||
]
|
||||
case 'ci-coverage':
|
||||
return [coverageGate()]
|
||||
return coverageGates()
|
||||
case 'ci-snapshot':
|
||||
return [pnpmScript('build', 'build'), snapshotGate()]
|
||||
case 'ci-artifacts':
|
||||
@@ -248,7 +249,7 @@ function ciPrimaryGates(): Gate[] {
|
||||
pnpmScript('typecheck', 'typecheck'),
|
||||
lintGate(),
|
||||
pnpmScript('duplication', 'duplication'),
|
||||
coverageGate(),
|
||||
...coverageGates(),
|
||||
...nodeCompatSmokeGates(),
|
||||
snapshotGate(),
|
||||
...docSyncLeafGates(),
|
||||
@@ -300,16 +301,21 @@ function nodeCompatSmokeGates(): Gate[] {
|
||||
]
|
||||
}
|
||||
|
||||
function ciStaticGates(): Gate[] {
|
||||
function ciStaticGates(options: { ownsBuild: boolean }): Gate[] {
|
||||
return [
|
||||
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
|
||||
pnpmScript('constraints', 'constraints'),
|
||||
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
|
||||
pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }),
|
||||
pnpmScript('build', 'build'),
|
||||
...options.ownsBuild ? [pnpmScript('build', 'build')] : [],
|
||||
...docSyncLeafGates({
|
||||
docTypecheckNeeds: ['build'],
|
||||
docTypecheckEnv: { DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1' },
|
||||
includeDocTypecheck: options.ownsBuild,
|
||||
...options.ownsBuild
|
||||
? {
|
||||
docTypecheckNeeds: ['build'],
|
||||
docTypecheckEnv: { DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1' },
|
||||
}
|
||||
: {},
|
||||
docsBuildScript: 'docs:build:mpa',
|
||||
}),
|
||||
pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }),
|
||||
@@ -331,23 +337,28 @@ function ciArtifactGates(): Gate[] {
|
||||
}
|
||||
|
||||
function ciConsumerGates(): Gate[] {
|
||||
const publicArtifacts = ['publint']
|
||||
const restoredBuild = ['built-package-invariants']
|
||||
const builtTree = ['build']
|
||||
const validatedBuild = ['built-package-invariants']
|
||||
return [
|
||||
pnpmScript('build', 'build'),
|
||||
pnpmScript('node-compat', 'check:node-compat', { label: 'Node compatibility' }),
|
||||
pnpmScript('publint', 'publint', { needs: builtTree }),
|
||||
builtPackageInvariantsGate(['publint']),
|
||||
pnpmScript('lint-and-duplication', 'check:ci:lint', {
|
||||
label: 'lint and duplication',
|
||||
needs: restoredBuild,
|
||||
needs: validatedBuild,
|
||||
}),
|
||||
snapshotGate(validatedBuild),
|
||||
webSnapshotGate(validatedBuild),
|
||||
pnpmScript('doc-typecheck', 'doc-typecheck', {
|
||||
needs: validatedBuild,
|
||||
env: { DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1' },
|
||||
}),
|
||||
pnpmScript('node-compat', 'check:node-compat', { label: 'Node compatibility' }),
|
||||
snapshotGate(restoredBuild),
|
||||
webSnapshotGate(restoredBuild),
|
||||
pnpmScript('publint', 'publint'),
|
||||
pnpmScript('node-next-types', 'verify-node-next-types', {
|
||||
label: 'node-next types',
|
||||
needs: restoredBuild,
|
||||
needs: validatedBuild,
|
||||
}),
|
||||
builtPackageInvariantsGate(publicArtifacts),
|
||||
builtBinSmokeGate(restoredBuild),
|
||||
builtBinSmokeGate(validatedBuild),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -382,7 +393,7 @@ function ciWindowsCompleteGates(): Gate[] {
|
||||
|
||||
function ciWindowsObservationalGates(): Gate[] {
|
||||
return [
|
||||
...ciStaticGates(),
|
||||
...ciStaticGates({ ownsBuild: true }),
|
||||
// Linux owns required lint, coverage, and snapshots; Windows omits those duplicates.
|
||||
pnpmScript('duplication', 'duplication'),
|
||||
pnpmScript('publint', 'publint', { needs: ['build'] }),
|
||||
@@ -402,20 +413,56 @@ function lintGate(): Gate {
|
||||
: { displayCommand: `DSH_OXLINT_THREADS=${raw} pnpm run lint` })
|
||||
}
|
||||
|
||||
function coverageGate(): Gate {
|
||||
return pnpmExec('coverage', [
|
||||
'vitest',
|
||||
'run',
|
||||
'--coverage',
|
||||
...positiveIntArg('DSH_COVERAGE_MAX_WORKERS', '--maxWorkers'),
|
||||
], {
|
||||
label: 'test:coverage',
|
||||
})
|
||||
// The heavy suites run uninstrumented beside the thresholded gate: their
|
||||
// compiler- and subprocess-bound fixtures pay a multiple of their runtime
|
||||
// under v8 instrumentation while contributing nothing the thresholds need
|
||||
// (membership contract in scripts/coverage-exempt.ts).
|
||||
//
|
||||
// DSH_COVERAGE_MAX_WORKERS is the lane's worker budget, so the two parallel
|
||||
// gates split it instead of each claiming it whole (the failover pool's
|
||||
// 8 x 6-instance bound assumes one lane never exceeds its value). The exempt
|
||||
// gate's wall clock is dominated by its longest single file, so it takes the
|
||||
// small share. A budget of 1 gives each gate 1 worker; lanes that need a
|
||||
// strict total of one (the serial reference jobs) also set
|
||||
// DSH_GATE_CONCURRENCY=1, which keeps the gates from overlapping at all.
|
||||
function coverageWorkerArgs(): { instrumented: string[]; exempt: string[] } {
|
||||
const [flag] = positiveIntArg('DSH_COVERAGE_MAX_WORKERS', '--maxWorkers')
|
||||
if (flag === undefined) return { instrumented: [], exempt: [] }
|
||||
const total = Number.parseInt(flag.split('=')[1] ?? '', 10)
|
||||
const exempt = Math.max(1, Math.floor(total / 3))
|
||||
const instrumented = Math.max(1, total - exempt)
|
||||
return {
|
||||
instrumented: [`--maxWorkers=${String(instrumented)}`],
|
||||
exempt: [`--maxWorkers=${String(exempt)}`],
|
||||
}
|
||||
}
|
||||
|
||||
function coverageGates(): Gate[] {
|
||||
const workers = coverageWorkerArgs()
|
||||
return [
|
||||
pnpmExec('coverage', [
|
||||
'vitest',
|
||||
'run',
|
||||
'--coverage',
|
||||
...workers.instrumented,
|
||||
], {
|
||||
label: 'test:coverage',
|
||||
env: { [COVERAGE_EXEMPT_ENV]: '1' },
|
||||
}),
|
||||
pnpmExec('coverage-exempt-heavy', [
|
||||
'vitest',
|
||||
'run',
|
||||
...coverageExemptHeavySuites.map(suite => suite.filter),
|
||||
...workers.exempt,
|
||||
], {
|
||||
label: 'test:coverage-exempt-heavy',
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
// Example and package snapshots boot their bins in `lib` mode (built artifacts under plain Node,
|
||||
// plugins via real exports); repository-script snapshots execute their real source entry path.
|
||||
// Build-owning modes wait on `build`; a restored-artifact mode passes its validation dependency.
|
||||
// Callers wait either on `build` or on a validation gate that transitively owns that build.
|
||||
function snapshotGate(needs: string[] = ['build']): Gate {
|
||||
return pnpmScript('snapshot', 'test:snapshot', {
|
||||
env: { DSH_EXAMPLE_MODE: 'lib' },
|
||||
@@ -463,6 +510,7 @@ function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] {
|
||||
}
|
||||
|
||||
function docSyncLeafGates(options: {
|
||||
includeDocTypecheck?: boolean
|
||||
docTypecheckNeeds?: string[]
|
||||
docTypecheckEnv?: Record<string, string | undefined>
|
||||
docsBuildScript?: 'docs:build' | 'docs:build:mpa'
|
||||
@@ -471,7 +519,9 @@ function docSyncLeafGates(options: {
|
||||
if (options.docTypecheckNeeds !== undefined) docTypecheckOptions.needs = options.docTypecheckNeeds
|
||||
if (options.docTypecheckEnv !== undefined) docTypecheckOptions.env = options.docTypecheckEnv
|
||||
return [
|
||||
pnpmScript('doc-typecheck', 'doc-typecheck', docTypecheckOptions),
|
||||
...options.includeDocTypecheck === false
|
||||
? []
|
||||
: [pnpmScript('doc-typecheck', 'doc-typecheck', docTypecheckOptions)],
|
||||
pnpmScript('cordis-catalog', 'verify-cordis-catalog', { label: 'cordis catalog' }),
|
||||
pnpmScript('export-jsdoc', 'verify-export-jsdoc', { label: 'export jsdoc' }),
|
||||
pnpmScript('tool-catalog', 'verify-tool-catalog', { label: 'tool catalog' }),
|
||||
|
||||
@@ -510,7 +510,7 @@ def smoke_sdk_default(base_url: str) -> None:
|
||||
root = Path(temporary).resolve()
|
||||
sessions = root / "sessions"
|
||||
with DeepSeekHarness(
|
||||
provider="deepseek",
|
||||
provider="deepseek-official",
|
||||
model="smoke-model",
|
||||
cwd=str(root),
|
||||
session_root=str(sessions),
|
||||
@@ -533,7 +533,7 @@ def smoke_sdk_custom(base_url: str, executable: Path) -> None:
|
||||
cordis = root / "cordis.yml"
|
||||
cordis.write_text(CUSTOM_CORDIS)
|
||||
with DeepSeekHarness(
|
||||
provider="deepseek",
|
||||
provider="deepseek-official",
|
||||
model="smoke-model",
|
||||
cwd=str(root),
|
||||
session_root=str(sessions),
|
||||
@@ -598,7 +598,7 @@ def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool)
|
||||
cordis = root / "cordis.yml"
|
||||
cordis.write_text(CUSTOM_CORDIS)
|
||||
with DeepSeekHarness(
|
||||
provider="deepseek",
|
||||
provider="deepseek-official",
|
||||
model="smoke-model",
|
||||
cwd=str(root),
|
||||
session_root=str(sessions),
|
||||
@@ -648,7 +648,7 @@ def smoke_direct(base_url: str, executable: Path) -> None:
|
||||
}
|
||||
peer = RuntimePeer([str(executable)], root, environment)
|
||||
try:
|
||||
peer.send({"jsonrpc": "2.0", "id": "initialize", "method": "initialize", "params": {"cwd": str(root), "provider": "deepseek", "model": "smoke-model"}})
|
||||
peer.send({"jsonrpc": "2.0", "id": "initialize", "method": "initialize", "params": {"cwd": str(root), "provider": "deepseek-official", "model": "smoke-model"}})
|
||||
peer.read_until(lambda message: message.get("id") == "initialize")
|
||||
peer.send({
|
||||
"jsonrpc": "2.0",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -61,7 +61,8 @@ describe('global test invariant host', () => {
|
||||
return () => {}
|
||||
})
|
||||
const fakeContext = { invariants: { register } } as unknown as Context
|
||||
for (const [rawPath, companion] of Object.entries(testInvariantCompanions)) {
|
||||
for (const [rawPath, load] of Object.entries(testInvariantCompanions)) {
|
||||
const companion = await load()
|
||||
const path = rawPath.replace(/^\.\.\//, '')
|
||||
expect(companion.default, path).toBeUndefined()
|
||||
const unwrapped = loader.unwrapExports(companion) as typeof companion
|
||||
|
||||
@@ -12,8 +12,8 @@ import InvariantService from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
declare global {
|
||||
interface ImportMeta {
|
||||
/** Eager Vite module-glob expansion used by the Vitest setup file. */
|
||||
glob<TModule>(pattern: string, options: { eager: true }): Record<string, TModule>
|
||||
/** Lazy Vite module-glob expansion used by the Vitest setup file. */
|
||||
glob<TModule>(pattern: string): Record<string, () => Promise<TModule>>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,15 @@ export interface TestInvariantCompanion {
|
||||
apply(ctx: Context): Promise<() => void>
|
||||
}
|
||||
|
||||
/** Every package companion, discovered eagerly so coverage observes each registration. */
|
||||
export const testInvariantCompanions: Readonly<Record<string, TestInvariantCompanion>> =
|
||||
import.meta.glob<TestInvariantCompanion>('../packages/*/*/src/invariant.ts', { eager: true })
|
||||
/**
|
||||
* Every package companion as a lazy loader keyed by glob path. Ordinary tests
|
||||
* load only their owner's module; the exhaustive topology test loads and
|
||||
* executes all of them, so aggregated coverage still observes every
|
||||
* registration while per-file setup stops importing 168 companions and their
|
||||
* transitive package sources.
|
||||
*/
|
||||
export const testInvariantCompanions: Readonly<Record<string, () => Promise<TestInvariantCompanion>>> =
|
||||
import.meta.glob<TestInvariantCompanion>('../packages/*/*/src/invariant.ts')
|
||||
|
||||
/** Manual-topology suites whose names cannot follow the focused invariant convention. */
|
||||
const MANUAL_INVARIANT_TEST_EXCEPTIONS = [
|
||||
@@ -36,7 +42,6 @@ const MANUAL_INVARIANT_TEST_EXCEPTIONS = [
|
||||
] as const
|
||||
|
||||
interface InvariantHost {
|
||||
readonly fibers: readonly PluginFiber[]
|
||||
readonly byCallback: ReadonlyMap<unknown, PluginFiber>
|
||||
readonly ready: Promise<void>
|
||||
}
|
||||
@@ -102,39 +107,40 @@ export function testInvariantCompanionPaths(testPath: string): string[] {
|
||||
}
|
||||
|
||||
function startInvariantHost(root: Context): InvariantHost {
|
||||
const fibers: PluginFiber[] = []
|
||||
const byCallback = new Map<unknown, PluginFiber>()
|
||||
const mount = (plugin: Plugin, config?: unknown): void => {
|
||||
const mount = (plugin: Plugin, config?: unknown): PluginFiber => {
|
||||
const fiber = originalPlugin.call(root.registry, plugin, config)
|
||||
const callback = root.registry.resolve(plugin)
|
||||
if (callback === undefined) throw new Error('test invariants: companion is not a valid Cordis plugin')
|
||||
fibers.push(fiber)
|
||||
byCallback.set(callback, fiber)
|
||||
return fiber
|
||||
}
|
||||
|
||||
mount(InvariantService, { enabled: true })
|
||||
// The service mounts synchronously so the intercepted registration that
|
||||
// started this host immediately finds its own fiber in byCallback.
|
||||
// Companions load and mount inside the ready chain (after the service is
|
||||
// active, so their startup is directly joinable); every joined root plugin
|
||||
// awaits ready, so none starts ahead of its package checks. Tests plugging
|
||||
// a companion directly must await an earlier root plugin first — the
|
||||
// duplicate-mount failure otherwise is loud (owner name already reserved).
|
||||
const serviceFiber = mount(InvariantService, { enabled: true })
|
||||
const testPath = expect.getState().testPath ?? ''
|
||||
const companionPaths = testInvariantCompanionPaths(testPath)
|
||||
for (const path of companionPaths) {
|
||||
const companion = testInvariantCompanions[path]
|
||||
if (companion === undefined) {
|
||||
throw new Error(`test invariants: selected companion vanished at ${path}`)
|
||||
}
|
||||
if (!companion.inject.includes('invariants')) {
|
||||
throw new Error(`test invariants: ${path} must inject the invariant service`)
|
||||
}
|
||||
mount(companion)
|
||||
}
|
||||
|
||||
const [serviceFiber, ...companionFibers] = fibers
|
||||
if (serviceFiber === undefined) throw new Error('test invariants: service fiber was not mounted')
|
||||
// A companion is initially PENDING on the invariant service, and Cordis
|
||||
// Fiber.await() only joins work already in flight. Wait for the service to
|
||||
// activate its dependants before joining their startup and failures.
|
||||
const ready = serviceFiber.await()
|
||||
.then(() => Promise.all(companionFibers.map(fiber => fiber.await())))
|
||||
.then(() => undefined)
|
||||
const host = { fibers, byCallback, ready }
|
||||
const ready = serviceFiber.await().then(async () => {
|
||||
const companionFibers = await Promise.all(companionPaths.map(async (path) => {
|
||||
const load = testInvariantCompanions[path]
|
||||
if (load === undefined) {
|
||||
throw new Error(`test invariants: selected companion vanished at ${path}`)
|
||||
}
|
||||
const companion = await load()
|
||||
if (!companion.inject.includes('invariants')) {
|
||||
throw new Error(`test invariants: ${path} must inject the invariant service`)
|
||||
}
|
||||
return mount(companion)
|
||||
}))
|
||||
await Promise.all(companionFibers.map(fiber => fiber.await()))
|
||||
})
|
||||
const host = { byCallback, ready }
|
||||
hosts.set(root, host)
|
||||
return host
|
||||
}
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
"symbol": "FinishReasonMap",
|
||||
"source": "packages/llm/llm/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "AdapterRegistrationHandle",
|
||||
"source": "packages/llm/llm/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "LlmProviderInfo",
|
||||
@@ -81,6 +86,11 @@
|
||||
"symbol": "LlmCallConfig",
|
||||
"source": "packages/llm/llm/src/call-config.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "LlmCallConfigAdapterDefaults",
|
||||
"source": "packages/llm/llm/src/call-config.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "SessionEvent",
|
||||
@@ -694,6 +704,11 @@
|
||||
"symbol": "AskUserQuestionOption",
|
||||
"source": "packages/ui/user-interaction/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/user-interaction.md",
|
||||
"symbol": "AskUserQuestionIntent",
|
||||
"source": "packages/ui/user-interaction/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/user-interaction.md",
|
||||
"symbol": "AskUserQuestionItem",
|
||||
@@ -1363,6 +1378,36 @@
|
||||
"doc": "docs/core-data-structures/settings.md",
|
||||
"symbol": "SettingsUpdateSource",
|
||||
"source": "packages/settings/settings/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/credentials.md",
|
||||
"symbol": "CredentialRef",
|
||||
"source": "packages/credentials/credentials/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/credentials.md",
|
||||
"symbol": "ResolvedCredential",
|
||||
"source": "packages/credentials/credentials/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/credentials.md",
|
||||
"symbol": "CredentialInfo",
|
||||
"source": "packages/credentials/credentials/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/settings.md",
|
||||
"symbol": "SettingsDescribeOptions",
|
||||
"source": "packages/settings/settings/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "LlmConfigurableProvider",
|
||||
"source": "packages/llm/llm/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/settings.md",
|
||||
"symbol": "SettingsPathOp",
|
||||
"source": "packages/settings/settings/src/index.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -33,6 +33,20 @@ const root = resolve(import.meta.dirname, '..')
|
||||
// specifiers resolve from apps/cli rather than the examples workspace.
|
||||
const appOverlayFiles = new Set(['examples/web-cordis/cordis.yml'])
|
||||
const metadataFields = ['id', 'name', 'group', 'disabled', 'inject', 'intercept', 'isolate'] as const
|
||||
|
||||
/** The adaptive directory-picker chooser package (mounts a backend row at boot). */
|
||||
const CHOOSER_PACKAGE = '@deepseek-ai/dsh-host-directory-picker-auto'
|
||||
|
||||
/**
|
||||
* The backends the chooser mounts by runtime string (mirror of its exported
|
||||
* `BACKEND_PACKAGES`), invisible to yml-row scanning: a composition mounting
|
||||
* the chooser must resolve both, or keyless Linux CI (which only ever
|
||||
* resolves `browse`) hides a dropped `-native` dependency until a macOS boot.
|
||||
*/
|
||||
const CHOOSER_BACKEND_PACKAGES = [
|
||||
'@deepseek-ai/dsh-host-directory-picker-native',
|
||||
'@deepseek-ai/dsh-host-directory-picker-browse',
|
||||
]
|
||||
const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
|
||||
kind: 'scalar',
|
||||
resolve: data => typeof data === 'string',
|
||||
@@ -60,6 +74,7 @@ for (const file of files) {
|
||||
|
||||
errors.push(...validateExampleResolution())
|
||||
errors.push(...validateAppResolution())
|
||||
errors.push(...validateSourcePlaneResolution())
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error('verify-cordis-config: invalid Loader metadata or plugin package resolution:')
|
||||
@@ -138,18 +153,75 @@ function validateAppResolution(): string[] {
|
||||
return missingPluginDependencies(references, dependencies, 'apps/cli/package.json')
|
||||
}
|
||||
|
||||
/**
|
||||
* Every configured specifier of a local workspace package must resolve through
|
||||
* the tsconfig `paths` facade to a `.ts`/`.tsx` source file. The `dsh` source
|
||||
* launch (tsx) and vitest resolve in the source plane; without a `paths` match
|
||||
* they fall back to package `exports`, which reach built `lib/` — present on a
|
||||
* built dev tree, absent on a clean one — so a missing mapping boots locally
|
||||
* yet breaks every clean checkout. Anything but a `.ts`/`.tsx` hit (a `.d.ts`
|
||||
* or `.js` under built `lib/`) is that artifact-plane fallback, not source.
|
||||
*/
|
||||
function validateSourcePlaneResolution(): string[] {
|
||||
const violations: string[] = []
|
||||
const localPackages = localPackageDirectories()
|
||||
const config = ts.readConfigFile(resolve(root, 'tsconfig.base.json'), path => ts.sys.readFile(path))
|
||||
if (config.error !== undefined) {
|
||||
throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, '\n'))
|
||||
}
|
||||
const { options, errors: optionErrors } = ts.convertCompilerOptionsFromJson(
|
||||
(config.config as { compilerOptions?: unknown }).compilerOptions,
|
||||
root,
|
||||
'tsconfig.base.json',
|
||||
)
|
||||
if (optionErrors.length > 0) {
|
||||
throw new Error(optionErrors.map(error => ts.flattenDiagnosticMessageText(error.messageText, '\n')).join('\n'))
|
||||
}
|
||||
// convertCompilerOptionsFromJson leaves `pathsBasePath` unset, so relative
|
||||
// `paths` targets resolve against the host's current directory; anchor it to
|
||||
// the repository root to keep the gate cwd-independent.
|
||||
const host: ts.ModuleResolutionHost = {
|
||||
fileExists: path => ts.sys.fileExists(path),
|
||||
readFile: path => ts.sys.readFile(path),
|
||||
directoryExists: path => ts.sys.directoryExists(path),
|
||||
getCurrentDirectory: () => root,
|
||||
}
|
||||
const sourceExtensions = new Set<string>([ts.Extension.Ts, ts.Extension.Tsx])
|
||||
const containingFile = resolve(root, 'scripts/verify-cordis-config.ts')
|
||||
const locationsBySpecifier = new Map<string, Set<string>>()
|
||||
for (const reference of pluginReferences) {
|
||||
const packageName = packageNameFromSpecifier(reference.name)
|
||||
if (packageName === undefined || !localPackages.has(packageName)) continue
|
||||
const locations = locationsBySpecifier.get(reference.name) ?? new Set<string>()
|
||||
locations.add(reference.file)
|
||||
locationsBySpecifier.set(reference.name, locations)
|
||||
}
|
||||
for (const [specifier, locations] of locationsBySpecifier) {
|
||||
const resolved = ts.resolveModuleName(specifier, containingFile, options, host).resolvedModule
|
||||
if (resolved !== undefined && sourceExtensions.has(resolved.extension)) continue
|
||||
violations.push(`${[...locations].join(', ')}: ${specifier} does not resolve to workspace source through tsconfig.base.json paths (add a mapping so the tsx source launch does not depend on built lib/)`)
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
function missingPluginDependencies(
|
||||
references: readonly PluginReference[],
|
||||
dependencies: Readonly<Record<string, string>>,
|
||||
manifestPath: string,
|
||||
): string[] {
|
||||
const requiredPackages = new Map<string, Set<string>>()
|
||||
const require = (packageName: string, file: string): void => {
|
||||
const locations = requiredPackages.get(packageName) ?? new Set<string>()
|
||||
locations.add(file)
|
||||
requiredPackages.set(packageName, locations)
|
||||
}
|
||||
for (const reference of references) {
|
||||
const packageName = packageNameFromSpecifier(reference.name)
|
||||
if (packageName === undefined) continue
|
||||
const locations = requiredPackages.get(packageName) ?? new Set<string>()
|
||||
locations.add(reference.file)
|
||||
requiredPackages.set(packageName, locations)
|
||||
require(packageName, reference.file)
|
||||
if (packageName === CHOOSER_PACKAGE) {
|
||||
for (const backend of CHOOSER_BACKEND_PACKAGES) require(backend, reference.file)
|
||||
}
|
||||
}
|
||||
return [...requiredPackages].flatMap(([packageName, locations]) => packageName in dependencies
|
||||
? []
|
||||
|
||||
@@ -53,6 +53,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/client/ui-slots': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/ui-primitives': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/web-react': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/schema-form': { kind: 'none', reason: 'Browser-side form-rendering library; registers no model surface.' },
|
||||
'packages/client/connection': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/runtime': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/ui-layout': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
@@ -80,6 +81,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/hooks/hook-protocol': { kind: 'indirect', reason: 'Only the hook bridge plugins render decoded hook output to a model.' },
|
||||
'packages/host/apiproxy': { kind: 'none', reason: 'The wire contract and fetch carriers move already-composed messages and register no model surface.' },
|
||||
'packages/host/directory-picker': { kind: 'none', reason: 'The GUI-host picking seam registers no model surface.' },
|
||||
'packages/host/directory-picker-auto': { kind: 'none', reason: 'The GUI-host picking chooser only mounts a backend row; registers no model surface.' },
|
||||
'packages/host/directory-picker-browse': { kind: 'none', reason: 'The GUI-host picking backend registers no model surface.' },
|
||||
'packages/host/directory-picker-native': { kind: 'none', reason: 'The GUI-host picking backend registers no model surface.' },
|
||||
'packages/host/webserver': { kind: 'none', reason: 'The HTTP carrier bridges browser and API handler and registers no model surface.' },
|
||||
@@ -103,6 +105,9 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/session-query/session-query-sqlite': { kind: 'none', reason: 'The search backend returns hits only to callers and registers no model surface.' },
|
||||
'packages/settings/settings': { kind: 'indirect', reason: 'The seam stores and resolves user settings; consumer plugins own any model surface a value feeds.' },
|
||||
'packages/settings/settings-local': { kind: 'indirect', reason: 'The file provider stores and publishes namespace sections; consumers of ctx.settings own any model surface.' },
|
||||
'packages/credentials/credentials': { kind: 'indirect', reason: 'The seam resolves credential references; the consuming adapter owns every model surface a value authorizes.' },
|
||||
'packages/credentials/credentials-local': { kind: 'indirect', reason: 'The file/environment provider stores credential values; consumers of ctx.credentials own any model surface.' },
|
||||
'packages/util/atomic-write': { kind: 'none', reason: 'Pure filesystem write primitive; registers no model surface.' },
|
||||
'packages/telemetry/session-telemetry': { kind: 'none', reason: 'The seam observes the session stream and hands redacted copies outward; it registers no model surface.' },
|
||||
'packages/telemetry/session-telemetry-otel': { kind: 'none', reason: 'The backend forwards seam records into the OTel SDK pipeline and registers no model surface.' },
|
||||
'packages/skill/skill': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-skill.' },
|
||||
|
||||
Reference in New Issue
Block a user