package-invariants.spec.ts 12 KB

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