verify-package-dependencies.spec.ts 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989
  1. import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
  2. import { tmpdir } from 'node:os'
  3. import { dirname, join } from 'node:path'
  4. import { afterEach, describe, expect, it } from 'vitest'
  5. import {
  6. PACKAGE_DEPENDENCY_POLICY,
  7. type PackageDependencyPolicy,
  8. } from './package-dependency-policy.ts'
  9. import {
  10. collectHostDependencyExportPolicyViolations,
  11. collectPackageDependencyViolations,
  12. collectRuntimeSourceExportUses,
  13. discoverPackageDependencyScope,
  14. expectedPackageDependencies,
  15. fixPackageDependencies,
  16. formatManagedRuntimeDependencies,
  17. formatPeerRequiredRuntimeDependencies,
  18. readPackageDependencyFacts,
  19. readPackageDependencyState,
  20. repairPackageDependencyManifest,
  21. type PackageDependencyFacts,
  22. type PackageDependencyManifest,
  23. type PackageDependencyRole,
  24. type WorkspacePackageManifest,
  25. } from './verify-package-dependencies.ts'
  26. const CORDIS = '@deepseek-ai/cordis'
  27. const roots: string[] = []
  28. afterEach(() => {
  29. for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
  30. })
  31. function pkg(
  32. name: string,
  33. manifestPath: string,
  34. manifest: Partial<PackageDependencyManifest> = {},
  35. ): WorkspacePackageManifest {
  36. return {
  37. name,
  38. manifestPath,
  39. dir: dirname(manifestPath),
  40. manifest: { name, ...manifest },
  41. }
  42. }
  43. function policy(fields: Partial<PackageDependencyPolicy> = {}): PackageDependencyPolicy {
  44. return {
  45. clientFaceInclude: [],
  46. clientFaceExclude: [],
  47. hostPackages: [],
  48. configurationOnlyDevDependencies: {},
  49. safeHostDependencyExports: {},
  50. peerRequiredHostExports: {},
  51. ...fields,
  52. }
  53. }
  54. function facts(manifest: PackageDependencyManifest): PackageDependencyFacts {
  55. return {
  56. manifestPath: 'packages/core/probe/package.json',
  57. role: 'configured-host',
  58. manifest,
  59. workspaceNames: new Set([
  60. CORDIS,
  61. '@deepseek-ai/dsh-runtime',
  62. '@deepseek-ai/dsh-types',
  63. '@deepseek-ai/dsh-stale',
  64. '@deepseek-ai/schemastery',
  65. ]),
  66. allSourceUses: new Map([
  67. ['@deepseek-ai/dsh-runtime', ['packages/core/probe/src/index.ts']],
  68. ['@deepseek-ai/dsh-types', ['packages/core/probe/src/types.ts']],
  69. ]),
  70. hostRuntimeSourceUses: new Map([
  71. ['@deepseek-ai/dsh-runtime', ['packages/core/probe/src/index.ts']],
  72. ]),
  73. hostRuntimeExportUses: [{
  74. packageName: '@deepseek-ai/dsh-runtime',
  75. specifier: '@deepseek-ai/dsh-runtime',
  76. exportName: 'runtimeValue',
  77. sourcePath: 'packages/core/probe/src/index.ts',
  78. line: 1,
  79. column: 10,
  80. sourceLine: "import { runtimeValue } from '@deepseek-ai/dsh-runtime'",
  81. }],
  82. peerRequiredHostDependencies: new Set(),
  83. configurationOnlyDevDependencies: new Set(),
  84. clientInject: new Set(),
  85. }
  86. }
  87. function sourceFacts(
  88. files: Readonly<Record<string, string>>,
  89. manifest: Partial<PackageDependencyManifest> = {},
  90. role: PackageDependencyRole = 'client-host',
  91. ): PackageDependencyFacts {
  92. const root = mkdtempSync(join(tmpdir(), 'dsh-dependency-source-'))
  93. roots.push(root)
  94. const subject = pkg('@f/probe', 'packages/g/probe/package.json', manifest)
  95. for (const [path, source] of Object.entries(files)) {
  96. const absolute = join(root, subject.dir, path)
  97. mkdirSync(dirname(absolute), { recursive: true })
  98. writeFileSync(absolute, source)
  99. }
  100. return readPackageDependencyFacts(root, subject, role, new Set([CORDIS, subject.name]), policy())
  101. }
  102. function generatedHostFixture(mode: 'schema' | 'object'): { root: string; manifestPath: string; source: string } {
  103. const root = mkdtempSync(join(tmpdir(), 'dsh-generated-host-dependencies-'))
  104. roots.push(root)
  105. const manifestPath = 'packages/client/probe/package.json'
  106. const source = `/** @typert ${mode} */\nexport interface Payload { value: string }\n`
  107. const manifest = {
  108. name: '@fixture/generated',
  109. type: 'module',
  110. dsh: { client: {} },
  111. exports: {
  112. '.': { types: './lib/types/index.d.ts', default: './lib/index.js' },
  113. './typert': { types: './lib/typert.host.d.ts', default: './lib/typert.host.js' },
  114. },
  115. files: ['lib/typert.host.js', 'lib/typert.host.d.ts'],
  116. dependencies: { zod: '^4.0.0' },
  117. devDependencies: { [CORDIS]: 'workspace:^' },
  118. peerDependencies: { [CORDIS]: 'workspace:^' },
  119. }
  120. const files = {
  121. 'tsconfig.base.json': JSON.stringify({
  122. compilerOptions: {
  123. target: 'ES2024', module: 'ESNext', moduleResolution: 'Bundler', strict: true,
  124. composite: true, noEmit: true, types: [], skipLibCheck: true,
  125. },
  126. }),
  127. 'tsconfig.host.json': JSON.stringify({
  128. extends: './tsconfig.base.json', files: [], references: [{ path: './packages/client/probe' }],
  129. }),
  130. 'packages/client/probe/tsconfig.json': JSON.stringify({
  131. extends: '../../../tsconfig.base.json', compilerOptions: { rootDir: 'src' }, include: ['src'],
  132. }),
  133. [manifestPath]: JSON.stringify(manifest),
  134. 'packages/client/probe/src/index.ts': source,
  135. }
  136. for (const [path, content] of Object.entries(files)) {
  137. mkdirSync(dirname(join(root, path)), { recursive: true })
  138. writeFileSync(join(root, path), content)
  139. }
  140. return { root, manifestPath, source }
  141. }
  142. function hostRuntimeFixture(): {
  143. provider: WorkspacePackageManifest
  144. workspaceNames: Set<string>
  145. consumerFacts: PackageDependencyFacts
  146. } {
  147. const consumer = pkg('@f/consumer', 'packages/core/consumer/package.json')
  148. const provider = pkg('@f/provider', 'packages/core/provider/package.json')
  149. const sourcePath = 'packages/core/consumer/src/index.ts'
  150. const specifier = `${provider.name}/api`
  151. const workspaceNames = new Set([CORDIS, consumer.name, provider.name])
  152. const consumerFacts: PackageDependencyFacts = {
  153. manifestPath: consumer.manifestPath,
  154. role: 'configured-host',
  155. manifest: consumer.manifest,
  156. workspaceNames,
  157. allSourceUses: new Map(),
  158. hostRuntimeSourceUses: new Map([[provider.name, [sourcePath]]]),
  159. hostRuntimeExportUses: [{
  160. packageName: provider.name,
  161. specifier,
  162. exportName: 'safeValue',
  163. sourcePath,
  164. line: 1,
  165. column: 10,
  166. sourceLine: `import { safeValue } from '${specifier}'`,
  167. }],
  168. peerRequiredHostDependencies: new Set(),
  169. configurationOnlyDevDependencies: new Set(),
  170. clientInject: new Set(),
  171. }
  172. return { provider, workspaceNames, consumerFacts }
  173. }
  174. describe('package dependency scope', () => {
  175. it('keeps the measured Host relay roster explicit', () => {
  176. expect(PACKAGE_DEPENDENCY_POLICY.clientFaceExclude).toEqual([
  177. '@deepseek-ai/dsh-api-session-controller',
  178. '@deepseek-ai/dsh-api-workspace-controller',
  179. ])
  180. expect(PACKAGE_DEPENDENCY_POLICY.hostPackages).toEqual([
  181. '@deepseek-ai/dsh-llm',
  182. '@deepseek-ai/dsh-session',
  183. ])
  184. expect(PACKAGE_DEPENDENCY_POLICY.configurationOnlyDevDependencies).toEqual({
  185. '@deepseek-ai/dsh-client-locale': ['@deepseek-ai/dsh-api-remotes'],
  186. '@deepseek-ai/dsh-client-ui-conversation': [
  187. '@deepseek-ai/dsh-api-remotes',
  188. '@deepseek-ai/dsh-client-ui-workspace',
  189. ],
  190. '@deepseek-ai/dsh-client-ui-model-selection': ['@deepseek-ai/dsh-client-ui-input-trigger'],
  191. '@deepseek-ai/dsh-client-ui-sidebar': ['@deepseek-ai/dsh-client-ui-workspace'],
  192. '@deepseek-ai/dsh-client-ui-subagent': ['@deepseek-ai/dsh-client-ui-input-trigger'],
  193. '@deepseek-ai/dsh-client-ui-theme': ['@deepseek-ai/dsh-api-remotes'],
  194. '@deepseek-ai/dsh-client-ui-tool': ['@deepseek-ai/dsh-api-remotes'],
  195. })
  196. expect(PACKAGE_DEPENDENCY_POLICY.duplicateSafePackages).toEqual([
  197. '@deepseek-ai/dsh-brand',
  198. '@deepseek-ai/dsh-lazy-require',
  199. '@deepseek-ai/dsh-typert-protocol',
  200. '@deepseek-ai/dsh-util-crypto',
  201. '@deepseek-ai/dsh-util-values',
  202. ])
  203. expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/dsh-deque']).toEqual(['Deque'])
  204. expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/schemastery']).toEqual(['default'])
  205. expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/dsh-session/types']).toBeUndefined()
  206. expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/dsh-typert-protocol']).toBeUndefined()
  207. expect(PACKAGE_DEPENDENCY_POLICY.peerRequiredHostExports['@deepseek-ai/dsh-scope']).toEqual([
  208. 'carrierKeyOf', 'scopeOf', 'scopeTarget',
  209. ])
  210. expect(PACKAGE_DEPENDENCY_POLICY.peerRequiredHostExports['@deepseek-ai/dsh-typert-protocol']).toBeUndefined()
  211. })
  212. it('discovers the Client directory, dsh.client declarations, and configured Host packages', () => {
  213. const packages = [
  214. pkg('@f/static', 'packages/client/static/package.json'),
  215. pkg('@f/dynamic-client', 'packages/client/dynamic/package.json', { dsh: { client: {} } }),
  216. pkg('@f/dual', 'packages/api/dual/package.json', { dsh: { client: {} } }),
  217. pkg('@f/export-only', 'packages/api/export-only/package.json', { exports: { './client': './lib/client.js' } }),
  218. pkg('@f/forced-client', 'packages/api/forced/package.json'),
  219. pkg('@f/excluded', 'packages/api/excluded/package.json', { dsh: { client: {} } }),
  220. pkg('@f/host', 'packages/core/host/package.json'),
  221. ]
  222. const found = discoverPackageDependencyScope(packages, policy({
  223. clientFaceInclude: ['@f/forced-client'],
  224. clientFaceExclude: ['@f/excluded'],
  225. hostPackages: ['@f/host'],
  226. }))
  227. expect(found.violations).toEqual([])
  228. expect(found.selected.map(item => [item.name, item.role])).toEqual([
  229. ['@f/dual', 'client-host'],
  230. ['@f/forced-client', 'client-host'],
  231. ['@f/dynamic-client', 'client-host'],
  232. ['@f/static', 'client-only'],
  233. ['@f/host', 'configured-host'],
  234. ])
  235. })
  236. it('rejects stale, redundant, overlapping, and unknown configuration', () => {
  237. const packages = [
  238. pkg('@f/client', 'packages/client/client/package.json'),
  239. pkg('@f/dual', 'packages/api/dual/package.json', { dsh: { client: {} } }),
  240. pkg('@f/host', 'packages/core/host/package.json'),
  241. ]
  242. const found = discoverPackageDependencyScope(packages, policy({
  243. clientFaceInclude: ['@f/dual', '@f/missing', '@f/host'],
  244. clientFaceExclude: ['@f/client', '@f/host', '@f/missing'],
  245. hostPackages: ['@f/dual'],
  246. }))
  247. expect(found.violations).toEqual(expect.arrayContaining([
  248. expect.stringContaining('clientFaceInclude redundantly names automatically discovered package @f/dual'),
  249. expect.stringContaining('@f/host appears in both clientFaceInclude and clientFaceExclude'),
  250. expect.stringContaining('clientFaceExclude cannot exempt packages/client package @f/client'),
  251. expect.stringContaining('clientFaceExclude names @f/host, which declares no dsh.client entry'),
  252. expect.stringContaining('hostPackages redundantly names Client-faced package @f/dual'),
  253. expect.stringContaining('unknown release package @f/missing'),
  254. ]))
  255. })
  256. it('rejects stale, duplicate, and unbounded safe Host export entries', () => {
  257. const { provider, workspaceNames, consumerFacts } = hostRuntimeFixture()
  258. expect(collectHostDependencyExportPolicyViolations(
  259. [consumerFacts],
  260. workspaceNames,
  261. {
  262. safeHostDependencyExports: {
  263. [`${provider.name}/api`]: ['safeValue', 'safeValue', '*', 'staleValue'],
  264. },
  265. peerRequiredHostExports: {
  266. [`${provider.name}/api`]: ['safeValue'],
  267. },
  268. },
  269. )).toEqual(expect.arrayContaining([
  270. expect.stringContaining('export safeValue more than once'),
  271. expect.stringContaining('cannot classify unbounded'),
  272. expect.stringContaining('unused @f/provider/api export staleValue'),
  273. expect.stringContaining('appears in both Host export classifications'),
  274. ]))
  275. })
  276. it('applies a duplicate-safe package classification to its subpaths', () => {
  277. const { provider, workspaceNames, consumerFacts } = hostRuntimeFixture()
  278. expect(collectHostDependencyExportPolicyViolations(
  279. [consumerFacts],
  280. workspaceNames,
  281. {
  282. duplicateSafePackages: [provider.name],
  283. safeHostDependencyExports: {},
  284. peerRequiredHostExports: {},
  285. },
  286. )).toEqual([])
  287. expect(collectHostDependencyExportPolicyViolations(
  288. [consumerFacts],
  289. workspaceNames,
  290. {
  291. duplicateSafePackages: [provider.name],
  292. safeHostDependencyExports: { [`${provider.name}/api`]: ['safeValue'] },
  293. peerRequiredHostExports: {},
  294. },
  295. )).toContain(`safeHostDependencyExports redundantly classifies duplicate-install-safe package ${provider.name}/api`)
  296. })
  297. })
  298. describe('face-aware source classification', () => {
  299. it('keeps generated Host schema imports in dependencies without reading or writing lib', () => {
  300. const { root, manifestPath, source } = generatedHostFixture('schema')
  301. const before = readFileSync(join(root, manifestPath), 'utf8')
  302. const state = readPackageDependencyState(root, policy())
  303. const subject = state.facts[0]
  304. if (subject === undefined) throw new Error('generated Host fixture was not classified')
  305. expect(subject.allSourceUses.has('zod')).toBe(false)
  306. expect(subject.hostRuntimeExportUses).toContainEqual(expect.objectContaining({
  307. packageName: 'zod', specifier: 'zod', exportName: 'z',
  308. sourcePath: 'packages/client/probe/lib/typert.host.js',
  309. }))
  310. const standalone = readPackageDependencyFacts(root, pkg('@fixture/generated', manifestPath, subject.manifest),
  311. subject.role, state.workspaceNames, policy())
  312. expect(standalone.hostRuntimeExportUses).toEqual(subject.hostRuntimeExportUses)
  313. expect(collectPackageDependencyViolations(state)).toEqual([])
  314. repairPackageDependencyManifest(subject)
  315. expect(subject.manifest.dependencies?.zod).toBe('^4.0.0')
  316. expect(subject.manifest.devDependencies?.zod).toBeUndefined()
  317. delete subject.manifest.dependencies?.zod
  318. subject.manifest.devDependencies = { ...subject.manifest.devDependencies, zod: '^4.0.0' }
  319. expect(collectPackageDependencyViolations(state)).toContainEqual(
  320. expect.stringContaining('must be dependencies-only; found devDependencies'),
  321. )
  322. repairPackageDependencyManifest(subject)
  323. expect(subject.manifest.dependencies?.zod).toBe('^4.0.0')
  324. expect(subject.manifest.devDependencies?.zod).toBeUndefined()
  325. delete subject.manifest.dependencies?.zod
  326. expect(() => { repairPackageDependencyManifest(subject) }).toThrow('undeclared third-party dependency zod')
  327. expect(existsSync(join(root, 'packages/client/probe/lib'))).toBe(false)
  328. expect(readFileSync(join(root, manifestPath), 'utf8')).toBe(before)
  329. expect(readFileSync(join(root, 'packages/client/probe/src/index.ts'), 'utf8')).toBe(source)
  330. })
  331. it('does not infer a zod runtime dependency from a metadata-only Typert export', () => {
  332. const { root } = generatedHostFixture('object')
  333. const state = readPackageDependencyState(root, policy())
  334. const subject = state.facts[0]
  335. if (subject === undefined) throw new Error('generated Host fixture was not classified')
  336. expect(subject.hostRuntimeSourceUses.has('zod')).toBe(false)
  337. expect(expectedPackageDependencies(subject).get('zod')?.section).toBe('devDependencies')
  338. repairPackageDependencyManifest(subject)
  339. expect(subject.manifest.dependencies?.zod).toBeUndefined()
  340. expect(subject.manifest.devDependencies?.zod).toBe('^4.0.0')
  341. expect(existsSync(join(root, 'packages/client/probe/lib'))).toBe(false)
  342. })
  343. it('rejects a declared Host Typert module absent from the Host program', () => {
  344. const { root } = generatedHostFixture('schema')
  345. rmSync(join(root, 'tsconfig.host.json'))
  346. expect(() => readPackageDependencyState(root, policy())).toThrow(
  347. 'packages/client/probe/package.json: declared Host Typert export has no generated module',
  348. )
  349. expect(existsSync(join(root, 'packages/client/probe/lib'))).toBe(false)
  350. })
  351. it('propagates generator publication errors without writing or repairing manifests', () => {
  352. const { root, manifestPath } = generatedHostFixture('schema')
  353. const manifest = JSON.parse(readFileSync(join(root, manifestPath), 'utf8')) as { files: string[] }
  354. manifest.files = []
  355. const before = JSON.stringify(manifest)
  356. writeFileSync(join(root, manifestPath), before)
  357. expect(() => readPackageDependencyState(root, policy())).toThrow(
  358. 'package files must include lib/typert.host.js',
  359. )
  360. expect(readFileSync(join(root, manifestPath), 'utf8')).toBe(before)
  361. expect(existsSync(join(root, 'packages/client/probe/lib'))).toBe(false)
  362. })
  363. it('counts browser imports, JSX, type-only references, and augmentations as development inputs', () => {
  364. const subject = sourceFacts({
  365. 'src/index.ts': [
  366. "import { readFile } from 'node:fs'",
  367. "import { join } from 'path'",
  368. "import type { HostType } from 'host-types'",
  369. "import { type MixedType } from 'mixed-types'",
  370. "import type { Hidden } from './type-helper.ts'",
  371. ].join('\n'),
  372. 'src/type-helper.ts': "import { hidden } from 'hidden-value'; export type Hidden = typeof hidden",
  373. 'src/client/index.tsx': [
  374. "import { browser } from '@browser/kit/subpath'",
  375. "import 'react-dom/client'",
  376. "import '#local'",
  377. "import 'https://example.test/browser.js'",
  378. 'export const view = <div />',
  379. ].join('\n'),
  380. 'src/client/augmentation.d.ts': [
  381. "declare module 'augmented' { interface Extra {} }",
  382. "declare module '*.css' {}",
  383. "declare module '*.module.css' {}",
  384. ].join('\n'),
  385. })
  386. expect([...subject.hostRuntimeSourceUses]).toEqual([])
  387. expect([...expectedPackageDependencies(subject)].map(([name, rule]) => [name, rule.section]).sort()).toEqual([
  388. ['@browser/kit', 'devDependencies'],
  389. [CORDIS, 'peer-dev'],
  390. ['augmented', 'devDependencies'],
  391. ['hidden-value', 'devDependencies'],
  392. ['host-types', 'devDependencies'],
  393. ['mixed-types', 'devDependencies'],
  394. ['react', 'devDependencies'],
  395. ['react-dom', 'devDependencies'],
  396. ])
  397. })
  398. it.each(['client-host', 'configured-host'] as const)('retains Host and shared third-party values in dependencies for %s', (role) => {
  399. const subject = sourceFacts({
  400. 'src/index.ts': "import 'host-only'; export { shared } from './nested.ts'",
  401. 'src/nested.ts': "export { shared } from 'shared-runtime'",
  402. 'src/client/index.ts': "import 'browser-only'; import 'shared-runtime'",
  403. }, {}, role)
  404. const expected = expectedPackageDependencies(subject)
  405. expect(expected.get('browser-only')?.section).toBe('devDependencies')
  406. expect(expected.get('host-only')?.section).toBe('dependencies')
  407. expect(expected.get('shared-runtime')?.section).toBe('dependencies')
  408. })
  409. it('uses declared DefinitelyTyped providers only for erased source references', () => {
  410. const subject = sourceFacts({
  411. 'src/index.ts': "import type { ReactNode } from 'react'",
  412. 'src/client/index.ts': "import type { Root } from 'mdast'; import type { Kind } from '@scope/types'",
  413. }, {
  414. dependencies: { '@types/mdast': '^4.0.0' },
  415. devDependencies: { '@types/react': '^18.0.0', '@types/scope__types': '^1.0.0' },
  416. })
  417. expect([...subject.allSourceUses.keys()].sort()).toEqual(['@types/mdast', '@types/react', '@types/scope__types'])
  418. expect([...subject.hostRuntimeSourceUses]).toEqual([])
  419. repairPackageDependencyManifest(subject)
  420. expect(subject.manifest.devDependencies?.['@types/mdast']).toBe('^4.0.0')
  421. expect(subject.manifest.dependencies?.['@types/mdast']).toBeUndefined()
  422. expect(subject.manifest.devDependencies?.mdast).toBeUndefined()
  423. })
  424. it.each(["import 'runtime-library'", 'export const view = <div />'])('does not let type providers satisfy runtime imports or JSX: %s', (source) => {
  425. const name = source.includes('<div') ? 'react' : 'runtime-library'
  426. const subject = sourceFacts({
  427. 'src/index.ts': 'export function apply() {}',
  428. 'src/client/index.tsx': source,
  429. }, { devDependencies: { [`@types/${name}`]: '^1.0.0' } })
  430. expect(subject.allSourceUses.has(name)).toBe(true)
  431. expect(() => { repairPackageDependencyManifest(subject) }).toThrow(`undeclared third-party dependency ${name}`)
  432. })
  433. it('does not treat static browser library entries as Host modules', () => {
  434. const subject = sourceFacts({
  435. 'src/index.tsx': "import 'static-input'; export const view = <div />",
  436. 'src/invariant.ts': "import 'browser-companion'",
  437. }, {
  438. exports: {
  439. '.': { types: './lib/types/index.d.ts', default: './lib/index.js' },
  440. './invariant': { types: './lib/types/invariant.d.ts', default: './lib/invariant.js' },
  441. },
  442. }, 'client-only')
  443. expect([...subject.hostRuntimeSourceUses]).toEqual([])
  444. for (const name of ['static-input', 'react', 'browser-companion']) {
  445. expect(expectedPackageDependencies(subject).get(name)?.section).toBe('devDependencies')
  446. }
  447. })
  448. it('scans published Node companions, conditional entries, and emitted-tree subpaths from source', () => {
  449. const subject = sourceFacts({
  450. 'src/index.ts': 'export function apply() {}',
  451. 'src/invariant.ts': "import 'invariant-runtime'; import type { Kind } from 'invariant-types'",
  452. 'src/node/helper.ts': "export { helper } from 'node-helper'",
  453. 'src/node.mts': "import 'node-import'",
  454. 'src/node.cts': "require('node-require')",
  455. 'src/emitted.tsx': 'export const view = <div />',
  456. 'src/worker/one.ts': "import 'worker-one'",
  457. 'src/worker/two.ts': "import('worker-two')",
  458. 'src/client/index.ts': "import 'browser-only'",
  459. 'src/types-only.ts': "import 'type-export-only'",
  460. }, {
  461. exports: {
  462. '.': { types: './lib/types/index.d.ts', default: './lib/index.js' },
  463. './invariant': { types: './lib/types/invariant.d.ts', default: './lib/invariant.js' },
  464. './renamed': { types: './lib/types/node/helper.d.ts', default: './lib/node-bundle.js' },
  465. './conditional': { browser: './lib/browser.js', node: { import: './lib/node.mjs', require: './lib/node.cjs' } },
  466. './emitted': { types: './lib/types/emitted.d.ts', default: './lib/types/emitted.js' },
  467. './worker/*': './lib/worker/*.js',
  468. './client': { types: './lib/types/client/index.d.ts', default: './lib/client.js' },
  469. './client/extra': './lib/missing-browser.js',
  470. './types-only': { types: './lib/types/types-only.d.ts' },
  471. './src/*': './src/*',
  472. './package.json': './package.json',
  473. './disabled': null,
  474. },
  475. })
  476. expect([...subject.hostRuntimeSourceUses.keys()].sort()).toEqual([
  477. 'invariant-runtime', 'node-helper', 'node-import', 'node-require', 'react', 'worker-one', 'worker-two',
  478. ])
  479. const expected = expectedPackageDependencies(subject)
  480. for (const name of subject.hostRuntimeSourceUses.keys()) expect(expected.get(name)?.section).toBe('dependencies')
  481. for (const name of ['browser-only', 'invariant-types', 'type-export-only']) {
  482. expect(expected.get(name)?.section).toBe('devDependencies')
  483. }
  484. })
  485. it.each([
  486. './lib/missing.js',
  487. { types: './lib/types/missing.d.ts', default: './lib/renamed.js' },
  488. ['./lib/missing.cjs'],
  489. './lib/missing/*.js',
  490. ])('rejects a published Node entry with no matching source: %j', (target) => {
  491. expect(() => sourceFacts({ 'src/index.ts': 'export function apply() {}' }, {
  492. exports: { './node': target },
  493. })).toThrow('Host export ./node has no source entry')
  494. })
  495. it('rejects a Node export outside the source mapping', () => {
  496. expect(() => sourceFacts({ 'src/index.ts': 'export function apply() {}' }, {
  497. exports: { './node': './other/node.js' },
  498. })).toThrow('Host export ./node cannot map ./other/node.js to a source entry')
  499. })
  500. it('fails when a managed Host package has no Host entry', () => {
  501. const root = mkdtempSync(join(tmpdir(), 'dsh-package-missing-host-'))
  502. roots.push(root)
  503. const subject = pkg('@f/host', 'packages/g/host/package.json')
  504. expect(() => readPackageDependencyFacts(root, subject, 'configured-host', new Set([subject.name])))
  505. .toThrow('packages/g/host/package.json: Host runtime entry packages/g/host/src/index.ts does not exist')
  506. })
  507. it('counts Host values as dependencies and Client values as development inputs', () => {
  508. const root = mkdtempSync(join(tmpdir(), 'dsh-package-faces-'))
  509. roots.push(root)
  510. const subject = pkg('@f/dual', 'packages/g/dual/package.json', {
  511. dsh: { client: { inject: ['@f/injected'] } },
  512. })
  513. const files = {
  514. 'packages/g/dual/src/index.ts': [
  515. "import { value } from '@f/runtime'",
  516. "import type { Shared } from '@f/types'",
  517. "import type { Hidden } from './types.ts'",
  518. "export { nested } from './nested.ts'",
  519. ].join('\n'),
  520. 'packages/g/dual/src/nested.ts': "export { nested } from '@f/nested'",
  521. 'packages/g/dual/src/types.ts': "import { hidden } from '@f/hidden'; export type Hidden = typeof hidden",
  522. 'packages/g/dual/src/client/index.ts': "import { browser } from '@f/browser'",
  523. }
  524. for (const [path, source] of Object.entries(files)) {
  525. mkdirSync(dirname(join(root, path)), { recursive: true })
  526. writeFileSync(join(root, path), source)
  527. }
  528. const found = readPackageDependencyFacts(root, subject, 'client-host', new Set([
  529. CORDIS, '@f/runtime', '@f/types', '@f/nested', '@f/hidden', '@f/browser', '@f/injected',
  530. ]), policy({
  531. configurationOnlyDevDependencies: { '@f/dual': ['@f/injected'] },
  532. }))
  533. expect([...found.hostRuntimeSourceUses.keys()].sort()).toEqual(['@f/nested', '@f/runtime'])
  534. expect([...found.configurationOnlyDevDependencies]).toEqual(['@f/injected'])
  535. expect(found.hostRuntimeExportUses).toEqual([
  536. {
  537. packageName: '@f/nested',
  538. specifier: '@f/nested',
  539. exportName: 'nested',
  540. sourcePath: 'packages/g/dual/src/nested.ts',
  541. line: 1,
  542. column: 10,
  543. sourceLine: "export { nested } from '@f/nested'",
  544. },
  545. {
  546. packageName: '@f/runtime',
  547. specifier: '@f/runtime',
  548. exportName: 'value',
  549. sourcePath: 'packages/g/dual/src/index.ts',
  550. line: 1,
  551. column: 10,
  552. sourceLine: "import { value } from '@f/runtime'",
  553. },
  554. ])
  555. expect([...found.allSourceUses.keys()].sort()).toEqual([
  556. '@f/browser', '@f/hidden', '@f/nested', '@f/runtime', '@f/types',
  557. ])
  558. })
  559. it('identifies exact runtime exports without treating type imports as values', () => {
  560. const source = [
  561. "import defaultValue, { value as local, type Kind } from '@f/root'",
  562. "import * as namespace from '@f/namespace'",
  563. "import '@f/effect'",
  564. "import type { TypeOnly } from '@f/types'",
  565. "export { source as renamed, type SourceType } from '@f/reexport'",
  566. "export * from '@f/star'",
  567. "void import('@f/dynamic')",
  568. "void require('@f/required')",
  569. "import { createLazyRequire as lazy } from '@deepseek-ai/dsh-lazy-require'",
  570. "import * as lazyModule from '@deepseek-ai/dsh-lazy-require'",
  571. "void lazy('@f/lazy', import.meta.url)",
  572. "void lazyModule.createLazyRequire('@f/lazy-namespace', import.meta.url)",
  573. 'void defaultValue; void local; void namespace',
  574. ].join('\n')
  575. const uses = collectRuntimeSourceExportUses('probe.ts', source)
  576. expect(uses.map(({ specifier, exportName }) => ({ specifier, exportName }))).toEqual([
  577. { specifier: '@deepseek-ai/dsh-lazy-require', exportName: '*' },
  578. { specifier: '@deepseek-ai/dsh-lazy-require', exportName: 'createLazyRequire' },
  579. { specifier: '@f/dynamic', exportName: '*' },
  580. { specifier: '@f/effect', exportName: '(side effect)' },
  581. { specifier: '@f/lazy', exportName: '*' },
  582. { specifier: '@f/lazy-namespace', exportName: '*' },
  583. { specifier: '@f/namespace', exportName: '*' },
  584. { specifier: '@f/reexport', exportName: 'source' },
  585. { specifier: '@f/required', exportName: '*' },
  586. { specifier: '@f/root', exportName: 'default' },
  587. { specifier: '@f/root', exportName: 'value' },
  588. { specifier: '@f/star', exportName: '*' },
  589. ])
  590. expect(uses.find(use => use.specifier === '@f/root' && use.exportName === 'value')).toMatchObject({
  591. line: 1,
  592. column: 24,
  593. sourceLine: "import defaultValue, { value as local, type Kind } from '@f/root'",
  594. })
  595. })
  596. })
  597. describe('dependency sections', () => {
  598. it.each(['client-only', 'client-host'] as const)('moves unused third-party and CSS inputs to development dependencies for %s', (role) => {
  599. const subject = sourceFacts({
  600. 'src/index.ts': "import 'host-runtime'",
  601. }, {
  602. dependencies: { 'unused-browser-dep': '^1.2.3', '@fontsource/test-font': '~2.0.0', 'host-runtime': '^3.0.0' },
  603. optionalDependencies: { 'unused-optional': '^4.0.0' },
  604. }, role)
  605. repairPackageDependencyManifest(subject)
  606. expect(subject.manifest.devDependencies).toMatchObject({
  607. 'unused-browser-dep': '^1.2.3',
  608. '@fontsource/test-font': '~2.0.0',
  609. 'unused-optional': '^4.0.0',
  610. })
  611. expect(subject.manifest.dependencies).toEqual(role === 'client-host' ? { 'host-runtime': '^3.0.0' } : undefined)
  612. expect(subject.manifest.optionalDependencies).toBeUndefined()
  613. const repaired = structuredClone(subject.manifest)
  614. repairPackageDependencyManifest(subject)
  615. expect(subject.manifest).toEqual(repaired)
  616. })
  617. it('preserves unreferenced third-party declarations in configured Host packages', () => {
  618. const subject = sourceFacts({ 'src/index.ts': 'export function apply() {}' }, {
  619. dependencies: { 'unused-host-dep': '^1.0.0' },
  620. optionalDependencies: { 'unused-host-optional': '^2.0.0' },
  621. }, 'configured-host')
  622. repairPackageDependencyManifest(subject)
  623. expect(subject.manifest.dependencies).toEqual({ 'unused-host-dep': '^1.0.0' })
  624. expect(subject.manifest.optionalDependencies).toEqual({ 'unused-host-optional': '^2.0.0' })
  625. })
  626. it.each(['peerDependencies', 'optionalDependencies'] as const)('rejects browser-only imports declared in %s', (section) => {
  627. const subject = sourceFacts({
  628. 'src/index.ts': 'export function apply() {}',
  629. 'src/client/index.ts': "import 'external'",
  630. }, {
  631. devDependencies: { [CORDIS]: 'workspace:^' },
  632. peerDependencies: { [CORDIS]: 'workspace:^' },
  633. [section]: { [CORDIS]: 'workspace:^', external: '~1.2.3' },
  634. peerDependenciesMeta: { external: { optional: true } },
  635. })
  636. if (section === 'optionalDependencies') delete subject.manifest.optionalDependencies?.[CORDIS]
  637. const state = { facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames }
  638. expect(collectPackageDependencyViolations(state)).toContainEqual(
  639. expect.stringContaining(`must be devDependencies-only; found ${section}`),
  640. )
  641. repairPackageDependencyManifest(subject)
  642. expect(subject.manifest.devDependencies?.external).toBe('~1.2.3')
  643. expect(subject.manifest[section]?.external).toBeUndefined()
  644. expect(subject.manifest.peerDependenciesMeta).toBeUndefined()
  645. expect(collectPackageDependencyViolations(state)).toEqual([])
  646. })
  647. it('rejects a missing third-party declaration and leaves the in-memory manifest unchanged', () => {
  648. const subject = sourceFacts({
  649. 'src/index.ts': 'export function apply() {}',
  650. 'src/client/index.ts': "import 'undeclared'",
  651. })
  652. const before = structuredClone(subject.manifest)
  653. const state = { facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames }
  654. expect(collectPackageDependencyViolations(state)).toContain(
  655. 'packages/g/probe/package.json: undeclared (packages/g/probe/src/client/index.ts) '
  656. + 'must be devDependencies-only; found no dependency section',
  657. )
  658. expect(() => { repairPackageDependencyManifest(subject) }).toThrow(
  659. 'packages/g/probe/package.json: cannot repair undeclared third-party dependency undeclared; declare its version range first',
  660. )
  661. expect(subject.manifest).toEqual(before)
  662. })
  663. it('validates every third-party range before writing any manifest in a repair batch', () => {
  664. const root = mkdtempSync(join(tmpdir(), 'dsh-dependency-batch-'))
  665. roots.push(root)
  666. const valid = { ...facts({ name: '@deepseek-ai/dsh-first' }), manifestPath: 'first.json' }
  667. const base = facts({ name: '@deepseek-ai/dsh-second' })
  668. const invalid: PackageDependencyFacts = {
  669. ...base,
  670. manifestPath: 'second.json',
  671. allSourceUses: new Map([...base.allSourceUses, ['undeclared', ['src/client/index.ts']]]),
  672. }
  673. const subjects = [valid, invalid]
  674. const originals = subjects.map(subject => ({ subject, content: `${JSON.stringify(subject.manifest)}\n` }))
  675. for (const { subject, content } of originals) writeFileSync(join(root, subject.manifestPath), content)
  676. const state = { facts: subjects, packages: [], policyViolations: [], workspaceNames: valid.workspaceNames }
  677. expect(fixPackageDependencies(root, { ...state, policyViolations: ['unclassified Host export'] })).toEqual([])
  678. expect(() => fixPackageDependencies(root, state)).toThrow(
  679. 'second.json: cannot repair undeclared third-party dependency undeclared; declare its version range first',
  680. )
  681. for (const { subject, content } of originals) {
  682. expect(readFileSync(join(root, subject.manifestPath), 'utf8')).toBe(content)
  683. expect(`${JSON.stringify(subject.manifest)}\n`).toBe(content)
  684. }
  685. })
  686. it('moves browser-only third-party imports to development dependencies without changing their ranges', () => {
  687. const manifest: PackageDependencyManifest = {
  688. name: '@deepseek-ai/dsh-probe',
  689. dependencies: { '@deepseek-ai/dsh-runtime': 'workspace:^', external: '^1.2.3' },
  690. devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-types': 'workspace:^' },
  691. peerDependencies: { [CORDIS]: 'workspace:^' },
  692. }
  693. const base = facts(manifest)
  694. const subject: PackageDependencyFacts = {
  695. ...base,
  696. allSourceUses: new Map([...base.allSourceUses, ['external', ['packages/core/probe/src/client/index.ts']]]),
  697. }
  698. const state = { facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames }
  699. expect(collectPackageDependencyViolations(state)).toEqual([
  700. 'packages/core/probe/package.json: external (packages/core/probe/src/client/index.ts) '
  701. + 'must be devDependencies-only; found dependencies',
  702. ])
  703. repairPackageDependencyManifest(subject)
  704. expect(manifest.dependencies?.external).toBeUndefined()
  705. expect(manifest.devDependencies?.external).toBe('^1.2.3')
  706. expect(collectPackageDependencyViolations(state)).toEqual([])
  707. const repaired = structuredClone(manifest)
  708. repairPackageDependencyManifest(subject)
  709. expect(manifest).toEqual(repaired)
  710. })
  711. it('does not leak repository configuration into captured dependency facts', () => {
  712. const manifest: PackageDependencyManifest = {
  713. name: '@deepseek-ai/dsh-client-locale',
  714. dependencies: { '@deepseek-ai/dsh-runtime': 'workspace:^' },
  715. devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-types': 'workspace:^' },
  716. peerDependencies: { [CORDIS]: 'workspace:^' },
  717. }
  718. const base = facts(manifest)
  719. const subject: PackageDependencyFacts = {
  720. ...base,
  721. workspaceNames: new Set([...base.workspaceNames, '@deepseek-ai/dsh-api-remotes']),
  722. }
  723. expect(collectPackageDependencyViolations({
  724. facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames,
  725. })).toEqual([])
  726. })
  727. it('requires non-workspace Host runtime imports in dependencies', () => {
  728. const manifest: PackageDependencyManifest = {
  729. name: '@deepseek-ai/dsh-probe',
  730. dependencies: { '@deepseek-ai/dsh-runtime': 'workspace:^' },
  731. devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-types': 'workspace:^', external: '^1.0.0' },
  732. peerDependencies: { [CORDIS]: 'workspace:^' },
  733. }
  734. const subject: PackageDependencyFacts = {
  735. ...facts(manifest),
  736. hostRuntimeSourceUses: new Map([
  737. ['@deepseek-ai/dsh-runtime', ['packages/core/probe/src/index.ts']],
  738. ['external', ['packages/core/probe/src/index.ts']],
  739. ]),
  740. allSourceUses: new Map([
  741. ...facts(manifest).allSourceUses,
  742. ['external', ['packages/core/probe/src/client/index.ts']],
  743. ]),
  744. }
  745. const state = {
  746. facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames,
  747. }
  748. expect(collectPackageDependencyViolations(state)).toContain(
  749. 'packages/core/probe/package.json: external (packages/core/probe/src/client/index.ts, packages/core/probe/src/index.ts) '
  750. + 'must be dependencies-only; found devDependencies',
  751. )
  752. repairPackageDependencyManifest(subject)
  753. expect(manifest.dependencies?.external).toBe('^1.0.0')
  754. expect(manifest.devDependencies?.external).toBeUndefined()
  755. const repaired = structuredClone(manifest)
  756. repairPackageDependencyManifest(subject)
  757. expect(manifest).toEqual(repaired)
  758. delete manifest.dependencies?.external
  759. expect(collectPackageDependencyViolations(state)).toContain(
  760. 'packages/core/probe/package.json: external (packages/core/probe/src/client/index.ts, packages/core/probe/src/index.ts) '
  761. + 'must be dependencies-only; found no dependency section',
  762. )
  763. })
  764. it('accepts Host dependencies, development-only inputs, and shared Cordis', () => {
  765. const manifest: PackageDependencyManifest = {
  766. name: '@deepseek-ai/dsh-probe',
  767. dependencies: {
  768. '@deepseek-ai/dsh-runtime': 'workspace:^',
  769. '@deepseek-ai/schemastery': 'workspace:^',
  770. external: '^1.0.0',
  771. },
  772. devDependencies: {
  773. '@deepseek-ai/dsh-types': 'workspace:^',
  774. [CORDIS]: 'workspace:^',
  775. },
  776. peerDependencies: { [CORDIS]: 'workspace:^' },
  777. }
  778. expect(collectPackageDependencyViolations({
  779. facts: [facts(manifest)], packages: [], policyViolations: [], workspaceNames: facts(manifest).workspaceNames,
  780. })).toEqual([])
  781. })
  782. it('lists managed Host runtime dependencies for fix review', () => {
  783. const subject = facts({ name: '@deepseek-ai/dsh-probe' })
  784. expect(formatManagedRuntimeDependencies({
  785. facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames,
  786. })).toEqual([
  787. 'verify-package-dependencies: 1 managed Host runtime edge(s) remain in dependencies across 1 package(s):',
  788. ' @deepseek-ai/dsh-probe -> @deepseek-ai/dsh-runtime: @deepseek-ai/dsh-runtime#runtimeValue',
  789. ])
  790. })
  791. it('reports an unapproved Host runtime export without rewriting its dependency section', () => {
  792. const manifest: PackageDependencyManifest = {
  793. name: '@deepseek-ai/dsh-probe',
  794. dependencies: { '@deepseek-ai/dsh-runtime': 'workspace:^' },
  795. devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-types': 'workspace:^' },
  796. peerDependencies: { [CORDIS]: 'workspace:^' },
  797. }
  798. const subject = facts(manifest)
  799. const safetyViolations = collectHostDependencyExportPolicyViolations(
  800. [subject],
  801. subject.workspaceNames,
  802. { safeHostDependencyExports: {}, peerRequiredHostExports: {} },
  803. )
  804. const state = {
  805. facts: [subject], packages: [], policyViolations: safetyViolations, workspaceNames: subject.workspaceNames,
  806. }
  807. expect(safetyViolations).toEqual([
  808. 'packages/core/probe/src/index.ts:1:10: @deepseek-ai/dsh-runtime#runtimeValue is not classified as '
  809. + 'safe or peer-required — import { runtimeValue } from \'@deepseek-ai/dsh-runtime\'',
  810. ])
  811. expect(fixPackageDependencies('/unused', state)).toEqual([])
  812. expect(manifest.dependencies).toEqual({ '@deepseek-ai/dsh-runtime': 'workspace:^' })
  813. })
  814. it('keeps an edge as a peer when one imported export requires shared identity', () => {
  815. const manifest: PackageDependencyManifest = {
  816. name: '@deepseek-ai/dsh-probe',
  817. dependencies: { '@deepseek-ai/dsh-runtime': 'workspace:^' },
  818. devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-types': 'workspace:^' },
  819. peerDependencies: { [CORDIS]: 'workspace:^' },
  820. }
  821. const subject: PackageDependencyFacts = {
  822. ...facts(manifest),
  823. peerRequiredHostDependencies: new Set(['@deepseek-ai/dsh-runtime']),
  824. }
  825. expect(collectHostDependencyExportPolicyViolations(
  826. [subject],
  827. subject.workspaceNames,
  828. {
  829. safeHostDependencyExports: {},
  830. peerRequiredHostExports: {
  831. '@deepseek-ai/dsh-runtime': ['runtimeValue'],
  832. },
  833. },
  834. )).toEqual([])
  835. repairPackageDependencyManifest(subject)
  836. expect(manifest.dependencies).toBeUndefined()
  837. expect(manifest.peerDependencies).toMatchObject({
  838. [CORDIS]: 'workspace:^',
  839. '@deepseek-ai/dsh-runtime': 'workspace:^',
  840. })
  841. expect(manifest.devDependencies).toMatchObject({
  842. [CORDIS]: 'workspace:^',
  843. '@deepseek-ai/dsh-runtime': 'workspace:^',
  844. })
  845. expect(formatPeerRequiredRuntimeDependencies({
  846. facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames,
  847. })).toEqual([
  848. 'verify-package-dependencies: 1 Host runtime edge(s) remain in peerDependencies because their exports require shared identity across 1 package(s):',
  849. ' @deepseek-ai/dsh-probe -> @deepseek-ai/dsh-runtime: @deepseek-ai/dsh-runtime#runtimeValue',
  850. ])
  851. })
  852. it('reports wrong sections, workspace ranges, and stale peer metadata', () => {
  853. const manifest: PackageDependencyManifest = {
  854. name: '@deepseek-ai/dsh-probe',
  855. dependencies: { '@deepseek-ai/dsh-types': 'workspace:*' },
  856. devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-runtime': 'workspace:^' },
  857. peerDependencies: { [CORDIS]: 'workspace:*', '@deepseek-ai/dsh-runtime': 'workspace:^' },
  858. peerDependenciesMeta: { '@deepseek-ai/dsh-missing': { optional: true } },
  859. }
  860. const state = {
  861. facts: [facts(manifest)], packages: [], policyViolations: [], workspaceNames: facts(manifest).workspaceNames,
  862. }
  863. const violations = collectPackageDependencyViolations(state)
  864. expect(violations).toEqual(expect.arrayContaining([
  865. expect.stringContaining('@deepseek-ai/dsh-runtime'),
  866. expect.stringContaining('@deepseek-ai/dsh-types'),
  867. expect.stringContaining(`${CORDIS} must be matching peerDependencies + devDependencies`),
  868. expect.stringContaining('dependencies.@deepseek-ai/dsh-types must use workspace:^'),
  869. expect.stringContaining('peerDependenciesMeta.@deepseek-ai/dsh-missing has no matching'),
  870. ]))
  871. })
  872. it('repairs owned relationships without changing unrelated dependencies', () => {
  873. const root = mkdtempSync(join(tmpdir(), 'dsh-package-dependencies-'))
  874. roots.push(root)
  875. const manifestPath = 'package.json'
  876. const manifest: PackageDependencyManifest = {
  877. name: '@deepseek-ai/dsh-probe',
  878. dependencies: { '@deepseek-ai/schemastery': 'workspace:*', external: '^1.0.0' },
  879. devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-runtime': 'workspace:^' },
  880. peerDependencies: {
  881. [CORDIS]: 'workspace:^',
  882. '@deepseek-ai/dsh-runtime': 'workspace:^',
  883. '@deepseek-ai/dsh-stale': 'workspace:^',
  884. },
  885. peerDependenciesMeta: { '@deepseek-ai/dsh-stale': { optional: true } },
  886. }
  887. writeFileSync(join(root, manifestPath), `${JSON.stringify(manifest, null, 2)}\n`)
  888. const subject = { ...facts(manifest), manifestPath }
  889. const state = { facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames }
  890. expect(fixPackageDependencies(root, state)).toEqual([manifestPath])
  891. const fixed = JSON.parse(readFileSync(join(root, manifestPath), 'utf8')) as PackageDependencyManifest
  892. expect(fixed.dependencies).toEqual({
  893. '@deepseek-ai/schemastery': 'workspace:^',
  894. external: '^1.0.0',
  895. '@deepseek-ai/dsh-runtime': 'workspace:^',
  896. })
  897. expect(fixed.devDependencies).toEqual({
  898. [CORDIS]: 'workspace:^',
  899. '@deepseek-ai/dsh-types': 'workspace:^',
  900. '@deepseek-ai/dsh-stale': 'workspace:^',
  901. })
  902. expect(fixed.peerDependencies).toEqual({ [CORDIS]: 'workspace:^' })
  903. expect(fixed.peerDependenciesMeta).toBeUndefined()
  904. })
  905. it('repairs an in-memory manifest for benchmark simulation', () => {
  906. const manifest: PackageDependencyManifest = {
  907. name: '@deepseek-ai/dsh-probe',
  908. peerDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-runtime': 'workspace:^' },
  909. devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-runtime': 'workspace:^' },
  910. }
  911. repairPackageDependencyManifest(facts(manifest))
  912. expect(manifest.dependencies).toEqual({ '@deepseek-ai/dsh-runtime': 'workspace:^' })
  913. expect(manifest.peerDependencies).toEqual({ [CORDIS]: 'workspace:^' })
  914. })
  915. })