policy.spec.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. /** Event-level policy tests; no filesystem provider is needed because the plugin performs no I/O. */
  2. import { describe, expect, it } from 'vitest'
  3. import { Context } from 'cordis'
  4. import { FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
  5. import type { FsTarget, FsWriteIntent } from '@deepseek-ai/dsh-fs'
  6. import * as FsPolicy from '@deepseek-ai/dsh-fs-policy'
  7. import type { FsPolicyExec } from '@deepseek-ai/dsh-fs-policy'
  8. function target(path: string): FsTarget {
  9. return { targetKey: FsTargetKey(path), displayPath: path }
  10. }
  11. const ownerExec = (session: object): FsPolicyExec => ({ agent: { session } })
  12. /** Dispatch the write-intent waterfall with the bare default thunk. */
  13. function writeIntent(ctx: Context, t: FsTarget, actor: object | undefined): Promise<FsWriteIntent | undefined> {
  14. return ctx.waterfall('fs/write-intent', t, actor, () => undefined)
  15. }
  16. /** Dispatch the edit-intent waterfall with the bare default thunk. */
  17. function editIntent(ctx: Context, t: FsTarget, actor: object | undefined): Promise<{ version: FsVersion } | undefined> {
  18. return ctx.waterfall('fs/edit-intent', t, actor, () => undefined)
  19. }
  20. async function setup() {
  21. const ctx = new Context()
  22. const fiber = await ctx.plugin(FsPolicy)
  23. return { ctx, fiber }
  24. }
  25. describe('registration / disposal', () => {
  26. it('registers no service surface (it is a plugin, not ctx.fsPolicy)', async () => {
  27. const { ctx } = await setup()
  28. expect((ctx as Context & { fsPolicy?: unknown }).fsPolicy).toBeUndefined()
  29. })
  30. it('mounts with no inject (reads no services)', async () => {
  31. // It mounts immediately even with nothing else in the context.
  32. const ctx = new Context()
  33. await ctx.plugin(FsPolicy)
  34. // The listener is live: an unobserved write decides createIfAbsent.
  35. expect(await writeIntent(ctx, target('a.txt'), undefined)).toEqual({ kind: 'createIfAbsent' })
  36. })
  37. })
  38. describe('write-intent decision', () => {
  39. it('an unobserved target decides createIfAbsent', async () => {
  40. const { ctx } = await setup()
  41. expect(await writeIntent(ctx, target('a.txt'), ownerExec({}))).toEqual({ kind: 'createIfAbsent' })
  42. })
  43. it('a no-owner actor decides createIfAbsent', async () => {
  44. const { ctx } = await setup()
  45. expect(await writeIntent(ctx, target('a.txt'), undefined)).toEqual({ kind: 'createIfAbsent' })
  46. expect(await writeIntent(ctx, target('a.txt'), {})).toEqual({ kind: 'createIfAbsent' })
  47. })
  48. it('an actor with an agent but no session has no owner (createIfAbsent)', async () => {
  49. // The middle optional-chain rung: agent present, session undefined ⇒ owner
  50. // undefined ⇒ unobservable, so a write can only be a blind create.
  51. const { ctx } = await setup()
  52. expect(await writeIntent(ctx, target('a.txt'), { agent: {} })).toEqual({ kind: 'createIfAbsent' })
  53. })
  54. it('an observed target decides replaceIfVersion at the observed version', async () => {
  55. const { ctx } = await setup()
  56. const exec = ownerExec({})
  57. ctx.emit('fs/observed', target('a.txt'), FsVersion('v7'), exec)
  58. expect(await writeIntent(ctx, target('a.txt'), exec)).toEqual({ kind: 'replaceIfVersion', version: 'v7' })
  59. })
  60. })
  61. describe('edit-intent decision', () => {
  62. it('rejects an unread edit with FS_NOT_OBSERVED', async () => {
  63. const { ctx } = await setup()
  64. await expect(editIntent(ctx, target('a.txt'), ownerExec({}))).rejects.toMatchObject({ code: 'FS_NOT_OBSERVED' })
  65. })
  66. it('rejects an edit with no owner (cannot prove prior observation)', async () => {
  67. const { ctx } = await setup()
  68. await expect(editIntent(ctx, target('a.txt'), undefined)).rejects.toMatchObject({ code: 'FS_NOT_OBSERVED' })
  69. })
  70. it('rejects an edit whose actor has an agent but no session (no owner)', async () => {
  71. const { ctx } = await setup()
  72. await expect(editIntent(ctx, target('a.txt'), { agent: {} })).rejects.toMatchObject({ code: 'FS_NOT_OBSERVED' })
  73. })
  74. it('returns the observed version as the CAS basis after an observation', async () => {
  75. const { ctx } = await setup()
  76. const exec = ownerExec({})
  77. ctx.emit('fs/observed', target('a.txt'), FsVersion('v3'), exec)
  78. expect(await editIntent(ctx, target('a.txt'), exec)).toEqual({ version: 'v3' })
  79. })
  80. })
  81. describe('observed-state is the prior-observation record', () => {
  82. it('a read observation authorizes an in-place write at that version', async () => {
  83. const { ctx } = await setup()
  84. const exec = ownerExec({})
  85. ctx.emit('fs/observed', target('a.txt'), FsVersion('v0'), exec) // a read
  86. expect(await writeIntent(ctx, target('a.txt'), exec)).toEqual({ kind: 'replaceIfVersion', version: 'v0' })
  87. })
  88. it('a write/edit observation refreshes the basis, so the next edit needs no re-read', async () => {
  89. const { ctx } = await setup()
  90. const exec = ownerExec({})
  91. // A create records v1; the follow-up edit guards against v1 with no read.
  92. ctx.emit('fs/observed', target('a.txt'), FsVersion('v1'), exec)
  93. expect(await editIntent(ctx, target('a.txt'), exec)).toEqual({ version: 'v1' })
  94. // The edit records v2; a second edit guards against v2.
  95. ctx.emit('fs/observed', target('a.txt'), FsVersion('v2'), exec)
  96. expect(await editIntent(ctx, target('a.txt'), exec)).toEqual({ version: 'v2' })
  97. })
  98. it('a no-owner observation records nothing', async () => {
  99. const { ctx } = await setup()
  100. ctx.emit('fs/observed', target('a.txt'), FsVersion('v0'), undefined)
  101. // Still unobserved for any owner.
  102. await expect(editIntent(ctx, target('a.txt'), ownerExec({}))).rejects.toMatchObject({ code: 'FS_NOT_OBSERVED' })
  103. })
  104. })
  105. describe('multi-owner isolation', () => {
  106. it('owner A observing does not grant owner B edit authority', async () => {
  107. const { ctx } = await setup()
  108. const a = ownerExec({})
  109. const b = ownerExec({})
  110. ctx.emit('fs/observed', target('a.txt'), FsVersion('v0'), a)
  111. await expect(editIntent(ctx, target('a.txt'), b)).rejects.toMatchObject({ code: 'FS_NOT_OBSERVED' })
  112. expect(await editIntent(ctx, target('a.txt'), a)).toEqual({ version: 'v0' })
  113. })
  114. it('each owner records its own observed version independently', async () => {
  115. const { ctx } = await setup()
  116. const a = ownerExec({})
  117. const b = ownerExec({})
  118. ctx.emit('fs/observed', target('a.txt'), FsVersion('v0'), a) // A observed v0
  119. // B never observed → createIfAbsent; A still holds v0 → replaceIfVersion.
  120. expect(await writeIntent(ctx, target('a.txt'), b)).toEqual({ kind: 'createIfAbsent' })
  121. expect(await writeIntent(ctx, target('a.txt'), a)).toEqual({ kind: 'replaceIfVersion', version: 'v0' })
  122. })
  123. })
  124. describe('single-slot, first-wins', () => {
  125. it('fully decides the slot without calling next() (the bare default is unreached)', async () => {
  126. const { ctx } = await setup()
  127. let defaultRan = false
  128. const intent = await ctx.waterfall('fs/write-intent', target('a.txt'), ownerExec({}), () => {
  129. defaultRan = true
  130. return undefined
  131. })
  132. expect(intent).toEqual({ kind: 'createIfAbsent' })
  133. expect(defaultRan).toBe(false)
  134. })
  135. it('a SECOND decider registered AFTER fs-policy is not reached (first-wins short-circuit)', async () => {
  136. const { ctx } = await setup()
  137. let secondRan = false
  138. // Registered after fs-policy, so it dispatches second; fs-policy does
  139. // not call next(), so this never runs. (A decider registered BEFORE — or with
  140. // prepend — would instead win: first-wins is by convention, not enforced.)
  141. ctx.on('fs/edit-intent', () => {
  142. secondRan = true
  143. return Promise.resolve(undefined)
  144. })
  145. const exec = ownerExec({})
  146. ctx.emit('fs/observed', target('a.txt'), FsVersion('v0'), exec)
  147. await editIntent(ctx, target('a.txt'), exec)
  148. expect(secondRan).toBe(false)
  149. })
  150. it('a SECOND write-intent decider registered AFTER fs-policy is not reached', async () => {
  151. const { ctx } = await setup()
  152. let secondRan = false
  153. ctx.on('fs/write-intent', () => {
  154. secondRan = true
  155. return Promise.resolve(undefined)
  156. })
  157. await writeIntent(ctx, target('a.txt'), ownerExec({}))
  158. expect(secondRan).toBe(false)
  159. })
  160. })
  161. describe('disposal releases recorded state (HMR safety)', () => {
  162. it('a fresh plugin after disposal starts with no inherited state', async () => {
  163. const ctx = new Context()
  164. const exec = ownerExec({})
  165. const fiber = await ctx.plugin(FsPolicy)
  166. ctx.emit('fs/observed', target('a.txt'), FsVersion('v0'), exec)
  167. expect(await editIntent(ctx, target('a.txt'), exec)).toEqual({ version: 'v0' })
  168. await fiber.dispose()
  169. await ctx.plugin(FsPolicy)
  170. // Same owner object, but state was released on disposal.
  171. await expect(editIntent(ctx, target('a.txt'), exec)).rejects.toMatchObject({ code: 'FS_NOT_OBSERVED' })
  172. })
  173. it('no listeners remain after disposal (the gate no longer decides)', async () => {
  174. const ctx = new Context()
  175. const fiber = await ctx.plugin(FsPolicy)
  176. await fiber.dispose()
  177. // With no listener, the waterfall falls through to the bare default.
  178. expect(await writeIntent(ctx, target('a.txt'), ownerExec({}))).toBeUndefined()
  179. })
  180. })