|
|
@@ -0,0 +1,91 @@
|
|
|
+// @vitest-environment node
|
|
|
+import { afterEach, beforeEach, expect, it } from 'vitest'
|
|
|
+import { mkdtempSync, rmSync } from 'node:fs'
|
|
|
+import { tmpdir } from 'node:os'
|
|
|
+import { join } from 'node:path'
|
|
|
+import { randomUUID } from 'node:crypto'
|
|
|
+import { WorkspaceStore } from '../../src/server/workspaceStore'
|
|
|
+import { WorkspaceRuntime } from '../../src/server/workspaceRuntime'
|
|
|
+import type { WorkspaceNodes } from '../../src/server/workspaceGateway'
|
|
|
+import { UploadStore } from '../../src/server/uploads'
|
|
|
+import { HttpError } from '../../src/server/errors'
|
|
|
+import { WorkspaceRoutines } from '../../src/server/workspaceRoutines'
|
|
|
+import { WorkspaceRoutineTools } from '../../src/server/workspaceRoutineTools'
|
|
|
+import type { Work } from '../../src/server/workspaceScheduler'
|
|
|
+import type { WorkspaceAgent } from '../../src/shared/workspace'
|
|
|
+import { buildWorkspacePrompt, type WorkspacePromptInput } from '../../src/server/workspacePrompt'
|
|
|
+import { ROUTINE_TOOL_RULES } from '../../src/server/workspaceRoutineTools'
|
|
|
+
|
|
|
+let home: string, store: WorkspaceStore, runtime: WorkspaceRuntime, uploads: UploadStore, tools: WorkspaceRoutineTools, work: Work
|
|
|
+const owner = 'account-one'
|
|
|
+const schedule = { kind: 'daily' as const, timezone: 'Asia/Shanghai', time: '09:00' }
|
|
|
+
|
|
|
+beforeEach(() => {
|
|
|
+ home = mkdtempSync(join(tmpdir(), 'yaoyao-routine-tools-'))
|
|
|
+ store = new WorkspaceStore(home)
|
|
|
+ uploads = new UploadStore(home)
|
|
|
+ const nodes = {
|
|
|
+ requireSource: () => {},
|
|
|
+ target: () => { throw new Error('未连接') },
|
|
|
+ } as unknown as WorkspaceNodes
|
|
|
+ runtime = new WorkspaceRuntime(store, nodes, uploads, () => true)
|
|
|
+ const routines = new WorkspaceRoutines(store, { isUserActive: () => true } as never, nodes, runtime)
|
|
|
+ tools = new WorkspaceRoutineTools(routines, store, nodes, runtime)
|
|
|
+ runtime.routineTools = tools
|
|
|
+ const agent = store.createAgent(owner, { name: '日报', profile: 'default' })
|
|
|
+ const direct = store.list<{ id: string }>(owner, 'conversation')[0]!
|
|
|
+ const runId = randomUUID()
|
|
|
+ store.put(owner, 'run', runId, { id: runId, conversationId: direct.id, messageId: randomUUID(), status: 'running', mentionIds: [], stopRequested: false })
|
|
|
+ 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() }
|
|
|
+ store.put(owner, 'turn', work.id, work)
|
|
|
+})
|
|
|
+afterEach(() => {
|
|
|
+ runtime.close(); uploads.close(); store.close()
|
|
|
+ rmSync(home, { recursive: true, force: true })
|
|
|
+})
|
|
|
+
|
|
|
+it('creates, lists, updates and deletes only the speaking Bot routines, and retries reuse the same task', () => {
|
|
|
+ expect(tools.catalog(owner, work.id).map(tool => tool.id)).toEqual([
|
|
|
+ 'workspace_list_routines', 'workspace_create_routine', 'workspace_update_routine', 'workspace_delete_routine',
|
|
|
+ ])
|
|
|
+ const requestId = randomUUID()
|
|
|
+ const created = tools.call(owner, work.id, 'workspace_create_routine', { requestId, name: '晨间核对', prompt: '汇总昨日进展', enabled: true, schedule }) as { routine: { id: string } }
|
|
|
+ const again = tools.call(owner, work.id, 'workspace_create_routine', { requestId, name: '晨间核对', prompt: '汇总昨日进展', enabled: true, schedule }) as { routine: { id: string } }
|
|
|
+ expect(again.routine.id).toBe(created.routine.id)
|
|
|
+ expect(store.list(owner, 'routine')).toHaveLength(1)
|
|
|
+ const other = store.createAgent(owner, { name: '另一个', profile: 'default' })
|
|
|
+ const foreign = new WorkspaceRoutines(store, { isUserActive: () => true } as never, { requireSource: () => {} } as never, runtime)
|
|
|
+ .save(owner, other.id, { name: '别人的', prompt: '不要看见', enabled: false, schedule })
|
|
|
+ const listed = tools.call(owner, work.id, 'workspace_list_routines', {}) as { routines: Array<{ id: string }> }
|
|
|
+ expect(listed.routines.map(item => item.id)).toEqual([created.routine.id])
|
|
|
+ const paused = tools.call(owner, work.id, 'workspace_update_routine', { requestId: randomUUID(), routineId: created.routine.id, enabled: false }) as { routine: { enabled: boolean } }
|
|
|
+ expect(paused.routine.enabled).toBe(false)
|
|
|
+ expect(() => tools.call(owner, work.id, 'workspace_delete_routine', { requestId: randomUUID(), routineId: foreign.id })).toThrow(/不存在/)
|
|
|
+ tools.call(owner, work.id, 'workspace_delete_routine', { requestId: randomUUID(), routineId: created.routine.id })
|
|
|
+ expect(store.list(owner, 'routine').map((item: { id: string }) => item.id)).toEqual([foreign.id])
|
|
|
+})
|
|
|
+
|
|
|
+it('rejects temporary and archived bots and a reused request id with different content', () => {
|
|
|
+ const agent = store.require<WorkspaceAgent>(owner, 'agent', work.agentId)
|
|
|
+ store.put(owner, 'agent', agent.id, { ...agent, temporaryGoalId: randomUUID() })
|
|
|
+ expect(() => tools.call(owner, work.id, 'workspace_list_routines', {})).toThrow(HttpError)
|
|
|
+ store.put(owner, 'agent', agent.id, agent)
|
|
|
+ const requestId = randomUUID()
|
|
|
+ tools.call(owner, work.id, 'workspace_create_routine', { requestId, name: '一次', prompt: '原来的内容', enabled: true, schedule: { kind: 'interval', timezone: 'UTC', everyMinutes: 30 } })
|
|
|
+ expect(() => tools.call(owner, work.id, 'workspace_create_routine', { requestId, name: '一次', prompt: '换成别的', enabled: true, schedule: { kind: 'interval', timezone: 'UTC', everyMinutes: 30 } })).toThrow(/请求编号/)
|
|
|
+})
|
|
|
+
|
|
|
+it('tells every Bot not to use Hermes cron, and explains its own routine tools when they are mounted', () => {
|
|
|
+ const fixture = {
|
|
|
+ agent: { id: 'bot', name: '助手', avatar: '', instructions: '规则', nodeId: 'local', profile: 'dev', archived: false, revision: 1, createdAt: 1, updatedAt: 1 },
|
|
|
+ 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 },
|
|
|
+ members: [{ id: 'bot', name: '助手' }],
|
|
|
+ work: { depth: 0, requiredReply: false, replyMode: 'mentioned', messageId: 'message' },
|
|
|
+ run: { mentionIds: [] },
|
|
|
+ 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' }, desktop: { capturedAt: 1, hosts: [] }, virtual: { vm: { status: 'disabled' }, cloud: { status: 'disabled' } }, fileTransferMaxMiB: 25 },
|
|
|
+ memory: '', noReply: '[[YAOYAO_NO_REPLY_V1]]', marker: '[yaoyao-run:run:result]', content: '每天提醒我', contentKind: 'user', attachmentRefs: [],
|
|
|
+ } as WorkspacePromptInput
|
|
|
+ expect(buildWorkspacePrompt(fixture)).toContain('不要调用 Hermes 的 cronjob')
|
|
|
+ expect(buildWorkspacePrompt(fixture)).not.toContain('workspace_delete_routine')
|
|
|
+ expect(buildWorkspacePrompt({ ...fixture, routineRules: ROUTINE_TOOL_RULES })).toContain('到点后在它的单聊中发送任务内容')
|
|
|
+})
|