package-invariants.spec.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  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. renderBaselineInvariant,
  9. } from './package-invariants.ts'
  10. const roots: string[] = []
  11. afterEach(() => {
  12. for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
  13. })
  14. function fixture(options: {
  15. packageName?: string
  16. source?: string
  17. invariantExport?: boolean
  18. invariantDependency?: boolean
  19. invariantReference?: boolean
  20. buildEntry?: boolean
  21. } = {}): string {
  22. const root = mkdtempSync(join(tmpdir(), 'dsh-package-invariants-'))
  23. roots.push(root)
  24. const dir = join(root, 'packages/core/probe')
  25. mkdirSync(join(dir, 'src'), { recursive: true })
  26. const packageName = options.packageName ?? '@deepseek-ai/dsh-probe'
  27. const manifest = {
  28. name: packageName,
  29. exports: options.invariantExport === false ? {} : {
  30. './invariant': {
  31. types: './lib/types/invariant.d.ts',
  32. default: './lib/invariant.js',
  33. },
  34. },
  35. files: ['lib/index.js', 'lib/invariant.js', 'src'],
  36. peerDependencies: options.invariantDependency === false ? {} : {
  37. '@deepseek-ai/dsh-invariants': '^0.0.1',
  38. },
  39. devDependencies: options.invariantDependency === false ? {} : {
  40. '@deepseek-ai/dsh-invariants': 'workspace:^',
  41. },
  42. }
  43. writeFileSync(join(dir, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`)
  44. writeFileSync(join(dir, 'tsconfig.json'), `${JSON.stringify({
  45. references: options.invariantReference === false ? [] : [{ path: '../../support/invariants' }],
  46. }, null, 2)}\n`)
  47. const owner = packageInvariantOwners(root)[0]!
  48. writeFileSync(join(dir, 'src/invariant.ts'), options.source ?? renderBaselineInvariant(owner))
  49. writeFileSync(
  50. join(dir, 'tsdown.config.ts'),
  51. options.buildEntry === false ? "export default { entry: ['lib/types/index.js'] }\n" : "export default { entry: ['lib/types/index.js', 'lib/types/invariant.js'] }\n",
  52. )
  53. return root
  54. }
  55. describe('package invariant gate', () => {
  56. it('accepts a generated owner companion with publication metadata', () => {
  57. expect(collectPackageInvariantViolations(fixture())).toEqual([])
  58. })
  59. it('rejects missing publication metadata and build output', () => {
  60. const violations = collectPackageInvariantViolations(fixture({
  61. invariantExport: false,
  62. invariantDependency: false,
  63. invariantReference: false,
  64. buildEntry: false,
  65. }))
  66. expect(violations.map(violation => violation.message)).toEqual(expect.arrayContaining([
  67. expect.stringContaining('exports["./invariant"]'),
  68. expect.stringContaining('peerDependency'),
  69. expect.stringContaining('devDependency'),
  70. expect.stringContaining('TypeScript project references'),
  71. expect.stringContaining('must bundle lib/types/invariant.js'),
  72. ]))
  73. })
  74. it('rejects foreign, duplicate, and unresolved registrations', () => {
  75. const source = `
  76. export const name = 'probe-invariant'
  77. export const inject = ['invariants']
  78. const selected = process.env.PACKAGE_NAME
  79. export const apply = (ctx: { invariants: { register(name: string, install: () => void): () => void } }) => {
  80. ctx.invariants.register('@deepseek-ai/dsh-foreign', () => {})
  81. return ctx.invariants.register(selected!, () => {})
  82. }
  83. `
  84. const violations = collectPackageInvariantViolations(fixture({ source }))
  85. expect(violations.map(violation => violation.message)).toEqual(expect.arrayContaining([
  86. expect.stringContaining('must resolve to a local string constant'),
  87. expect.stringContaining('must register exactly its own package name'),
  88. ]))
  89. })
  90. it('rejects edits to a generated baseline', () => {
  91. const root = fixture()
  92. const path = join(root, 'packages/core/probe/src/invariant.ts')
  93. writeFileSync(path, `${renderBaselineInvariant(packageInvariantOwners(root)[0]!)}// stale\n`)
  94. expect(collectPackageInvariantViolations(root).map(violation => violation.message))
  95. .toContain('generated baseline is stale; run pnpm run gen-package-invariants')
  96. })
  97. })