service.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import { describe, expect, it, vi } from 'vitest'
  2. import { Context, Service } from '@deepseek-ai/cordis'
  3. import InvariantRegistry, {
  4. InvariantError,
  5. type Config,
  6. } from '@deepseek-ai/dsh-invariants'
  7. declare module '@deepseek-ai/cordis' {
  8. interface Context {
  9. invariantProbe: InvariantProbeService
  10. }
  11. interface Events {
  12. 'invariants-test/ping'(): void
  13. }
  14. }
  15. class InvariantProbeService extends Service {
  16. constructor(ctx: Context) {
  17. super(ctx, 'invariantProbe')
  18. }
  19. }
  20. interface RuntimeRegistration extends PromiseLike<() => void> {
  21. (): void | Promise<void>
  22. }
  23. interface InstalledRegistration {
  24. dispose(): Promise<void>
  25. }
  26. function runtimeRegistration(registration: () => void): RuntimeRegistration {
  27. return registration as RuntimeRegistration
  28. }
  29. async function setup(config: Config = {}): Promise<{ ctx: Context; fiber: Awaited<ReturnType<Context['plugin']>> }> {
  30. const ctx = new Context()
  31. const fiber = await ctx.plugin(InvariantRegistry, config)
  32. return { ctx, fiber }
  33. }
  34. async function registerProbe(
  35. ctx: Context,
  36. packageName: string,
  37. probe: () => void,
  38. ): Promise<InstalledRegistration> {
  39. const registration = runtimeRegistration(ctx.invariants.register(packageName, (child) => {
  40. child.on('invariants-test/ping', probe, { global: true })
  41. }))
  42. await registration
  43. return {
  44. async dispose() { await registration() },
  45. }
  46. }
  47. describe('InvariantRegistry selection', () => {
  48. it('applies defaults when constructed directly without schema normalization', async () => {
  49. const ctx = new Context()
  50. const service = new InvariantRegistry(ctx)
  51. const probe = vi.fn()
  52. const registration = runtimeRegistration(service.register('@deepseek-ai/dsh-session', (child) => {
  53. child.on('invariants-test/ping', probe, { global: true })
  54. }))
  55. await registration
  56. ctx.emit('invariants-test/ping')
  57. expect(probe).toHaveBeenCalledOnce()
  58. await registration()
  59. })
  60. it('enables registrations by default and treats empty lists as admit-all and exclude-none', async () => {
  61. for (const config of [{}, { package_allowlist: [], package_blocklist: [] }]) {
  62. const { ctx } = await setup(config)
  63. const probe = vi.fn()
  64. await registerProbe(ctx, '@deepseek-ai/dsh-session', probe)
  65. ctx.emit('invariants-test/ping')
  66. expect(probe).toHaveBeenCalledOnce()
  67. }
  68. })
  69. it('disables every installer while still reserving package ownership', async () => {
  70. const { ctx } = await setup({ enabled: false })
  71. const probe = vi.fn()
  72. const registration = await registerProbe(ctx, '@deepseek-ai/dsh-session', probe)
  73. expect(() => ctx.invariants.register('@deepseek-ai/dsh-session', () => {}))
  74. .toThrow(/already registered/)
  75. ctx.emit('invariants-test/ping')
  76. expect(probe).not.toHaveBeenCalled()
  77. await registration.dispose()
  78. })
  79. it('uses unanchored, case-sensitive JavaScript regex sources', async () => {
  80. const unanchored = await setup({ package_allowlist: ['session'] })
  81. const unanchoredProbe = vi.fn()
  82. await registerProbe(unanchored.ctx, '@deepseek-ai/dsh-session-extra', unanchoredProbe)
  83. unanchored.ctx.emit('invariants-test/ping')
  84. expect(unanchoredProbe).toHaveBeenCalledOnce()
  85. const anchored = await setup({ package_allowlist: ['^@deepseek-ai/dsh-session$'] })
  86. const anchoredProbe = vi.fn()
  87. await registerProbe(anchored.ctx, '@deepseek-ai/dsh-session-extra', anchoredProbe)
  88. anchored.ctx.emit('invariants-test/ping')
  89. expect(anchoredProbe).not.toHaveBeenCalled()
  90. const caseSensitive = await setup({ package_allowlist: ['Session'] })
  91. const caseProbe = vi.fn()
  92. await registerProbe(caseSensitive.ctx, '@deepseek-ai/dsh-session', caseProbe)
  93. caseSensitive.ctx.emit('invariants-test/ping')
  94. expect(caseProbe).not.toHaveBeenCalled()
  95. })
  96. it('lets the blocklist override an allowlist match', async () => {
  97. const { ctx } = await setup({
  98. package_allowlist: ['^@deepseek-ai/dsh-'],
  99. package_blocklist: ['session'],
  100. })
  101. const sessionProbe = vi.fn()
  102. const agentProbe = vi.fn()
  103. await registerProbe(ctx, '@deepseek-ai/dsh-session', sessionProbe)
  104. await registerProbe(ctx, '@deepseek-ai/dsh-agent', agentProbe)
  105. ctx.emit('invariants-test/ping')
  106. expect(sessionProbe).not.toHaveBeenCalled()
  107. expect(agentProbe).toHaveBeenCalledOnce()
  108. })
  109. it('accepts zero-match patterns for packages registered later', async () => {
  110. const { ctx } = await setup({ package_allowlist: ['^@later/invariants$'] })
  111. const now = vi.fn()
  112. const later = vi.fn()
  113. await registerProbe(ctx, '@deepseek-ai/dsh-session', now)
  114. await registerProbe(ctx, '@later/invariants', later)
  115. ctx.emit('invariants-test/ping')
  116. expect(now).not.toHaveBeenCalled()
  117. expect(later).toHaveBeenCalledOnce()
  118. })
  119. it('allows the same source in both lists and applies blocklist precedence', async () => {
  120. const { ctx } = await setup({ package_allowlist: ['agent'], package_blocklist: ['agent'] })
  121. const probe = vi.fn()
  122. await registerProbe(ctx, '@deepseek-ai/dsh-agent', probe)
  123. ctx.emit('invariants-test/ping')
  124. expect(probe).not.toHaveBeenCalled()
  125. })
  126. })
  127. describe('InvariantRegistry validation', () => {
  128. it.each([
  129. [{ package_allowlist: [''] }, /non-blank/],
  130. [{ package_allowlist: [' '] }, /non-blank/],
  131. [{ package_allowlist: [' session'] }, /surrounding whitespace/],
  132. [{ package_blocklist: ['session '] }, /surrounding whitespace/],
  133. [{ package_allowlist: ['session', 'session'] }, /duplicate regex/],
  134. [{ package_blocklist: ['agent', 'agent'] }, /duplicate regex/],
  135. [{ package_allowlist: ['['] }, /invalid regex/],
  136. [{ package_blocklist: ['('] }, /invalid regex/],
  137. ])('rejects malformed filter config %#', async (config, message) => {
  138. await expect((async () => {
  139. const ctx = new Context()
  140. await ctx.plugin(InvariantRegistry, config)
  141. })()).rejects.toThrow(message)
  142. })
  143. it.each(['', ' ', ' package', 'pack age', 'package\n'])('rejects malformed package name %j', async (packageName) => {
  144. const { ctx } = await setup()
  145. expect(() => ctx.invariants.register(packageName, () => {})).toThrow(/packageName/)
  146. })
  147. })
  148. describe('InvariantRegistry lifecycle', () => {
  149. it('honors the installer dependency API in its child fiber', async () => {
  150. const { ctx } = await setup()
  151. await ctx.plugin(InvariantProbeService)
  152. let registration!: RuntimeRegistration
  153. await ctx.plugin({
  154. inject: ['invariants', 'invariantProbe'],
  155. apply(child: Context) {
  156. const installer = Object.assign((installerCtx: Context) => {
  157. expect(Object.keys(installerCtx.fiber.inject)).toContain('invariantProbe')
  158. expect(Object.keys(installerCtx.fiber.store ?? {})).toContain('invariantProbe')
  159. expect(installerCtx.invariantProbe).toBeInstanceOf(InvariantProbeService)
  160. }, { inject: ['invariantProbe'] })
  161. expect(installer.inject).toEqual(['invariantProbe'])
  162. registration = runtimeRegistration(child.invariants.register('@deepseek-ai/dsh-probe', installer))
  163. return Promise.resolve(registration)
  164. },
  165. })
  166. await registration
  167. })
  168. it('attributes failures to the registering package with the stable code', async () => {
  169. const { ctx } = await setup()
  170. const registration = runtimeRegistration(ctx.invariants.register('@deepseek-ai/dsh-session', (child, fail) => {
  171. child.on('invariants-test/ping', () => fail('seq must strictly increase'), { global: true })
  172. }))
  173. await registration
  174. let caught: unknown
  175. try {
  176. ctx.emit('invariants-test/ping')
  177. } catch (error) {
  178. caught = error
  179. }
  180. expect(caught).toBeInstanceOf(InvariantError)
  181. expect(caught).toMatchObject({
  182. name: 'InvariantError',
  183. code: 'INVARIANT',
  184. packageName: '@deepseek-ai/dsh-session',
  185. message: 'invariant violated by "@deepseek-ai/dsh-session": seq must strictly increase',
  186. })
  187. })
  188. it('disposes the child fiber completely and permits HMR re-registration', async () => {
  189. const { ctx } = await setup()
  190. const first = vi.fn()
  191. const firstRegistration = await registerProbe(ctx, '@deepseek-ai/dsh-session', first)
  192. ctx.emit('invariants-test/ping')
  193. await firstRegistration.dispose()
  194. ctx.emit('invariants-test/ping')
  195. expect(first).toHaveBeenCalledOnce()
  196. const second = vi.fn()
  197. await registerProbe(ctx, '@deepseek-ai/dsh-session', second)
  198. ctx.emit('invariants-test/ping')
  199. expect(first).toHaveBeenCalledOnce()
  200. expect(second).toHaveBeenCalledOnce()
  201. })
  202. it('reserves ownership until asynchronous child disposal completes', async () => {
  203. const { ctx } = await setup()
  204. let finishDisposal!: () => void
  205. const disposalBarrier = new Promise<void>((resolve) => { finishDisposal = resolve })
  206. const registration = runtimeRegistration(ctx.invariants.register('@deepseek-ai/dsh-session', (child) => {
  207. child.effect(() => async () => { await disposalBarrier })
  208. }))
  209. await registration
  210. const disposing = registration()
  211. expect(() => ctx.invariants.register('@deepseek-ai/dsh-session', () => {}))
  212. .toThrow(/already registered/)
  213. finishDisposal()
  214. await disposing
  215. const replacement = runtimeRegistration(ctx.invariants.register('@deepseek-ai/dsh-session', () => {}))
  216. await replacement
  217. await replacement()
  218. })
  219. it('rolls back listeners and ownership atomically when an installer fails', async () => {
  220. const { ctx } = await setup()
  221. const leaked = vi.fn()
  222. const failed = runtimeRegistration(ctx.invariants.register('@deepseek-ai/dsh-session', (child) => {
  223. child.on('invariants-test/ping', leaked, { global: true })
  224. throw new Error('installer failed')
  225. }))
  226. await expect(Promise.resolve(failed)).rejects.toThrow('installer failed')
  227. ctx.emit('invariants-test/ping')
  228. expect(leaked).not.toHaveBeenCalled()
  229. const retry = vi.fn()
  230. await registerProbe(ctx, '@deepseek-ai/dsh-session', retry)
  231. ctx.emit('invariants-test/ping')
  232. expect(retry).toHaveBeenCalledOnce()
  233. })
  234. it('rolls back publication effects and ownership when child-fiber publication fails', async () => {
  235. const { ctx } = await setup()
  236. const leaked = vi.fn()
  237. let rejectPublication = true
  238. const stopRejecting = ctx.on('internal/plugin', (fiber) => {
  239. if (!rejectPublication || fiber.uid === null) return
  240. rejectPublication = false
  241. fiber.ctx.on('invariants-test/ping', leaked, { global: true })
  242. throw new Error('publication failed')
  243. })
  244. const failed = runtimeRegistration(ctx.invariants.register('@deepseek-ai/dsh-publication-probe', () => {}))
  245. await expect(Promise.resolve(failed)).rejects.toThrow('publication failed')
  246. ctx.emit('invariants-test/ping')
  247. expect(leaked).not.toHaveBeenCalled()
  248. stopRejecting()
  249. const retry = runtimeRegistration(ctx.invariants.register('@deepseek-ai/dsh-publication-probe', () => {}))
  250. await retry
  251. await retry()
  252. })
  253. it('joins asynchronous checks and rolls back their effects on failure', async () => {
  254. const { ctx } = await setup()
  255. const leaked = vi.fn()
  256. const failed = runtimeRegistration(ctx.invariants.register('@deepseek-ai/dsh-async-probe', async (child, fail) => {
  257. child.on('invariants-test/ping', leaked, { global: true })
  258. await Promise.resolve()
  259. fail('asynchronous check failed')
  260. }))
  261. await expect(Promise.resolve(failed)).rejects.toThrow(/asynchronous check failed/)
  262. ctx.emit('invariants-test/ping')
  263. expect(leaked).not.toHaveBeenCalled()
  264. const retry = runtimeRegistration(ctx.invariants.register('@deepseek-ai/dsh-async-probe', async () => {
  265. await Promise.resolve()
  266. }))
  267. await retry
  268. await retry()
  269. })
  270. it('releases a synchronous reservation if the service fiber is already inactive', async () => {
  271. const { ctx, fiber } = await setup()
  272. const service = ctx.invariants
  273. await fiber.dispose()
  274. expect(() => service.register('@deepseek-ai/dsh-session', () => {})).toThrow(/inactive/i)
  275. })
  276. })