verify-package-dependencies.spec.ts 22 KB

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