projection.spec.ts 7.3 KB

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