workspaceRoutineTools.test.ts 6.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. // @vitest-environment node
  2. import { afterEach, beforeEach, expect, it } from 'vitest'
  3. import { mkdtempSync, rmSync } from 'node:fs'
  4. import { tmpdir } from 'node:os'
  5. import { join } from 'node:path'
  6. import { randomUUID } from 'node:crypto'
  7. import { WorkspaceStore } from '../../src/server/workspaceStore'
  8. import { WorkspaceRuntime } from '../../src/server/workspaceRuntime'
  9. import type { WorkspaceNodes } from '../../src/server/workspaceGateway'
  10. import { UploadStore } from '../../src/server/uploads'
  11. import { HttpError } from '../../src/server/errors'
  12. import { WorkspaceRoutines } from '../../src/server/workspaceRoutines'
  13. import { WorkspaceRoutineTools } from '../../src/server/workspaceRoutineTools'
  14. import type { Work } from '../../src/server/workspaceScheduler'
  15. import type { WorkspaceAgent } from '../../src/shared/workspace'
  16. import { buildWorkspacePrompt, type WorkspacePromptInput } from '../../src/server/workspacePrompt'
  17. import { ROUTINE_TOOL_RULES } from '../../src/server/workspaceRoutineTools'
  18. let home: string, store: WorkspaceStore, runtime: WorkspaceRuntime, uploads: UploadStore, tools: WorkspaceRoutineTools, work: Work
  19. const owner = 'account-one'
  20. const schedule = { kind: 'daily' as const, timezone: 'Asia/Shanghai', time: '09:00' }
  21. beforeEach(() => {
  22. home = mkdtempSync(join(tmpdir(), 'yaoyao-routine-tools-'))
  23. store = new WorkspaceStore(home)
  24. uploads = new UploadStore(home)
  25. const nodes = {
  26. requireSource: () => {},
  27. target: () => { throw new Error('未连接') },
  28. } as unknown as WorkspaceNodes
  29. runtime = new WorkspaceRuntime(store, nodes, uploads, () => true)
  30. const routines = new WorkspaceRoutines(store, { isUserActive: () => true } as never, nodes, runtime)
  31. tools = new WorkspaceRoutineTools(routines, store, nodes, runtime)
  32. runtime.routineTools = tools
  33. const agent = store.createAgent(owner, { name: '日报', profile: 'default' })
  34. const direct = store.list<{ id: string }>(owner, 'conversation')[0]!
  35. const runId = randomUUID()
  36. store.put(owner, 'run', runId, { id: runId, conversationId: direct.id, messageId: randomUUID(), status: 'running', mentionIds: [], stopRequested: false })
  37. work = { id: randomUUID(), runId, conversationId: direct.id, agentId: agent.id, messageId: randomUUID(), triggerSeq: 1, depth: 0, batchId: runId, replyMode: 'mentioned', requiredReply: true, status: 'running', createdAt: Date.now() }
  38. store.put(owner, 'turn', work.id, work)
  39. })
  40. afterEach(() => {
  41. runtime.close(); uploads.close(); store.close()
  42. rmSync(home, { recursive: true, force: true })
  43. })
  44. it('creates, lists, updates and deletes only the speaking Bot routines, and retries reuse the same task', () => {
  45. expect(tools.catalog(owner, work.id).map(tool => tool.id)).toEqual([
  46. 'workspace_list_routines', 'workspace_create_routine', 'workspace_update_routine', 'workspace_delete_routine',
  47. ])
  48. const requestId = randomUUID()
  49. const created = tools.call(owner, work.id, 'workspace_create_routine', { requestId, name: '晨间核对', prompt: '汇总昨日进展', enabled: true, schedule }) as { routine: { id: string } }
  50. const again = tools.call(owner, work.id, 'workspace_create_routine', { requestId, name: '晨间核对', prompt: '汇总昨日进展', enabled: true, schedule }) as { routine: { id: string } }
  51. expect(again.routine.id).toBe(created.routine.id)
  52. expect(store.list(owner, 'routine')).toHaveLength(1)
  53. const other = store.createAgent(owner, { name: '另一个', profile: 'default' })
  54. const foreign = new WorkspaceRoutines(store, { isUserActive: () => true } as never, { requireSource: () => {} } as never, runtime)
  55. .save(owner, other.id, { name: '别人的', prompt: '不要看见', enabled: false, schedule })
  56. const listed = tools.call(owner, work.id, 'workspace_list_routines', {}) as { routines: Array<{ id: string }> }
  57. expect(listed.routines.map(item => item.id)).toEqual([created.routine.id])
  58. const paused = tools.call(owner, work.id, 'workspace_update_routine', { requestId: randomUUID(), routineId: created.routine.id, enabled: false }) as { routine: { enabled: boolean } }
  59. expect(paused.routine.enabled).toBe(false)
  60. expect(() => tools.call(owner, work.id, 'workspace_delete_routine', { requestId: randomUUID(), routineId: foreign.id })).toThrow(/不存在/)
  61. tools.call(owner, work.id, 'workspace_delete_routine', { requestId: randomUUID(), routineId: created.routine.id })
  62. expect(store.list(owner, 'routine').map((item: { id: string }) => item.id)).toEqual([foreign.id])
  63. })
  64. it('rejects temporary and archived bots and a reused request id with different content', () => {
  65. const agent = store.require<WorkspaceAgent>(owner, 'agent', work.agentId)
  66. store.put(owner, 'agent', agent.id, { ...agent, temporaryGoalId: randomUUID() })
  67. expect(() => tools.call(owner, work.id, 'workspace_list_routines', {})).toThrow(HttpError)
  68. store.put(owner, 'agent', agent.id, agent)
  69. const requestId = randomUUID()
  70. tools.call(owner, work.id, 'workspace_create_routine', { requestId, name: '一次', prompt: '原来的内容', enabled: true, schedule: { kind: 'interval', timezone: 'UTC', everyMinutes: 30 } })
  71. expect(() => tools.call(owner, work.id, 'workspace_create_routine', { requestId, name: '一次', prompt: '换成别的', enabled: true, schedule: { kind: 'interval', timezone: 'UTC', everyMinutes: 30 } })).toThrow(/请求编号/)
  72. })
  73. it('tells every Bot not to use Hermes cron, and explains its own routine tools when they are mounted', () => {
  74. const fixture = {
  75. agent: { id: 'bot', name: '助手', avatar: '', instructions: '规则', nodeId: 'local', profile: 'dev', archived: false, revision: 1, createdAt: 1, updatedAt: 1 },
  76. 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 },
  77. members: [{ id: 'bot', name: '助手' }],
  78. work: { depth: 0, requiredReply: false, replyMode: 'mentioned', messageId: 'message' },
  79. run: { mentionIds: [] },
  80. environment: { open: { computer: false, server: false, vm: false, cloud: false }, tools: { desktopView: false, desktopFile: false, vm: false, cloud: false, plugins: false }, version: 1, capturedAt: 1, execution: { nodeId: 'local', profile: 'dev' }, runtime: { connected: false }, desktop: { capturedAt: 1, hosts: [] }, virtual: { vm: { status: 'disabled' }, cloud: { status: 'disabled' } }, fileTransferMaxMiB: 25 },
  81. memory: '', noReply: '[[YAOYAO_NO_REPLY_V1]]', marker: '[yaoyao-run:run:result]', content: '每天提醒我', contentKind: 'user', attachmentRefs: [],
  82. } as WorkspacePromptInput
  83. expect(buildWorkspacePrompt(fixture)).toContain('不要调用 Hermes 的 cronjob')
  84. expect(buildWorkspacePrompt(fixture)).not.toContain('workspace_delete_routine')
  85. expect(buildWorkspacePrompt({ ...fixture, routineRules: ROUTINE_TOOL_RULES })).toContain('到点后在它的单聊中发送任务内容')
  86. })