inventory.spec.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import { afterEach, describe, expect, it } from 'vitest'
  2. import { Context, FiberState, type Plugin } from '@deepseek-ai/cordis'
  3. import Loader from '@deepseek-ai/cordis-plugin-loader'
  4. import { remoteMethods } from '@deepseek-ai/dsh-typert-protocol'
  5. import type { AgentPresets } from '@deepseek-ai/dsh-agent-presets'
  6. import { ensurePluginFailures, type ProfileRuntime } from '@deepseek-ai/dsh-app-boot'
  7. import PluginInventoryGateway from '../src/index.ts'
  8. const contexts: Context[] = []
  9. afterEach(async () => {
  10. await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
  11. })
  12. const activePlugin: Plugin.Function = () => {}
  13. const pendingPlugin: Plugin.Object = {
  14. inject: ['neverReady'],
  15. apply() {},
  16. }
  17. async function harness(): Promise<{
  18. ctx: Context
  19. inventory: PluginInventoryGateway
  20. }> {
  21. const ctx = new Context()
  22. contexts.push(ctx)
  23. await ctx.plugin(Loader)
  24. ctx.loader.builtins.active = activePlugin
  25. ctx.loader.builtins.pending = pendingPlugin
  26. await ctx.plugin(PluginInventoryGateway)
  27. const inventory = ctx.get('pluginInventory') as PluginInventoryGateway
  28. return { ctx, inventory }
  29. }
  30. describe('PluginInventoryGateway', () => {
  31. it('publishes one direct list method under the pluginInventory namespace', async () => {
  32. const { inventory } = await harness()
  33. expect(inventory.typertRemote).toMatchObject({
  34. serviceKey: 'pluginInventory',
  35. namespace: 'pluginInventory',
  36. })
  37. expect(remoteMethods(inventory)).toEqual([
  38. { method: 'list', invocation: { kind: 'direct' } },
  39. ])
  40. })
  41. it('projects current non-group Loader entries without a second cache', async () => {
  42. const { ctx, inventory } = await harness()
  43. const activeId = await ctx.loader.create({ name: 'cordis:active' })
  44. const pendingId = await ctx.loader.create({ name: 'cordis:pending' })
  45. const disabledId = await ctx.loader.create({
  46. name: 'cordis:not-installed',
  47. disabled: true,
  48. })
  49. await ctx.loader.create({ name: 'cordis:active', group: true })
  50. const snapshot = await inventory.list()
  51. // No agent-preset roster is composed, so the snapshot carries no presets.
  52. expect(snapshot.agentPresets).toBeUndefined()
  53. expect(snapshot.entries).toHaveLength(3)
  54. expect(snapshot.entries).toEqual(expect.arrayContaining([
  55. {
  56. entryId: activeId,
  57. moduleName: 'cordis:active',
  58. enabled: true,
  59. fiberPhase: 'active',
  60. trust: 'builtin',
  61. },
  62. {
  63. entryId: pendingId,
  64. moduleName: 'cordis:pending',
  65. enabled: true,
  66. fiberPhase: 'pending',
  67. trust: 'builtin',
  68. },
  69. {
  70. entryId: disabledId,
  71. moduleName: 'cordis:not-installed',
  72. enabled: false,
  73. fiberPhase: null,
  74. trust: 'builtin',
  75. disabledBy: 'composition',
  76. },
  77. ]))
  78. await ctx.loader.update(activeId, { disabled: true })
  79. expect((await inventory.list()).entries.find(entry => entry.entryId === activeId)).toEqual({
  80. entryId: activeId,
  81. moduleName: 'cordis:active',
  82. enabled: false,
  83. fiberPhase: null,
  84. trust: 'builtin',
  85. disabledBy: 'composition',
  86. })
  87. await ctx.loader.remove(pendingId)
  88. expect((await inventory.list()).entries.some(entry => entry.entryId === pendingId)).toBe(false)
  89. })
  90. it('attributes rows to their bundle through the profile runtime and lists recorded failures', async () => {
  91. const { ctx, inventory } = await harness()
  92. const versioned = await ctx.loader.create({ id: 'versioned', name: 'cordis:active' })
  93. const bare = await ctx.loader.create({ id: 'bare', name: 'cordis:active' })
  94. const off = await ctx.loader.create({ id: 'off', name: 'cordis:active', disabled: true })
  95. const origins = new Map([
  96. ['versioned', { trust: 'external', packageName: 'ext', version: '1.2.3' }],
  97. ['bare', { trust: 'external', packageName: 'ext' }],
  98. ['off', { trust: 'external', packageName: 'ext' }],
  99. ['gone', { trust: 'external', packageName: 'ext' }],
  100. ] as const)
  101. ctx.provide('profileRuntime', {
  102. originOf: (rowId: string) => origins.get(rowId as never),
  103. userDisabledRowIds: () => new Set(['off']),
  104. } as Partial<ProfileRuntime> as never)
  105. const registry = ensurePluginFailures(ctx)
  106. registry.record({ entryId: 'gone', rowId: 'gone', moduleName: 'cordis:throws', groupId: 'bundle/ext', stage: 'apply', message: 'boom' })
  107. // A record of a row that later mounted rides on the live entry and is not listed twice.
  108. registry.record({ entryId: bare, rowId: 'bare', moduleName: 'cordis:active', groupId: 'bundle/ext', stage: 'import', message: 'stale' })
  109. registry.record({
  110. entryId: 'conflict:bundle/late:versioned', rowId: 'versioned', moduleName: 'late', groupId: 'bundle/late',
  111. packageName: 'late', stage: 'conflict', message: 'row "versioned" is already declared by ext',
  112. })
  113. registry.record({
  114. entryId: 'conflict:/home/cordis.patch.yml:bare', rowId: 'bare', moduleName: 'mine', groupId: '/home/cordis.patch.yml',
  115. stage: 'conflict', message: 'row "bare" is already declared by ext',
  116. })
  117. const { entries } = await inventory.list()
  118. expect(entries).toEqual([
  119. { entryId: versioned, moduleName: 'cordis:active', enabled: true, fiberPhase: 'active', trust: 'external', package: { name: 'ext', version: '1.2.3' } },
  120. { entryId: bare, moduleName: 'cordis:active', enabled: true, fiberPhase: 'active', trust: 'external', package: { name: 'ext' }, failure: { stage: 'import', message: 'stale' } },
  121. { entryId: off, moduleName: 'cordis:active', enabled: false, fiberPhase: null, trust: 'external', package: { name: 'ext' }, disabledBy: 'user' },
  122. // A failed row the tree no longer holds is attributed through the runtime.
  123. { entryId: 'gone', moduleName: 'cordis:throws', enabled: true, fiberPhase: 'failed', trust: 'external', package: { name: 'ext' }, failure: { stage: 'apply', message: 'boom' } },
  124. // A conflict names the bundle that lost the id; the runtime would name the owner.
  125. { entryId: 'conflict:bundle/late:versioned', moduleName: 'late', enabled: true, fiberPhase: 'failed', trust: 'external', package: { name: 'late' }, failure: { stage: 'conflict', message: 'row "versioned" is already declared by ext' } },
  126. // A user-layer conflict belongs to no package.
  127. { entryId: 'conflict:/home/cordis.patch.yml:bare', moduleName: 'mine', enabled: true, fiberPhase: 'failed', trust: 'external', failure: { stage: 'conflict', message: 'row "bare" is already declared by ext' } },
  128. ])
  129. })
  130. it('carries each composed preset with root-fiber states mapped to phases', async () => {
  131. const { ctx, inventory } = await harness()
  132. ctx.provide('agentPresets', {
  133. compositionInventory: async () => [
  134. {
  135. id: 'standard',
  136. trust: 'system',
  137. name: '标准模式',
  138. isDefault: true,
  139. rows: [
  140. { entryId: 'alpha', moduleName: 'pkg-alpha', enabled: true, fiberState: FiberState.ACTIVE },
  141. { entryId: null, moduleName: 'pkg-file', enabled: 'conditional', condition: 'x' },
  142. ],
  143. },
  144. { id: 'damaged', trust: 'user', isDefault: false, broken: 'the composition file is missing', rows: [] },
  145. ],
  146. } as Partial<AgentPresets> as never)
  147. const snapshot = await inventory.list()
  148. expect(snapshot.agentPresets).toEqual([
  149. {
  150. id: 'standard',
  151. trust: 'system',
  152. name: '标准模式',
  153. isDefault: true,
  154. rows: [
  155. { entryId: 'alpha', moduleName: 'pkg-alpha', enabled: true, fiberPhase: 'active' },
  156. { entryId: null, moduleName: 'pkg-file', enabled: 'conditional', condition: 'x', fiberPhase: null },
  157. ],
  158. },
  159. { id: 'damaged', trust: 'user', isDefault: false, broken: 'the composition file is missing', rows: [] },
  160. ])
  161. })
  162. })