verify-package-dependencies.spec.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. import { 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. fixPackageDependencies,
  15. formatManagedRuntimeDependencies,
  16. formatPeerRequiredRuntimeDependencies,
  17. readPackageDependencyFacts,
  18. repairPackageDependencyManifest,
  19. type PackageDependencyFacts,
  20. type PackageDependencyManifest,
  21. type WorkspacePackageManifest,
  22. } from './verify-package-dependencies.ts'
  23. const CORDIS = '@deepseek-ai/cordis'
  24. const roots: string[] = []
  25. afterEach(() => {
  26. for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
  27. })
  28. function pkg(
  29. name: string,
  30. manifestPath: string,
  31. manifest: Partial<PackageDependencyManifest> = {},
  32. ): WorkspacePackageManifest {
  33. return {
  34. name,
  35. manifestPath,
  36. dir: dirname(manifestPath),
  37. manifest: { name, ...manifest },
  38. }
  39. }
  40. function policy(fields: Partial<PackageDependencyPolicy> = {}): PackageDependencyPolicy {
  41. return {
  42. clientFaceInclude: [],
  43. clientFaceExclude: [],
  44. hostPackages: [],
  45. configurationOnlyDevDependencies: {},
  46. safeHostDependencyExports: {},
  47. peerRequiredHostExports: {},
  48. ...fields,
  49. }
  50. }
  51. function facts(manifest: PackageDependencyManifest): PackageDependencyFacts {
  52. return {
  53. manifestPath: 'packages/core/probe/package.json',
  54. role: 'configured-host',
  55. manifest,
  56. workspaceNames: new Set([
  57. CORDIS,
  58. '@deepseek-ai/dsh-runtime',
  59. '@deepseek-ai/dsh-types',
  60. '@deepseek-ai/dsh-stale',
  61. '@deepseek-ai/schemastery',
  62. ]),
  63. allSourceUses: new Map([
  64. ['@deepseek-ai/dsh-runtime', ['packages/core/probe/src/index.ts']],
  65. ['@deepseek-ai/dsh-types', ['packages/core/probe/src/types.ts']],
  66. ]),
  67. hostRuntimeSourceUses: new Map([
  68. ['@deepseek-ai/dsh-runtime', ['packages/core/probe/src/index.ts']],
  69. ]),
  70. hostRuntimeExportUses: [{
  71. packageName: '@deepseek-ai/dsh-runtime',
  72. specifier: '@deepseek-ai/dsh-runtime',
  73. exportName: 'runtimeValue',
  74. sourcePath: 'packages/core/probe/src/index.ts',
  75. line: 1,
  76. column: 10,
  77. sourceLine: "import { runtimeValue } from '@deepseek-ai/dsh-runtime'",
  78. }],
  79. peerRequiredHostDependencies: new Set(),
  80. configurationOnlyDevDependencies: new Set(),
  81. clientInject: new Set(),
  82. }
  83. }
  84. function hostRuntimeFixture(): {
  85. provider: WorkspacePackageManifest
  86. workspaceNames: Set<string>
  87. consumerFacts: PackageDependencyFacts
  88. } {
  89. const consumer = pkg('@f/consumer', 'packages/core/consumer/package.json')
  90. const provider = pkg('@f/provider', 'packages/core/provider/package.json')
  91. const sourcePath = 'packages/core/consumer/src/index.ts'
  92. const specifier = `${provider.name}/api`
  93. const workspaceNames = new Set([CORDIS, consumer.name, provider.name])
  94. const consumerFacts: PackageDependencyFacts = {
  95. manifestPath: consumer.manifestPath,
  96. role: 'configured-host',
  97. manifest: consumer.manifest,
  98. workspaceNames,
  99. allSourceUses: new Map(),
  100. hostRuntimeSourceUses: new Map([[provider.name, [sourcePath]]]),
  101. hostRuntimeExportUses: [{
  102. packageName: provider.name,
  103. specifier,
  104. exportName: 'safeValue',
  105. sourcePath,
  106. line: 1,
  107. column: 10,
  108. sourceLine: `import { safeValue } from '${specifier}'`,
  109. }],
  110. peerRequiredHostDependencies: new Set(),
  111. configurationOnlyDevDependencies: new Set(),
  112. clientInject: new Set(),
  113. }
  114. return { provider, workspaceNames, consumerFacts }
  115. }
  116. describe('package dependency scope', () => {
  117. it('keeps the measured Host relay roster explicit', () => {
  118. expect(PACKAGE_DEPENDENCY_POLICY.clientFaceExclude).toEqual([
  119. '@deepseek-ai/dsh-api-session-controller',
  120. '@deepseek-ai/dsh-api-workspace-controller',
  121. ])
  122. expect(PACKAGE_DEPENDENCY_POLICY.hostPackages).toEqual([
  123. '@deepseek-ai/dsh-llm',
  124. '@deepseek-ai/dsh-session',
  125. ])
  126. expect(PACKAGE_DEPENDENCY_POLICY.configurationOnlyDevDependencies).toEqual({
  127. '@deepseek-ai/dsh-client-locale': ['@deepseek-ai/dsh-api-remotes'],
  128. '@deepseek-ai/dsh-client-ui-conversation': [
  129. '@deepseek-ai/dsh-api-remotes',
  130. '@deepseek-ai/dsh-client-ui-workspace',
  131. ],
  132. '@deepseek-ai/dsh-client-ui-model-selection': ['@deepseek-ai/dsh-client-ui-input-trigger'],
  133. '@deepseek-ai/dsh-client-ui-sidebar': ['@deepseek-ai/dsh-client-ui-workspace'],
  134. '@deepseek-ai/dsh-client-ui-subagent': ['@deepseek-ai/dsh-client-ui-input-trigger'],
  135. '@deepseek-ai/dsh-client-ui-theme': ['@deepseek-ai/dsh-api-remotes'],
  136. '@deepseek-ai/dsh-client-ui-tool': ['@deepseek-ai/dsh-api-remotes'],
  137. })
  138. expect(PACKAGE_DEPENDENCY_POLICY.duplicateSafePackages).toEqual([
  139. '@deepseek-ai/dsh-brand',
  140. '@deepseek-ai/dsh-typert-protocol',
  141. '@deepseek-ai/dsh-util-crypto',
  142. '@deepseek-ai/dsh-util-values',
  143. ])
  144. expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/dsh-deque']).toEqual(['Deque'])
  145. expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/schemastery']).toEqual(['default'])
  146. expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/dsh-session/types']).toBeUndefined()
  147. expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/dsh-typert-protocol']).toBeUndefined()
  148. expect(PACKAGE_DEPENDENCY_POLICY.peerRequiredHostExports['@deepseek-ai/dsh-scope']).toEqual([
  149. 'carrierKeyOf', 'scopeOf', 'scopeTarget',
  150. ])
  151. expect(PACKAGE_DEPENDENCY_POLICY.peerRequiredHostExports['@deepseek-ai/dsh-typert-protocol']).toBeUndefined()
  152. })
  153. it('discovers the Client directory, dsh.client declarations, and configured Host packages', () => {
  154. const packages = [
  155. pkg('@f/static', 'packages/client/static/package.json'),
  156. pkg('@f/dynamic-client', 'packages/client/dynamic/package.json', { dsh: { client: {} } }),
  157. pkg('@f/dual', 'packages/api/dual/package.json', { dsh: { client: {} } }),
  158. pkg('@f/export-only', 'packages/api/export-only/package.json', { exports: { './client': './lib/client.js' } }),
  159. pkg('@f/forced-client', 'packages/api/forced/package.json'),
  160. pkg('@f/excluded', 'packages/api/excluded/package.json', { dsh: { client: {} } }),
  161. pkg('@f/host', 'packages/core/host/package.json'),
  162. ]
  163. const found = discoverPackageDependencyScope(packages, policy({
  164. clientFaceInclude: ['@f/forced-client'],
  165. clientFaceExclude: ['@f/excluded'],
  166. hostPackages: ['@f/host'],
  167. }))
  168. expect(found.violations).toEqual([])
  169. expect(found.selected.map(item => [item.name, item.role])).toEqual([
  170. ['@f/dual', 'client-host'],
  171. ['@f/forced-client', 'client-host'],
  172. ['@f/dynamic-client', 'client-host'],
  173. ['@f/static', 'client-only'],
  174. ['@f/host', 'configured-host'],
  175. ])
  176. })
  177. it('rejects stale, redundant, overlapping, and unknown configuration', () => {
  178. const packages = [
  179. pkg('@f/client', 'packages/client/client/package.json'),
  180. pkg('@f/dual', 'packages/api/dual/package.json', { dsh: { client: {} } }),
  181. pkg('@f/host', 'packages/core/host/package.json'),
  182. ]
  183. const found = discoverPackageDependencyScope(packages, policy({
  184. clientFaceInclude: ['@f/dual', '@f/missing', '@f/host'],
  185. clientFaceExclude: ['@f/client', '@f/host', '@f/missing'],
  186. hostPackages: ['@f/dual'],
  187. }))
  188. expect(found.violations).toEqual(expect.arrayContaining([
  189. expect.stringContaining('clientFaceInclude redundantly names automatically discovered package @f/dual'),
  190. expect.stringContaining('@f/host appears in both clientFaceInclude and clientFaceExclude'),
  191. expect.stringContaining('clientFaceExclude cannot exempt packages/client package @f/client'),
  192. expect.stringContaining('clientFaceExclude names @f/host, which declares no dsh.client entry'),
  193. expect.stringContaining('hostPackages redundantly names Client-faced package @f/dual'),
  194. expect.stringContaining('unknown release package @f/missing'),
  195. ]))
  196. })
  197. it('rejects stale, duplicate, and unbounded safe Host export entries', () => {
  198. const { provider, workspaceNames, consumerFacts } = hostRuntimeFixture()
  199. expect(collectHostDependencyExportPolicyViolations(
  200. [consumerFacts],
  201. workspaceNames,
  202. {
  203. safeHostDependencyExports: {
  204. [`${provider.name}/api`]: ['safeValue', 'safeValue', '*', 'staleValue'],
  205. },
  206. peerRequiredHostExports: {
  207. [`${provider.name}/api`]: ['safeValue'],
  208. },
  209. },
  210. )).toEqual(expect.arrayContaining([
  211. expect.stringContaining('export safeValue more than once'),
  212. expect.stringContaining('cannot classify unbounded'),
  213. expect.stringContaining('unused @f/provider/api export staleValue'),
  214. expect.stringContaining('appears in both Host export classifications'),
  215. ]))
  216. })
  217. it('applies a duplicate-safe package classification to its subpaths', () => {
  218. const { provider, workspaceNames, consumerFacts } = hostRuntimeFixture()
  219. expect(collectHostDependencyExportPolicyViolations(
  220. [consumerFacts],
  221. workspaceNames,
  222. {
  223. duplicateSafePackages: [provider.name],
  224. safeHostDependencyExports: {},
  225. peerRequiredHostExports: {},
  226. },
  227. )).toEqual([])
  228. expect(collectHostDependencyExportPolicyViolations(
  229. [consumerFacts],
  230. workspaceNames,
  231. {
  232. duplicateSafePackages: [provider.name],
  233. safeHostDependencyExports: { [`${provider.name}/api`]: ['safeValue'] },
  234. peerRequiredHostExports: {},
  235. },
  236. )).toContain(`safeHostDependencyExports redundantly classifies duplicate-install-safe package ${provider.name}/api`)
  237. })
  238. })
  239. describe('face-aware source classification', () => {
  240. it('fails when a managed Host package has no Host entry', () => {
  241. const root = mkdtempSync(join(tmpdir(), 'dsh-package-missing-host-'))
  242. roots.push(root)
  243. const subject = pkg('@f/host', 'packages/g/host/package.json')
  244. expect(() => readPackageDependencyFacts(root, subject, 'configured-host', new Set([subject.name])))
  245. .toThrow('packages/g/host/package.json: Host runtime entry packages/g/host/src/index.ts does not exist')
  246. })
  247. it('counts Host values as dependencies and Client values as development inputs', () => {
  248. const root = mkdtempSync(join(tmpdir(), 'dsh-package-faces-'))
  249. roots.push(root)
  250. const subject = pkg('@f/dual', 'packages/g/dual/package.json', {
  251. dsh: { client: { inject: ['@f/injected'] } },
  252. })
  253. const files = {
  254. 'packages/g/dual/src/index.ts': [
  255. "import { value } from '@f/runtime'",
  256. "import type { Shared } from '@f/types'",
  257. "import type { Hidden } from './types.ts'",
  258. "export { nested } from './nested.ts'",
  259. ].join('\n'),
  260. 'packages/g/dual/src/nested.ts': "export { nested } from '@f/nested'",
  261. 'packages/g/dual/src/types.ts': "import { hidden } from '@f/hidden'; export type Hidden = typeof hidden",
  262. 'packages/g/dual/src/client/index.ts': "import { browser } from '@f/browser'",
  263. }
  264. for (const [path, source] of Object.entries(files)) {
  265. mkdirSync(dirname(join(root, path)), { recursive: true })
  266. writeFileSync(join(root, path), source)
  267. }
  268. const found = readPackageDependencyFacts(root, subject, 'client-host', new Set([
  269. CORDIS, '@f/runtime', '@f/types', '@f/nested', '@f/hidden', '@f/browser', '@f/injected',
  270. ]), policy({
  271. configurationOnlyDevDependencies: { '@f/dual': ['@f/injected'] },
  272. }))
  273. expect([...found.hostRuntimeSourceUses.keys()].sort()).toEqual(['@f/nested', '@f/runtime'])
  274. expect([...found.configurationOnlyDevDependencies]).toEqual(['@f/injected'])
  275. expect(found.hostRuntimeExportUses).toEqual([
  276. {
  277. packageName: '@f/nested',
  278. specifier: '@f/nested',
  279. exportName: 'nested',
  280. sourcePath: 'packages/g/dual/src/nested.ts',
  281. line: 1,
  282. column: 10,
  283. sourceLine: "export { nested } from '@f/nested'",
  284. },
  285. {
  286. packageName: '@f/runtime',
  287. specifier: '@f/runtime',
  288. exportName: 'value',
  289. sourcePath: 'packages/g/dual/src/index.ts',
  290. line: 1,
  291. column: 10,
  292. sourceLine: "import { value } from '@f/runtime'",
  293. },
  294. ])
  295. expect([...found.allSourceUses.keys()].sort()).toEqual([
  296. '@f/browser', '@f/hidden', '@f/nested', '@f/runtime', '@f/types',
  297. ])
  298. })
  299. it('identifies exact runtime exports without treating type imports as values', () => {
  300. const source = [
  301. "import defaultValue, { value as local, type Kind } from '@f/root'",
  302. "import * as namespace from '@f/namespace'",
  303. "import '@f/effect'",
  304. "import type { TypeOnly } from '@f/types'",
  305. "export { source as renamed, type SourceType } from '@f/reexport'",
  306. "export * from '@f/star'",
  307. "void import('@f/dynamic')",
  308. "void require('@f/required')",
  309. 'void defaultValue; void local; void namespace',
  310. ].join('\n')
  311. const uses = collectRuntimeSourceExportUses('probe.ts', source)
  312. expect(uses.map(({ specifier, exportName }) => ({ specifier, exportName }))).toEqual([
  313. { specifier: '@f/dynamic', exportName: '*' },
  314. { specifier: '@f/effect', exportName: '(side effect)' },
  315. { specifier: '@f/namespace', exportName: '*' },
  316. { specifier: '@f/reexport', exportName: 'source' },
  317. { specifier: '@f/required', exportName: '*' },
  318. { specifier: '@f/root', exportName: 'default' },
  319. { specifier: '@f/root', exportName: 'value' },
  320. { specifier: '@f/star', exportName: '*' },
  321. ])
  322. expect(uses.find(use => use.specifier === '@f/root' && use.exportName === 'value')).toMatchObject({
  323. line: 1,
  324. column: 24,
  325. sourceLine: "import defaultValue, { value as local, type Kind } from '@f/root'",
  326. })
  327. })
  328. })
  329. describe('dependency sections', () => {
  330. it('does not leak repository configuration into captured dependency facts', () => {
  331. const manifest: PackageDependencyManifest = {
  332. name: '@deepseek-ai/dsh-client-locale',
  333. dependencies: { '@deepseek-ai/dsh-runtime': 'workspace:^' },
  334. devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-types': 'workspace:^' },
  335. peerDependencies: { [CORDIS]: 'workspace:^' },
  336. }
  337. const base = facts(manifest)
  338. const subject: PackageDependencyFacts = {
  339. ...base,
  340. workspaceNames: new Set([...base.workspaceNames, '@deepseek-ai/dsh-api-remotes']),
  341. }
  342. expect(collectPackageDependencyViolations({
  343. facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames,
  344. })).toEqual([])
  345. })
  346. it('requires non-workspace Host runtime imports in dependencies', () => {
  347. const manifest: PackageDependencyManifest = {
  348. name: '@deepseek-ai/dsh-probe',
  349. dependencies: { '@deepseek-ai/dsh-runtime': 'workspace:^' },
  350. devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-types': 'workspace:^', external: '^1.0.0' },
  351. peerDependencies: { [CORDIS]: 'workspace:^' },
  352. }
  353. const subject: PackageDependencyFacts = {
  354. ...facts(manifest),
  355. hostRuntimeSourceUses: new Map([
  356. ['@deepseek-ai/dsh-runtime', ['packages/core/probe/src/index.ts']],
  357. ['external', ['packages/core/probe/src/index.ts']],
  358. ]),
  359. }
  360. const state = {
  361. facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames,
  362. }
  363. expect(collectPackageDependencyViolations(state)).toContain(
  364. 'packages/core/probe/package.json: external (packages/core/probe/src/index.ts) '
  365. + 'must be dependencies-only; found devDependencies',
  366. )
  367. repairPackageDependencyManifest(subject)
  368. expect(manifest.dependencies?.external).toBe('^1.0.0')
  369. expect(manifest.devDependencies?.external).toBeUndefined()
  370. delete manifest.dependencies?.external
  371. expect(collectPackageDependencyViolations(state)).toContain(
  372. 'packages/core/probe/package.json: external (packages/core/probe/src/index.ts) '
  373. + 'must be dependencies-only; found no dependency section',
  374. )
  375. })
  376. it('accepts Host dependencies, development-only inputs, and shared Cordis', () => {
  377. const manifest: PackageDependencyManifest = {
  378. name: '@deepseek-ai/dsh-probe',
  379. dependencies: {
  380. '@deepseek-ai/dsh-runtime': 'workspace:^',
  381. '@deepseek-ai/schemastery': 'workspace:^',
  382. external: '^1.0.0',
  383. },
  384. devDependencies: {
  385. '@deepseek-ai/dsh-types': 'workspace:^',
  386. [CORDIS]: 'workspace:^',
  387. },
  388. peerDependencies: { [CORDIS]: 'workspace:^' },
  389. }
  390. expect(collectPackageDependencyViolations({
  391. facts: [facts(manifest)], packages: [], policyViolations: [], workspaceNames: facts(manifest).workspaceNames,
  392. })).toEqual([])
  393. })
  394. it('lists managed Host runtime dependencies for fix review', () => {
  395. const subject = facts({ name: '@deepseek-ai/dsh-probe' })
  396. expect(formatManagedRuntimeDependencies({
  397. facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames,
  398. })).toEqual([
  399. 'verify-package-dependencies: 1 managed Host runtime edge(s) remain in dependencies across 1 package(s):',
  400. ' @deepseek-ai/dsh-probe -> @deepseek-ai/dsh-runtime: @deepseek-ai/dsh-runtime#runtimeValue',
  401. ])
  402. })
  403. it('reports an unapproved Host runtime export without rewriting its dependency section', () => {
  404. const manifest: PackageDependencyManifest = {
  405. name: '@deepseek-ai/dsh-probe',
  406. dependencies: { '@deepseek-ai/dsh-runtime': 'workspace:^' },
  407. devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-types': 'workspace:^' },
  408. peerDependencies: { [CORDIS]: 'workspace:^' },
  409. }
  410. const subject = facts(manifest)
  411. const safetyViolations = collectHostDependencyExportPolicyViolations(
  412. [subject],
  413. subject.workspaceNames,
  414. { safeHostDependencyExports: {}, peerRequiredHostExports: {} },
  415. )
  416. const state = {
  417. facts: [subject], packages: [], policyViolations: safetyViolations, workspaceNames: subject.workspaceNames,
  418. }
  419. expect(safetyViolations).toEqual([
  420. 'packages/core/probe/src/index.ts:1:10: @deepseek-ai/dsh-runtime#runtimeValue is not classified as '
  421. + 'safe or peer-required — import { runtimeValue } from \'@deepseek-ai/dsh-runtime\'',
  422. ])
  423. expect(fixPackageDependencies('/unused', state)).toEqual([])
  424. expect(manifest.dependencies).toEqual({ '@deepseek-ai/dsh-runtime': 'workspace:^' })
  425. })
  426. it('keeps an edge as a peer when one imported export requires shared identity', () => {
  427. const manifest: PackageDependencyManifest = {
  428. name: '@deepseek-ai/dsh-probe',
  429. dependencies: { '@deepseek-ai/dsh-runtime': 'workspace:^' },
  430. devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-types': 'workspace:^' },
  431. peerDependencies: { [CORDIS]: 'workspace:^' },
  432. }
  433. const subject: PackageDependencyFacts = {
  434. ...facts(manifest),
  435. peerRequiredHostDependencies: new Set(['@deepseek-ai/dsh-runtime']),
  436. }
  437. expect(collectHostDependencyExportPolicyViolations(
  438. [subject],
  439. subject.workspaceNames,
  440. {
  441. safeHostDependencyExports: {},
  442. peerRequiredHostExports: {
  443. '@deepseek-ai/dsh-runtime': ['runtimeValue'],
  444. },
  445. },
  446. )).toEqual([])
  447. repairPackageDependencyManifest(subject)
  448. expect(manifest.dependencies).toBeUndefined()
  449. expect(manifest.peerDependencies).toMatchObject({
  450. [CORDIS]: 'workspace:^',
  451. '@deepseek-ai/dsh-runtime': 'workspace:^',
  452. })
  453. expect(manifest.devDependencies).toMatchObject({
  454. [CORDIS]: 'workspace:^',
  455. '@deepseek-ai/dsh-runtime': 'workspace:^',
  456. })
  457. expect(formatPeerRequiredRuntimeDependencies({
  458. facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames,
  459. })).toEqual([
  460. 'verify-package-dependencies: 1 Host runtime edge(s) remain in peerDependencies because their exports require shared identity across 1 package(s):',
  461. ' @deepseek-ai/dsh-probe -> @deepseek-ai/dsh-runtime: @deepseek-ai/dsh-runtime#runtimeValue',
  462. ])
  463. })
  464. it('reports wrong sections, workspace ranges, and stale peer metadata', () => {
  465. const manifest: PackageDependencyManifest = {
  466. name: '@deepseek-ai/dsh-probe',
  467. dependencies: { '@deepseek-ai/dsh-types': 'workspace:*' },
  468. devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-runtime': 'workspace:^' },
  469. peerDependencies: { [CORDIS]: 'workspace:*', '@deepseek-ai/dsh-runtime': 'workspace:^' },
  470. peerDependenciesMeta: { '@deepseek-ai/dsh-missing': { optional: true } },
  471. }
  472. const state = {
  473. facts: [facts(manifest)], packages: [], policyViolations: [], workspaceNames: facts(manifest).workspaceNames,
  474. }
  475. const violations = collectPackageDependencyViolations(state)
  476. expect(violations).toEqual(expect.arrayContaining([
  477. expect.stringContaining('@deepseek-ai/dsh-runtime'),
  478. expect.stringContaining('@deepseek-ai/dsh-types'),
  479. expect.stringContaining(`${CORDIS} must be matching peerDependencies + devDependencies`),
  480. expect.stringContaining('dependencies.@deepseek-ai/dsh-types must use workspace:^'),
  481. expect.stringContaining('peerDependenciesMeta.@deepseek-ai/dsh-missing has no matching'),
  482. ]))
  483. })
  484. it('repairs owned relationships without changing unrelated dependencies', () => {
  485. const root = mkdtempSync(join(tmpdir(), 'dsh-package-dependencies-'))
  486. roots.push(root)
  487. const manifestPath = 'package.json'
  488. const manifest: PackageDependencyManifest = {
  489. name: '@deepseek-ai/dsh-probe',
  490. dependencies: { '@deepseek-ai/schemastery': 'workspace:*', external: '^1.0.0' },
  491. devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-runtime': 'workspace:^' },
  492. peerDependencies: {
  493. [CORDIS]: 'workspace:^',
  494. '@deepseek-ai/dsh-runtime': 'workspace:^',
  495. '@deepseek-ai/dsh-stale': 'workspace:^',
  496. },
  497. peerDependenciesMeta: { '@deepseek-ai/dsh-stale': { optional: true } },
  498. }
  499. writeFileSync(join(root, manifestPath), `${JSON.stringify(manifest, null, 2)}\n`)
  500. const subject = { ...facts(manifest), manifestPath }
  501. const state = { facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames }
  502. expect(fixPackageDependencies(root, state)).toEqual([manifestPath])
  503. const fixed = JSON.parse(readFileSync(join(root, manifestPath), 'utf8')) as PackageDependencyManifest
  504. expect(fixed.dependencies).toEqual({
  505. '@deepseek-ai/schemastery': 'workspace:^',
  506. external: '^1.0.0',
  507. '@deepseek-ai/dsh-runtime': 'workspace:^',
  508. })
  509. expect(fixed.devDependencies).toEqual({
  510. [CORDIS]: 'workspace:^',
  511. '@deepseek-ai/dsh-types': 'workspace:^',
  512. '@deepseek-ai/dsh-stale': 'workspace:^',
  513. })
  514. expect(fixed.peerDependencies).toEqual({ [CORDIS]: 'workspace:^' })
  515. expect(fixed.peerDependenciesMeta).toBeUndefined()
  516. })
  517. it('repairs an in-memory manifest for benchmark simulation', () => {
  518. const manifest: PackageDependencyManifest = {
  519. name: '@deepseek-ai/dsh-probe',
  520. peerDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-runtime': 'workspace:^' },
  521. devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-runtime': 'workspace:^' },
  522. }
  523. repairPackageDependencyManifest(facts(manifest))
  524. expect(manifest.dependencies).toEqual({ '@deepseek-ai/dsh-runtime': 'workspace:^' })
  525. expect(manifest.peerDependencies).toEqual({ [CORDIS]: 'workspace:^' })
  526. })
  527. })