projection.spec.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. /**
  2. * The `plan` projection unit (session-projection RFC's complete example): a
  3. * event fold over the session log. `command/run` records named `plan` with
  4. * recorded input set the candidate target (`off` → false, anything else →
  5. * true); `command/done` keeps successful candidates and drops failures;
  6. * `plan/mode` commits and clears a selection. `view` reports pending only while
  7. * an outstanding selection differs from the logged state.
  8. * Pending is thereby a pure replay quantity — a cold fold answers it without
  9. * the service's in-memory intent. Composition without plan-mode has no `plan`
  10. * key; unloading the fiber removes it (HMR safety).
  11. */
  12. import { describe, expect, it } from 'vitest'
  13. import { Context } from '@deepseek-ai/cordis'
  14. import AgentRegistry from '@deepseek-ai/dsh-agent'
  15. import type { Agent } from '@deepseek-ai/dsh-agent'
  16. import SessionStore from '@deepseek-ai/dsh-session'
  17. import type { Session } from '@deepseek-ai/dsh-session'
  18. import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
  19. import ToolRuntime from '@deepseek-ai/dsh-tools'
  20. import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
  21. import UserQuestionService from '@deepseek-ai/dsh-user-questions'
  22. import { CommandId } from '@deepseek-ai/dsh-commands/brand'
  23. import PlanModeController from '@deepseek-ai/dsh-plan-mode'
  24. interface Bench {
  25. ctx: Context
  26. session: Session
  27. values(): Record<string, unknown>
  28. }
  29. async function harness(withPlanMode: boolean): Promise<Bench> {
  30. const ctx = new Context()
  31. await ctx.plugin(SessionStore)
  32. await ctx.plugin(SystemPrompt, { persona: '' })
  33. await ctx.plugin(ToolRuntime)
  34. await ctx.plugin(UserQuestionService)
  35. await ctx.plugin(AgentRegistry)
  36. await ctx.plugin(SessionProjectionRegistry)
  37. if (withPlanMode) await ctx.plugin(PlanModeController, { section: 'plan policy' })
  38. const session = ctx.sessions.create()
  39. ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
  40. return {
  41. ctx,
  42. session,
  43. values: () => ctx.sessionProjections.snapshot(session).values,
  44. }
  45. }
  46. /** Append one logged /plan selection record (the executor's command/run shape). */
  47. function runPlanCommand(session: Session, args: string, index: number): CommandId {
  48. const commandId = CommandId(`plan-proj-${String(index)}`)
  49. session.append('command/run', {
  50. commandId,
  51. name: 'plan',
  52. args,
  53. source: { kind: 'user' },
  54. })
  55. return commandId
  56. }
  57. /** Append the paired settlement for one projected plan command. */
  58. function settlePlanCommand(session: Session, commandId: CommandId, kind: 'success' | 'error'): void {
  59. session.append('command/done', { commandId, kind })
  60. }
  61. /** Commit one plan/mode flip inside an open turn (the invariant's turn-enclosure rule). */
  62. function commitPlanMode(session: Session, active: boolean, turn: number): void {
  63. session.append('turn/start', { turn })
  64. session.append('plan/mode', { active })
  65. session.append('turn/end', { turn, reason: { kind: 'completed' } })
  66. }
  67. describe('plan projection unit', () => {
  68. it('serves inactive/not-pending for the empty log', async () => {
  69. const bench = await harness(true)
  70. expect(bench.values()).toEqual({ plan: { active: false, pending: false } })
  71. })
  72. it('a logged /plan selection reads pending until plan/mode records it', async () => {
  73. const bench = await harness(true)
  74. const commandId = runPlanCommand(bench.session, '', 0)
  75. expect(bench.values().plan).toEqual({ active: false, pending: true })
  76. settlePlanCommand(bench.session, commandId, 'success')
  77. expect(bench.values().plan).toEqual({ active: false, pending: true })
  78. commitPlanMode(bench.session, true, 0)
  79. expect(bench.values().plan).toEqual({ active: true, pending: false })
  80. })
  81. it('drops a plan selection when its command settles with an error', async () => {
  82. const bench = await harness(true)
  83. commitPlanMode(bench.session, true, 0)
  84. const commandId = runPlanCommand(bench.session, 'off', 0)
  85. expect(bench.values().plan).toEqual({ active: true, pending: true })
  86. settlePlanCommand(bench.session, commandId, 'error')
  87. expect(bench.values().plan).toEqual({ active: true, pending: false })
  88. })
  89. it('folds `off` args and non-plan commands correctly, and a matching selection is not pending', async () => {
  90. const bench = await harness(true)
  91. commitPlanMode(bench.session, true, 0)
  92. // Another command's record never touches plan state.
  93. bench.session.append('command/run', {
  94. commandId: CommandId('other-1'), name: 'compact', args: '', source: { kind: 'user' },
  95. })
  96. expect(bench.values().plan).toEqual({ active: true, pending: false })
  97. // A command lifecycle with omitted input carries no plan selection.
  98. bench.session.append('command/run', {
  99. commandId: CommandId('plan-no-input'), name: 'plan', source: { kind: 'user' },
  100. })
  101. expect(bench.values().plan).toEqual({ active: true, pending: false })
  102. runPlanCommand(bench.session, ' off', 1)
  103. expect(bench.values().plan).toEqual({ active: true, pending: true })
  104. commitPlanMode(bench.session, false, 1)
  105. expect(bench.values().plan).toEqual({ active: false, pending: false })
  106. // Selecting the already-committed state folds to not-pending (net zero).
  107. runPlanCommand(bench.session, 'off', 2)
  108. expect(bench.values().plan).toEqual({ active: false, pending: false })
  109. })
  110. it('a /plan message-argument selection targets plan mode', async () => {
  111. const bench = await harness(true)
  112. runPlanCommand(bench.session, ' sketch the refactor first', 0)
  113. expect(bench.values().plan).toEqual({ active: false, pending: true })
  114. })
  115. it('has no plan key when plan-mode is not composed', async () => {
  116. const bench = await harness(false)
  117. expect('plan' in bench.values()).toBe(false)
  118. })
  119. it('drops the key when the plan-mode fiber unloads (HMR safety)', async () => {
  120. const bench = await harness(false)
  121. const fiber = await bench.ctx.plugin(PlanModeController, { section: 'plan policy' })
  122. expect(bench.values().plan).toEqual({ active: false, pending: false })
  123. await fiber.dispose()
  124. expect('plan' in bench.values()).toBe(false)
  125. })
  126. it('cold replay recovers pending from the log alone (a fresh registry refolds it)', async () => {
  127. const bench = await harness(true)
  128. runPlanCommand(bench.session, '', 0)
  129. // A second registry over the same log (the cold-read shape): no service
  130. // memory involved, the fold alone answers {active:false, pending:true}.
  131. const cold = await harness(true)
  132. for (const event of bench.session.events) {
  133. if (event.type === 'command/run' || event.type === 'command/done' || event.type === 'plan/mode') {
  134. cold.session.append(event.type, event.data)
  135. }
  136. }
  137. expect(cold.values().plan).toEqual({ active: false, pending: true })
  138. })
  139. })