Files
deepseek-harness/scripts/gen-third-party-notices.spec.ts
T
Huanqi Cao 601fb9d195 fix(fs-search): address the second-round #1119 review
- inline the collect() identity wrapper now that both streams use the
  seam's diagnostic-tail shape
- resolve the packaged rg path lazily at the first call (memoized):
  @vscode/ripgrep resolves its platform package at module evaluation, so a
  static import turned a missing/corrupt platform package into a Loader
  composition failure instead of the documented per-call SEARCH_FAILED
- classify synchronous spawn-creation throws (a NUL in argv, an abort
  racing the pre-check, a rejected resolution) into SEARCH_FAILED /
  SEARCH_ABORTED instead of leaking raw errors
- correct the stderrMaxBytes contract: the stderr excerpt is embedded in
  SEARCH_* error messages, not hidden from the model
- export virtualManifest and pin its three acceptance paths (prefix hit,
  pnpm-11 truncated-name content-scan fallback, both miss) with fixture
  unit tests

Tests: rg-path.spec.ts (resolution failure + memoized rejection),
tools.spec.ts spawn-creation classification, notices spec virtualManifest.
2026-08-02 01:11:08 +08:00

260 lines
12 KiB
TypeScript

import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { tmpdir } from 'node:os'
import { describe, expect, it } from 'vitest'
import { collectPythonDependencies, isPermissive, type Manifest, manifestPatterns, parsePyprojectRequirements, parseVendoredRows, render, tierExternalDeps, virtualManifest } from './gen-third-party-notices.ts'
const root = resolve(import.meta.dirname, '..')
describe('THIRD_PARTY_NOTICES.md', () => {
// Freshness lives here rather than in its own doc-sync gate: this spec file
// already runs in the test lane, so the check costs no extra CI process.
// Pre-commit regenerates the file whenever a manifest is staged, so reaching
// this assertion means the notices were committed without that hook.
it('matches what the generator produces from the current manifests', () => {
expect(readFileSync(resolve(root, 'THIRD_PARTY_NOTICES.md'), 'utf8'), 'stale notices — run `pnpm run gen-third-party-notices`').toBe(render())
})
})
/** Build the (manifests, names) pair `tierExternalDeps` consumes. */
function workspace(entries: Record<string, Manifest>): { manifests: Map<string, Manifest>; names: Set<string> } {
const manifests = new Map(Object.entries(entries))
const names = new Set<string>()
for (const manifest of manifests.values()) {
if (manifest.name !== undefined) names.add(manifest.name)
}
return { manifests, names }
}
describe('tierExternalDeps', () => {
it('tiers by declaring area, not by the declaring section name', () => {
const { manifests, names } = workspace({
// Root tooling and test infrastructure never ship, whichever section declares them.
'package.json': { dependencies: { 'root-runtime-looking': '^1' }, devDependencies: { 'lint-tool': '^1' } },
'packages/support/loader-smoke/package.json': { name: '@deepseek-ai/dsh-loader-smoke', dependencies: { 'smoke-helper': '^1' } },
'packages/client/test-runtime/package.json': { name: '@deepseek-ai/dsh-client-test-runtime', dependencies: { 'test-lib': '^1' } },
'website/package.json': { devDependencies: { 'site-tool': '^1' } },
// A plugin package's runtime dependency ships even when no app mounts it by default.
'packages/mcp/mcp-client/package.json': { name: '@deepseek-ai/dsh-mcp-client', dependencies: { 'protocol-sdk': '^1' }, devDependencies: { 'protocol-fixture-server': '^1' } },
'apps/cli/package.json': { name: '@deepseek-ai/dsh-cli', dependencies: { 'cli-lib': '^1', '@deepseek-ai/dsh-mcp-client': 'workspace:^' } },
})
expect(tierExternalDeps(manifests, names)).toEqual(new Map([
['tsx', true],
['root-runtime-looking', false],
['lint-tool', false],
['smoke-helper', false],
['test-lib', false],
['site-tool', false],
['protocol-sdk', true],
['protocol-fixture-server', false],
['cli-lib', true],
]))
})
it('keeps a package runtime when any shipping area declares it, and excludes workspace links', () => {
const { manifests, names } = workspace({
'package.json': { devDependencies: { shared: '^1' } },
'packages/ui/tui/package.json': { name: '@deepseek-ai/dsh-tui', dependencies: { shared: '^1', '@deepseek-ai/dsh-cli': 'workspace:^' } },
'apps/cli/package.json': { name: '@deepseek-ai/dsh-cli' },
})
expect(tierExternalDeps(manifests, names).get('shared')).toBe(true)
expect(tierExternalDeps(manifests, names).has('@deepseek-ai/dsh-cli')).toBe(false)
})
})
describe('virtualManifest', () => {
it('resolves a manifest from an ordinary prefix-matching store directory', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-notices-prefix-'))
try {
const name = '@scope/pkg'
const version = '1.0.0'
const store = join(root, 'store')
const manifestDir = join(store, `${name.replace('/', '+')}@${version}`, 'node_modules', name)
mkdirSync(manifestDir, { recursive: true })
writeFileSync(join(manifestDir, 'package.json'), JSON.stringify({ name, version, license: 'MIT' }))
expect(virtualManifest(store, name)).toMatchObject({ name, version, license: 'MIT' })
} finally {
rmSync(root, { recursive: true, force: true })
}
})
it('falls back to a content scan when pnpm 11 truncates the store directory name', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-notices-truncated-'))
try {
const name = '@scope/pkg'
const version = '2.0.0'
const store = join(root, 'store')
// The truncated name no longer starts with `@scope+pkg@`, so only the
// whole-store content scan can find the package.
const manifestDir = join(store, '@scope+pkg_9f1c2d3e4a5b6c7d8e9f0a1b2c3d4e5f', 'node_modules', name)
mkdirSync(manifestDir, { recursive: true })
writeFileSync(join(manifestDir, 'package.json'), JSON.stringify({ name, version, license: 'Apache-2.0' }))
expect(virtualManifest(store, name)).toMatchObject({ name, version, license: 'Apache-2.0' })
} finally {
rmSync(root, { recursive: true, force: true })
}
})
it('returns undefined when neither the prefix nor the content scan finds the package', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-notices-miss-'))
try {
const store = join(root, 'store')
const other = join(store, 'other-pkg@1.0.0', 'node_modules', 'other-pkg')
mkdirSync(other, { recursive: true })
writeFileSync(join(other, 'package.json'), JSON.stringify({ name: 'other-pkg', version: '1.0.0' }))
expect(virtualManifest(store, '@scope/missing')).toBeUndefined()
} finally {
rmSync(root, { recursive: true, force: true })
}
})
})
describe('parseVendoredRows', () => {
it('reads the committed vendor manifest table', () => {
const rows = parseVendoredRows(readFileSync(resolve(root, 'vendor/README.md'), 'utf8'))
expect(rows.length).toBeGreaterThan(0)
expect(rows).toContainEqual({ npmName: 'cordis', upstream: 'https://github.com/cordiverse/cordis' })
// The upstream column carries a trailing package path for some rows; it is not part of the URL.
expect(rows.every(row => /^https:\/\/\S+$/.test(row.upstream))).toBe(true)
})
it('yields nothing when the table shape changes, so the generator fails loud', () => {
expect(parseVendoredRows('| `cordis/` | cordis | 4.0.0 | https://example.com | `abc123` |\n')).toEqual([])
})
it('covers every vendored directory, so no package can drop out of the notices', () => {
const parsed = new Set(parseVendoredRows(readFileSync(resolve(root, 'vendor/README.md'), 'utf8')).map(row => row.npmName))
const onDisk = readdirSync(resolve(root, 'vendor'), { withFileTypes: true })
.filter(entry => entry.isDirectory())
.map(entry => (JSON.parse(readFileSync(resolve(root, 'vendor', entry.name, 'package.json'), 'utf8')) as Manifest).name)
expect([...onDisk].sort()).toEqual([...parsed].sort())
})
})
describe('parsePyprojectRequirements', () => {
it('reads the committed manifests', () => {
expect(parsePyprojectRequirements(readFileSync(resolve(root, 'python/sdk/pyproject.toml'), 'utf8'))).toContain('pydantic')
})
it('locates requirement arrays by TOML table, so author-named groups are not missed', () => {
expect(parsePyprojectRequirements([
'[build-system]',
'requires = ["hatchling>=1.24.0"]',
'',
'[project]',
'name = "not-a-requirement"',
'dependencies = ["pydantic>=2.12"]',
'',
'[project.optional-dependencies]',
'cli = ["click"]',
'',
'[dependency-groups]',
'docs = ["sphinx>=7"]',
'',
'[tool.hatch.build.targets.wheel]',
'packages = ["src/deepseek_harness"]',
'',
'[tool.pytest.ini_options]',
'testpaths = ["tests"]',
].join('\n'))).toEqual(['hatchling', 'pydantic', 'click', 'sphinx'])
})
it('does not truncate an array at a bracket inside extras', () => {
expect(parsePyprojectRequirements('[project]\ndependencies = ["httpx[http2]", "requests"]\n'))
.toEqual(['httpx', 'requests'])
})
it('reads names whether or not requirements carry versions, extras, or markers', () => {
expect(parsePyprojectRequirements("[project]\ndependencies = [\"pydantic>=2.12\", \"requests\", \"httpx[http2]\", \"tomli ; python_version < '3.11'\", \"hatchling >= 1.24.0\"]\n"))
.toEqual(['pydantic', 'requests', 'httpx', 'tomli', 'hatchling'])
})
it('reads single-quoted TOML literals and rejects an unreadable requirement', () => {
expect(parsePyprojectRequirements("[project]\ndependencies = ['requests', \"pydantic>=2\"]\n")).toEqual(['requests', 'pydantic'])
expect(() => parsePyprojectRequirements('[project]\ndependencies = ["!!broken"]\n')).toThrow(/cannot read a distribution name/)
})
it('reads a multi-line array', () => {
expect(parsePyprojectRequirements('[project]\ndependencies = [\n "pydantic>=2.12",\n "typing-extensions",\n]\n'))
.toEqual(['pydantic', 'typing-extensions'])
})
it('obeys TOML comments, quoted keys, and escaped strings', () => {
expect(parsePyprojectRequirements([
'[project] # a legal header comment',
'dependencies = [',
' "pydantic", # ] does not close the array',
' # "old-package" is not a dependency',
' "tomli; python_version < \'3.11\'",',
']',
'',
'[dependency-groups]',
'"test.docs" = ["pytest"]',
].join('\n'))).toEqual(['pydantic', 'tomli', 'pytest'])
})
it('accepts dependency-group includes and rejects unsupported requirement shapes', () => {
expect(parsePyprojectRequirements('[dependency-groups]\nbase = ["pytest"]\nall = [{ include-group = "base" }]\n'))
.toEqual(['pytest'])
expect(() => parsePyprojectRequirements('[project]\ndependencies = "pytest"\n')).toThrow(/must be an array/)
expect(() => parsePyprojectRequirements('[dependency-groups]\ntest = [{ unknown = "pytest" }]\n')).toThrow(/unsupported requirement entry/)
})
})
describe('collectPythonDependencies', () => {
it('excludes normalized local project names without exempting a third-party prefix', () => {
const pyprojects = [
'[project]\nname = "deepseek-harness-runtime-bin"\ndependencies = ["pydantic"]\n',
'[project]\nname = "deepseek-harness"\ndependencies = ["DeepSeek.Harness_Runtime-Bin", "deepseek-unrelated"]\n',
]
expect(() => collectPythonDependencies(pyprojects)).toThrow(
'python dependency deepseek-unrelated is missing from PYTHON_METADATA',
)
})
})
describe('isPermissive', () => {
it('accepts the licenses this project ships and rejects copyleft or unknown ones', () => {
expect(['MIT', 'ISC', 'BSD-3-Clause', 'Apache-2.0', 'MIT / Apache-2.0', '(MIT OR CC0-1.0)'].every(isPermissive)).toBe(true)
expect(['LGPL-3.0-only', 'MPL-2.0', 'GPL-3.0-or-later', 'SEE LICENSE IN LICENSE'].some(isPermissive)).toBe(false)
})
it('requires every operand of an AND, so a copyleft conjunct cannot ride along', () => {
expect(isPermissive('(MIT OR Apache-2.0) AND GPL-3.0-only')).toBe(false)
expect(isPermissive('MIT AND ISC')).toBe(true)
// An exception clause is not a recognized identifier, so it fails closed.
expect(isPermissive('GPL-2.0-only WITH Classpath-exception-2.0')).toBe(false)
})
it('honors grouping and SPDX precedence', () => {
expect(isPermissive('MIT OR (GPL-3.0-only AND GPL-2.0-only)')).toBe(true)
expect(isPermissive('(MIT OR Apache-2.0) AND ISC')).toBe(true)
})
it('fails closed for malformed expressions, additions, and exceptions', () => {
expect(['MIT)', '((MIT', '(MIT OR GPL-3.0-only', 'MIT OR OR GPL-3.0-only'].some(isPermissive)).toBe(false)
expect(isPermissive('MIT+')).toBe(false)
expect(isPermissive('GPL-2.0-only WITH Classpath-exception-2.0')).toBe(false)
})
})
describe('manifestPatterns', () => {
it('derives globs from the declared members, so a new member area is read', () => {
expect(manifestPatterns(['packages/*/*', 'tools/*'], ['packages/*'])).toEqual([
'package.json',
'packages/*/*/package.json',
'tools/*/package.json',
'examples/*/package.json',
'native/landlock-run/package.json',
'native/landlock-run/packages/*/package.json',
])
})
})