workspacePrompt.test.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. // @vitest-environment node
  2. import { describe, expect, it } from 'vitest'
  3. import { buildWorkspacePrompt, workspaceEnvironmentPrompt, workspacePromptHistory, type WorkspacePromptEnvironment, type WorkspacePromptInput } from '../../src/server/workspacePrompt'
  4. function environment(): WorkspacePromptEnvironment {
  5. return {
  6. open: { computer: true, server: true, vm: true, cloud: true },
  7. tools: { desktopView: true, desktopFile: true, vm: true, cloud: true, plugins: false },
  8. version: 1, capturedAt: 1, execution: { nodeId: 'local', profile: 'dev' },
  9. runtime: { connected: true },
  10. desktop: { capturedAt: 1, sourceHost: 'mac-id', hosts: [{ id: 'mac-id', target: 'mac-id', name: '工作 Mac', kind: 'computer', source: true, online: true, open: true,
  11. capabilities: { view: { enabled: true, status: 'ready' }, input: { enabled: true, status: 'ready' }, fileRead: { enabled: true, status: 'ready' }, fileWrite: { enabled: true, status: 'ready' }, shell: { enabled: true, status: 'ready' }, fileTransfer: { enabled: true, status: 'ready' } },
  12. transfer: { protocol: 'chunked', readMaxMiB: 50, writeMaxMiB: 50 } }] },
  13. virtual: { vm: { status: 'on_demand', environmentId: 'bot' }, cloud: { status: 'on_demand', cwd: '/workspace' } },
  14. cwd: '/srv/hermes/project', fileTransferMaxMiB: 50,
  15. }
  16. }
  17. function input(): WorkspacePromptInput {
  18. return {
  19. agent: { id: 'bot', name: '助手', avatar: '', instructions: '规则原文\n保留格式', description: '处理开发任务', job: '开发与验证', voice: 'concise', actBias: 'act_now', nodeId: 'local', profile: 'dev', archived: false, revision: 7, createdAt: 1, updatedAt: 1 },
  20. conversation: { id: 'chat', kind: 'direct', name: '对话', avatar: '', memberIds: ['bot'], administratorId: 'bot', mode: 'free', instructions: '', autoReplyIds: [], maxReplyRounds: 1, archived: false, pinned: false, readSeq: 0, lastSeq: 0, preview: '', createdAt: 1, updatedAt: 1 },
  21. members: [{ id: 'bot', name: '助手' }],
  22. work: { depth: 0, requiredReply: true, replyMode: 'mentioned', messageId: 'message' },
  23. run: { mentionIds: [] }, environment: environment(),
  24. memory: '已确认的项目事实', noReply: '[[YAOYAO_NO_REPLY_V1]]', marker: '[yaoyao-run:run:result]',
  25. content: '读取本机文件\n然后核对结果', contentKind: 'user', attachmentRefs: ['[附件](ref:file)'],
  26. }
  27. }
  28. describe('Bot environment routing', () => {
  29. it('keeps native Hermes operations available when a server desktop proxy is offline', () => {
  30. const env = environment()
  31. env.runtime.metadata = {version:1,profile:'dev',platform:'linux',osRelease:'6.8',arch:'x86_64',nativeTools:{terminal:['terminal'],files:['read_file'],desktop:['computer_use'],browser:[]}}
  32. const offline = structuredClone(env.desktop.hosts[0]!)
  33. offline.id='local';offline.target='server';offline.name='服务器';offline.kind='server';offline.source=false;offline.online=false
  34. for(const capability of Object.values(offline.capabilities))Object.assign(capability,{enabled:false,status:'offline'})
  35. env.desktop.hosts=[offline]
  36. env.desktop.sourceHost=undefined
  37. env.tools.desktopView=false;env.tools.desktopFile=false
  38. const text=workspaceEnvironmentPrompt(env)
  39. expect(text).toContain('服务端 Hermes 已连接')
  40. expect(text).toContain('服务器桌面代理;host="server";控制通道=离线')
  41. expect(text).toContain('操作服务端运行环境时优先使用')
  42. expect(text).toContain('"desktop":["computer_use"]')
  43. expect(text).toContain('目录存在不保证后端连接就绪')
  44. expect(text).toContain('「本机」无法确定')
  45. expect(text).not.toContain('电脑桌面工具已挂载')
  46. })
  47. it('describes all mounted environments conditionally without moving the model or the message origin', () => {
  48. const text = workspaceEnvironmentPrompt(environment())
  49. expect(text).toContain('模型会话运行在服务端 Hermes')
  50. expect(text).toContain('「本机」只指本轮消息来源电脑')
  51. expect(text).toContain('不得切换其他机器代做')
  52. expect(text).toContain('host="server"')
  53. expect(text).toContain('desktop_shell')
  54. expect(text).toContain('computer_shell')
  55. expect(text).toContain('cloud_computer_*')
  56. expect(text).toContain('最大 50 MiB')
  57. expect(text).not.toContain('25 MiB')
  58. expect(text).not.toContain('当前环境是')
  59. expect(text).not.toContain('一律用 desktop_*')
  60. expect(text.match(/工作 Mac/g)).toHaveLength(2) // source and inventory, not one list per environment
  61. expect(text).toContain('该路径仅属于 Hermes 运行环境')
  62. })
  63. it('distinguishes global policy from mounted capabilities and does not promise native tools', () => {
  64. const env = environment()
  65. env.tools = { desktopView: false, desktopFile: false, vm: false, cloud: false, plugins: false }
  66. const text = workspaceEnvironmentPrompt(env)
  67. expect(text).toContain('开放不等于在线或已授权')
  68. expect(text).toContain('以本轮实际工具目录为准')
  69. expect(text).not.toContain('工具均可使用')
  70. for (const tool of ['desktop_file_copy', 'desktop_environment_view', 'computer_shell', 'cloud_computer_*']) expect(text).not.toContain(tool)
  71. })
  72. it.each(['none','server','virtual'] as const)('uses default %s as a preference and prioritizes the enabled managed browser',mode=>{
  73. const env=environment()
  74. env.selection={mode,revision:2,credentialSource:mode==='server'?'native':'yaoyao'}
  75. env.tools.managedBrowser=true
  76. const text=workspaceEnvironmentPrompt(env)
  77. expect(text).toContain('不限制其他已开放且获授权的环境')
  78. expect(text).toContain('用户明确指定目标时优先使用该目标')
  79. expect(text).toContain('「本机」只指本轮消息来源电脑')
  80. expect(text).toContain('默认使用 managed_browser_open')
  81. expect(text).toContain('此规则优先于服务器或虚拟机的默认运行环境')
  82. expect(text).toContain('不要仅因历史曾用过其他浏览器而覆盖本轮默认')
  83. expect(text).toContain('继续操作已经明确指定的页面')
  84. expect(text).toContain('computer_shell')
  85. expect(text).toContain('cloud_computer_*')
  86. expect(text).not.toContain('只使用所选环境')
  87. expect(text).not.toContain('执行位置已由系统固定')
  88. expect(text).not.toContain('虚拟环境不可用')
  89. env.tools.managedBrowser=false
  90. expect(workspaceEnvironmentPrompt(env)).not.toContain('managed_browser_open')
  91. })
  92. it('does not require a screenshot for file-only computers', () => {
  93. const env = environment()
  94. env.tools.desktopView = false
  95. const text = workspaceEnvironmentPrompt(env)
  96. expect(text).toContain('文件与命令操作无需先截图')
  97. expect(text).not.toContain('desktop_environment_view')
  98. expect(text).toContain('desktop_file_copy')
  99. env.tools.desktopFile = false
  100. env.tools.desktopView = true
  101. const view = workspaceEnvironmentPrompt(env)
  102. expect(view).toContain('desktop_environment_view')
  103. expect(view).not.toContain('电脑文件与命令工具已挂载')
  104. expect(view).not.toContain('desktop_file_copy')
  105. })
  106. it('cannot turn Grok model discussion, old VM text or a follow-up into environment selection', () => {
  107. const fixture = input()
  108. fixture.environment.tools.cloud = false
  109. fixture.environment.tools.vm = false
  110. const prompts = ['介绍 Grok 模型', '以前使用过虚拟机', '继续'].map(content => buildWorkspacePrompt({ ...fixture, content }))
  111. const environments = prompts.map(text => text.split('【本轮环境与设备】')[1]!.split('【Bot 记忆与相关事实】')[0])
  112. expect(new Set(environments).size).toBe(1)
  113. expect(environments[0]).not.toContain('cloud_computer_*')
  114. expect(environments[0]).not.toContain('computer_shell')
  115. })
  116. })
  117. describe('Bot prompt composition', () => {
  118. it('retains user-authored section titles, task instructions, facts and attachments in the historical form', () => {
  119. const fixture = input()
  120. fixture.content = '【基础行为规则】\n用户原文\n【本轮用户消息】\n仍是用户原文'
  121. fixture.run.internalInstruction = '完成此轮分派,保留要求'
  122. const history = workspacePromptHistory(fixture)
  123. expect(history).toContain(fixture.content)
  124. expect(history).toContain(fixture.memory)
  125. expect(history).toContain(fixture.run.internalInstruction)
  126. expect(history).toContain(fixture.marker)
  127. expect(history).toContain(fixture.attachmentRefs[0])
  128. expect(history).not.toContain('本轮环境与设备')
  129. expect(history).not.toContain(fixture.agent.instructions)
  130. expect(buildWorkspacePrompt(fixture)).toContain(fixture.agent.instructions)
  131. })
  132. it('routes configured MCP discovery through the current Bot catalog instead of Hermes CLI configuration', () => {
  133. const fixture = input()
  134. fixture.environment.tools.plugins = true
  135. fixture.pluginServices = [{ name: 'vaultwarden', transport: 'stdio', toolCount: 59 }, { name: '远程服务', transport: 'http', toolCount: 2 }]
  136. const text = buildWorkspacePrompt(fixture)
  137. expect(text).toContain('"name":"vaultwarden","transport":"stdio","toolCount":59')
  138. expect(text).toContain('"name":"远程服务","transport":"http","toolCount":2')
  139. expect(text).toContain('tool_search')
  140. expect(text).toContain('yaoyao_tools')
  141. expect(text).toContain('yaoyao_call')
  142. expect(text).toContain('按 service(服务名)或 query')
  143. expect(text).toContain('不要在拿到参数后重复搜索')
  144. expect(text).toContain('普通 Bot 对话不是 Hermes 看板任务')
  145. expect(text).toContain('不重复相同的无效调用')
  146. expect(text).toContain('yaoyao_plugin_')
  147. expect(text).not.toContain('工具名以 plugin_ 开头')
  148. expect(text).toContain('不能根据 hermes mcp list/test 的结果判断这些服务不存在')
  149. expect(text).toContain('工具发现成功不代表账号已登录')
  150. fixture.environment.tools.plugins = false
  151. expect(buildWorkspacePrompt(fixture)).not.toContain('vaultwarden')
  152. })
  153. it('keeps persona, provenance, user content and attachments in their own sections', () => {
  154. const fixture = input(), text = buildWorkspacePrompt(fixture)
  155. expect(text).toContain(`版本 7):\n${fixture.agent.instructions}`)
  156. expect(text).toContain('你是 助手。')
  157. expect(text).toContain('直接行动:合理默认即可开工')
  158. expect(text).toContain(`【本轮用户消息】\n${fixture.content}`)
  159. expect(text).toContain('【本轮附件】\n[附件](ref:file)')
  160. expect(text).toContain(fixture.marker)
  161. expect(text.indexOf('【Bot 身份与长期规则】')).toBeLessThan(text.indexOf('【本轮任务与协作】'))
  162. expect(text.indexOf('【本轮环境与设备】')).toBeLessThan(text.indexOf('【Bot 记忆与相关事实】'))
  163. expect(text.indexOf('【Bot 记忆与相关事实】')).toBeLessThan(text.indexOf('【本轮用户消息】'))
  164. })
  165. it('preserves discussion participation, member roles and history boundaries', () => {
  166. const fixture = input()
  167. fixture.conversation = { ...fixture.conversation, kind: 'group', memberRoles: { bot: { name: '审核', description: '核实事实' } } }
  168. fixture.run = { mentionIds: ['bot'], discussion: { memberIds: ['bot'], rounds: 2 }, projectId: 'project' }
  169. fixture.work.depth = 1
  170. fixture.contentKind = 'history'
  171. const text = buildWorkspacePrompt(fixture)
  172. expect(text).toContain('平等讨论,第 2 轮')
  173. expect(text).toContain('@助手:审核;核实事实')
  174. expect(text).toContain('本轮用户指定成员:@助手')
  175. expect(text).toContain('不要用私信或 @ 再次派发本轮工作')
  176. expect(text).toContain('【本轮对话上下文】')
  177. expect(text).toContain('当前项目 ID:project')
  178. })
  179. it('preserves automatic silence and does not offer unmounted export tools to a helper', () => {
  180. const fixture = input()
  181. fixture.work.requiredReply = false
  182. fixture.work.replyMode = 'automatic'
  183. fixture.agent.temporaryGoalId = 'goal'
  184. fixture.environment.tools.vm = false
  185. const text = buildWorkspacePrompt(fixture)
  186. expect(text).toContain('完整答复只能是 [[YAOYAO_NO_REPLY_V1]]')
  187. expect(text).toContain('不要创建团队或改变自身权限')
  188. expect(text).not.toContain('computer_export')
  189. })
  190. })
  191. it('describes failed optional services separately without replacing the user request or promising unavailable tools',()=>{
  192. const value=input()
  193. value.pluginWarnings=[{service:'故障 MCP',code:'plugin_initialization_failed',message:'本轮暂时不可用'}]
  194. const prompt=buildWorkspacePrompt(value)
  195. expect(prompt).toContain('【本轮服务连接提示】')
  196. expect(prompt).toContain(JSON.stringify(value.pluginWarnings))
  197. expect(prompt).toContain('不要因为无关插件离线而中断整轮对话')
  198. expect(prompt).toContain('不能宣称已经调用或执行成功')
  199. expect(prompt).toContain('【本轮用户消息】\n'+value.content)
  200. expect(prompt).not.toContain('已完成本轮连接和工具发现的服务')
  201. })