package-invariants.spec.ts 8.5 KB

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