package-invariants.spec.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
  2. import { tmpdir } from 'node:os'
  3. import { join } from 'node:path'
  4. import { afterEach, describe, expect, it } from 'vitest'
  5. import {
  6. collectPackageInvariantViolations,
  7. } from './package-invariants.ts'
  8. import { usesFlattenedPackageDependencies } from './package-dependency-policy.ts'
  9. const roots: string[] = []
  10. afterEach(() => {
  11. for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
  12. })
  13. function handwrittenInvariant(packageName: string): string {
  14. return `
  15. export const name = 'probe-invariant'
  16. export const inject = ['invariants']
  17. const install = (ctx: { on(name: string, listener: (value: number) => void): void }, fail: (message: string) => never) => {
  18. ctx.on('probe/value', (value) => {
  19. if (value < 0) fail('observed values must be non-negative')
  20. })
  21. }
  22. export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) =>
  23. Promise.resolve(ctx.invariants.register(${JSON.stringify(packageName)}, install))
  24. `
  25. }
  26. function fixture(options: {
  27. packageName?: string
  28. packageDirectory?: string
  29. source?: string
  30. clientDeclaration?: boolean
  31. clientExport?: boolean
  32. invariantExport?: boolean
  33. invariantDependency?: boolean
  34. invariantReference?: boolean
  35. buildEntry?: boolean
  36. } = {}): string {
  37. const root = mkdtempSync(join(tmpdir(), 'dsh-package-invariants-'))
  38. roots.push(root)
  39. const packageDirectory = options.packageDirectory ?? 'packages/core/probe'
  40. const dir = join(root, packageDirectory)
  41. mkdirSync(join(dir, 'src'), { recursive: true })
  42. const packageName = options.packageName ?? '@deepseek-ai/dsh-probe'
  43. const exports = options.invariantExport === false ? {} : {
  44. './invariant': {
  45. types: './lib/types/invariant.d.ts',
  46. default: './lib/invariant.js',
  47. },
  48. ...(options.clientExport === true ? {
  49. './client': {
  50. types: './lib/types/client/index.d.ts',
  51. default: './lib/client.js',
  52. },
  53. } : {}),
  54. }
  55. const dsh = options.clientDeclaration === true ? { client: {} } : undefined
  56. const developmentOnlyInvariant = usesFlattenedPackageDependencies(
  57. `${packageDirectory}/package.json`,
  58. packageName,
  59. dsh,
  60. )
  61. const manifest = {
  62. name: packageName,
  63. ...(dsh === undefined ? {} : { dsh }),
  64. exports,
  65. files: ['lib/index.js', 'lib/invariant.js'],
  66. peerDependencies: options.invariantDependency === false || developmentOnlyInvariant ? {} : {
  67. '@deepseek-ai/dsh-invariants': 'workspace:^',
  68. },
  69. devDependencies: options.invariantDependency === false ? {} : {
  70. '@deepseek-ai/dsh-invariants': 'workspace:^',
  71. },
  72. }
  73. writeFileSync(join(dir, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`)
  74. writeFileSync(join(dir, 'tsconfig.json'), `${JSON.stringify({
  75. references: options.invariantReference === false ? [] : [{ path: '../../runtime-diagnostics/invariants' }],
  76. }, null, 2)}\n`)
  77. writeFileSync(join(dir, 'src/invariant.ts'), options.source ?? handwrittenInvariant(packageName))
  78. writeFileSync(
  79. join(dir, 'tsdown.config.ts'),
  80. options.buildEntry === false ? "export default { entry: ['lib/types/index.js'] }\n" : "export default { entry: ['lib/types/index.js', 'lib/types/invariant.js'] }\n",
  81. )
  82. return root
  83. }
  84. describe('package invariant gate', () => {
  85. it('accepts a hand-owned checking companion with publication metadata', () => {
  86. expect(collectPackageInvariantViolations(fixture())).toEqual([])
  87. })
  88. it('accepts development-only invariants for configured Host dependencies', () => {
  89. expect(collectPackageInvariantViolations(fixture({ packageName: '@deepseek-ai/dsh-llm' }))).toEqual([])
  90. })
  91. it('accepts development-only invariants for client packages', () => {
  92. expect(collectPackageInvariantViolations(fixture({
  93. packageName: '@deepseek-ai/dsh-client-probe',
  94. packageDirectory: 'packages/client/probe',
  95. }))).toEqual([])
  96. })
  97. it('accepts development-only invariants for packages with a dsh.client entry', () => {
  98. expect(collectPackageInvariantViolations(fixture({ clientDeclaration: true, clientExport: true }))).toEqual([])
  99. })
  100. it('keeps invariant peers for packages that only export a Client API', () => {
  101. expect(collectPackageInvariantViolations(fixture({ clientExport: true }))).toEqual([])
  102. })
  103. it('keeps invariant peers for experimental packages with a dsh.client entry', () => {
  104. expect(collectPackageInvariantViolations(fixture({
  105. packageDirectory: 'packages/experimental/probe',
  106. clientDeclaration: true,
  107. clientExport: true,
  108. }))).toEqual([])
  109. })
  110. it('accepts an invariant reference owned by a package-local leaf project', () => {
  111. const root = fixture({ invariantReference: false })
  112. const dir = join(root, 'packages/core/probe')
  113. writeFileSync(join(dir, 'tsconfig.json'), `${JSON.stringify({
  114. files: [],
  115. references: [{ path: './tsconfig.host.json' }],
  116. }, null, 2)}\n`)
  117. writeFileSync(join(dir, 'tsconfig.host.json'), `${JSON.stringify({
  118. references: [{ path: '../../runtime-diagnostics/invariants' }],
  119. }, null, 2)}\n`)
  120. expect(collectPackageInvariantViolations(root)).toEqual([])
  121. })
  122. it('rejects missing publication metadata and build output', () => {
  123. const violations = collectPackageInvariantViolations(fixture({
  124. invariantExport: false,
  125. invariantDependency: false,
  126. invariantReference: false,
  127. buildEntry: false,
  128. }))
  129. expect(violations.map(violation => violation.message)).toEqual(expect.arrayContaining([
  130. expect.stringContaining('exports["./invariant"]'),
  131. expect.stringContaining('peerDependency'),
  132. expect.stringContaining('devDependency'),
  133. expect.stringContaining('TypeScript project references'),
  134. expect.stringContaining('must bundle lib/types/invariant.js'),
  135. ]))
  136. })
  137. it('rejects foreign, duplicate, and unresolved registrations', () => {
  138. const source = `
  139. export const name = 'probe-invariant'
  140. export const inject = ['invariants']
  141. const selected = process.env.PACKAGE_NAME
  142. const install = (_ctx: unknown, fail: (message: string) => never) => { fail('probe') }
  143. export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) => {
  144. ctx.invariants.register('@deepseek-ai/dsh-foreign', install)
  145. return ctx.invariants.register(selected!, install)
  146. }
  147. `
  148. const violations = collectPackageInvariantViolations(fixture({ source }))
  149. expect(violations.map(violation => violation.message)).toEqual(expect.arrayContaining([
  150. expect.stringContaining('must resolve to a local string constant'),
  151. expect.stringContaining('must register exactly its own package name'),
  152. ]))
  153. })
  154. it('rejects generated markers and reporter-free executable installers', () => {
  155. const generated = fixture({
  156. source: `/** @generated */\n${handwrittenInvariant('@deepseek-ai/dsh-probe')}`,
  157. })
  158. expect(collectPackageInvariantViolations(generated).map(violation => violation.message))
  159. .toContain('invariant companions must be hand-owned and may not carry @generated markers')
  160. const reporterFree = fixture({
  161. source: `
  162. export const name = 'probe-invariant'
  163. export const inject = ['invariants']
  164. const install = () => { void 0 }
  165. export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) =>
  166. Promise.resolve(ctx.invariants.register('@deepseek-ai/dsh-probe', install))
  167. `,
  168. })
  169. expect(collectPackageInvariantViolations(reporterFree).map(violation => violation.message))
  170. .toContain('install function must accept the bound failure reporter as its second parameter')
  171. const unused = fixture({
  172. source: `
  173. export const name = 'probe-invariant'
  174. export const inject = ['invariants']
  175. const install = (_ctx: unknown, _fail: (message: string) => never) => { void 0 }
  176. export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) =>
  177. Promise.resolve(ctx.invariants.register('@deepseek-ai/dsh-probe', install))
  178. `,
  179. })
  180. expect(collectPackageInvariantViolations(unused).map(violation => violation.message))
  181. .toContain('install function must use its bound failure reporter')
  182. })
  183. it('rejects registering a different installer than the checked local function', () => {
  184. const decoy = fixture({
  185. source: `
  186. export const name = 'probe-invariant'
  187. export const inject = ['invariants']
  188. const install = (_ctx: unknown, fail: (message: string) => never) => { fail('checked decoy') }
  189. export const apply = (ctx: { invariants: { register(name: string, install: () => void): () => void } }) =>
  190. ctx.invariants.register('@deepseek-ai/dsh-probe', () => {})
  191. `,
  192. })
  193. expect(collectPackageInvariantViolations(decoy).map(violation => violation.message))
  194. .toContain('line 6: ctx.invariants.register must use the checked local install function')
  195. })
  196. it.each([
  197. 'export default { name, inject, apply }',
  198. "export * as default from './probe.ts'",
  199. ])('rejects a default export that would collapse the Loader namespace', (defaultExport) => {
  200. const source = `${handwrittenInvariant('@deepseek-ai/dsh-probe')}\n${defaultExport}\n`
  201. expect(collectPackageInvariantViolations(fixture({ source })).map(violation => violation.message))
  202. .toContain('must not default-export; Loader must retain the companion namespace')
  203. })
  204. it('accepts explained empty installers and rejects unexplained ones', () => {
  205. const explained = `
  206. export const name = 'probe-invariant'
  207. export const inject = ['invariants']
  208. const PACKAGE_NAME = '@deepseek-ai/dsh-probe'
  209. /** No runtime invariant: this pure package owns no events or mutable data. */
  210. const install = () => {}
  211. export const apply = (ctx: { invariants: { register(name: string, install: () => void): () => void } }) =>
  212. ctx.invariants.register(PACKAGE_NAME, install)
  213. `
  214. expect(collectPackageInvariantViolations(fixture({ source: explained }))).toEqual([])
  215. const unexplained = `
  216. export const name = 'probe-invariant'
  217. export const inject = ['invariants']
  218. const PACKAGE_NAME = '@deepseek-ai/dsh-probe'
  219. const install = () => {}
  220. export const apply = (ctx: { invariants: { register(name: string, install: () => void): () => void } }) =>
  221. ctx.invariants.register(PACKAGE_NAME, install)
  222. `
  223. expect(collectPackageInvariantViolations(fixture({ source: unexplained })).map(violation => violation.message))
  224. .toContain('empty install function must explain why with a "No runtime invariant:" comment')
  225. })
  226. })