1
0

simulation-engine.react.spec.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import { describe, expect, it, vi } from "vitest"
  2. import type { NovelAgent, SimulationState, StoryNode, ExtractionResult } from "@/lib/novel/story-simulation/types"
  3. import {
  4. createSimulationBlackboard,
  5. } from "@/lib/novel/story-simulation/multi-agent-orchestrator"
  6. import { agentDecideAndActWithReact } from "@/lib/novel/story-simulation/simulation-engine"
  7. import { ModelDoesNotSupportToolsError } from "@/lib/agent/runner"
  8. let mockRun: any
  9. vi.mock("@/lib/agent/runner", () => ({
  10. AgentRunner: class {
  11. run(...args: any[]) {
  12. return mockRun(...args)
  13. }
  14. },
  15. ModelDoesNotSupportToolsError: class extends Error {
  16. constructor() {
  17. super("当前模型不支持工具调用")
  18. this.name = "ModelDoesNotSupportToolsError"
  19. }
  20. },
  21. }))
  22. function makeAgent(id: string, name: string): NovelAgent {
  23. return {
  24. characterId: id,
  25. name,
  26. profile: `${name}的档案`,
  27. aura: null,
  28. cognition: null,
  29. soul: "",
  30. currentGoal: "完成当前目标",
  31. emotionalState: "neutral",
  32. knownFacts: new Set(),
  33. relationships: new Map(),
  34. powerLevel: "normal",
  35. memory: {
  36. observedEvents: [],
  37. knownSecrets: new Set(),
  38. sentiments: new Map(),
  39. recentDecisions: [],
  40. rumorCredibility: 0.5,
  41. },
  42. knowledgeScope: [],
  43. personality: [],
  44. speakingStyle: "",
  45. }
  46. }
  47. function makeNode(): StoryNode {
  48. return {
  49. index: 0,
  50. phase: "起",
  51. title: "开端",
  52. coreConflict: "冲突",
  53. involvedCharacters: ["甲", "乙"],
  54. goal: "推进剧情",
  55. causeFromPrev: "无",
  56. expectedOutcome: "完成开端",
  57. }
  58. }
  59. function makeExtraction(): ExtractionResult {
  60. return {
  61. characters: [],
  62. chapterContents: [],
  63. memoryData: {
  64. characterStates: "",
  65. characterCognition: null,
  66. foreshadowingTracker: null,
  67. timeline: [],
  68. canonFacts: "",
  69. conflicts: "",
  70. },
  71. worldRules: "",
  72. powerSystem: "",
  73. foreshadowing: null,
  74. timeline: [],
  75. outlineContent: "",
  76. soulDoc: "",
  77. }
  78. }
  79. function makeState(agents: NovelAgent[]): SimulationState {
  80. return {
  81. currentRound: 0,
  82. timelineEvents: [],
  83. activeAgents: new Map(agents.map((a) => [a.characterId, a])),
  84. worldState: {},
  85. directorEnabled: false,
  86. nextNodeInjectionMap: new Map(),
  87. }
  88. }
  89. describe("agentDecideAndActWithReact", () => {
  90. it("parses action from AgentRunner finalText and creates timeline event", async () => {
  91. const agentA = makeAgent("a", "甲")
  92. const agentB = makeAgent("b", "乙")
  93. const agents = [agentA, agentB]
  94. const node = makeNode()
  95. const state = makeState(agents)
  96. const extraction = makeExtraction()
  97. const blackboard = createSimulationBlackboard({ agents })
  98. mockRun = vi.fn().mockResolvedValue({
  99. toolCalls: [],
  100. roundsUsed: 1,
  101. finalText: JSON.stringify({
  102. type: "speak",
  103. content: "你好乙",
  104. target: "乙",
  105. visibility: "target_only",
  106. motivation: "打招呼",
  107. plot_push: "建立联系",
  108. }),
  109. })
  110. const result = await agentDecideAndActWithReact(
  111. agentA,
  112. node,
  113. state,
  114. {} as any,
  115. extraction,
  116. [],
  117. undefined,
  118. blackboard,
  119. )
  120. expect(result).not.toBeNull()
  121. expect(result!.parsed.action.type).toBe("speak")
  122. expect(result!.parsed.action.content).toBe("你好乙")
  123. expect(result!.parsed.action.target).toBe("乙")
  124. expect(result!.tlEvent.actorId).toBe("a")
  125. expect(result!.tlEvent.content).toBe("你好乙")
  126. expect(mockRun).toHaveBeenCalledTimes(1)
  127. const callArgs = mockRun.mock.calls[0]
  128. expect(callArgs[0].maxRounds).toBe(3)
  129. expect(callArgs[0].tools.length).toBe(6)
  130. })
  131. it("throws ModelDoesNotSupportToolsError when model does not support tools", async () => {
  132. const agentA = makeAgent("a", "甲")
  133. const agents = [agentA]
  134. const node = makeNode()
  135. const state = makeState(agents)
  136. const extraction = makeExtraction()
  137. const blackboard = createSimulationBlackboard({ agents })
  138. mockRun = vi.fn().mockImplementation(async (_config, _registry, _messages, callbacks) => {
  139. callbacks.onError(new ModelDoesNotSupportToolsError())
  140. return { toolCalls: [], roundsUsed: 0, finalText: "" }
  141. })
  142. await expect(
  143. agentDecideAndActWithReact(
  144. agentA,
  145. node,
  146. state,
  147. {} as any,
  148. extraction,
  149. [],
  150. undefined,
  151. blackboard,
  152. ),
  153. ).rejects.toThrow(ModelDoesNotSupportToolsError)
  154. })
  155. it("returns null when signal is aborted", async () => {
  156. const agentA = makeAgent("a", "甲")
  157. const agents = [agentA]
  158. const node = makeNode()
  159. const state = makeState(agents)
  160. const extraction = makeExtraction()
  161. const blackboard = createSimulationBlackboard({ agents })
  162. mockRun = vi.fn().mockResolvedValue({
  163. toolCalls: [],
  164. roundsUsed: 0,
  165. finalText: "",
  166. })
  167. const controller = new AbortController()
  168. controller.abort()
  169. const result = await agentDecideAndActWithReact(
  170. agentA,
  171. node,
  172. state,
  173. {} as any,
  174. extraction,
  175. [],
  176. undefined,
  177. blackboard,
  178. controller.signal,
  179. )
  180. expect(result).toBeNull()
  181. })
  182. it("handles malformed JSON gracefully with fallback", async () => {
  183. const agentA = makeAgent("a", "甲")
  184. const agents = [agentA]
  185. const node = makeNode()
  186. const state = makeState(agents)
  187. const extraction = makeExtraction()
  188. const blackboard = createSimulationBlackboard({ agents })
  189. mockRun = vi.fn().mockResolvedValue({
  190. toolCalls: [],
  191. roundsUsed: 1,
  192. finalText: "这不是有效的JSON格式",
  193. })
  194. const result = await agentDecideAndActWithReact(
  195. agentA,
  196. node,
  197. state,
  198. {} as any,
  199. extraction,
  200. [],
  201. undefined,
  202. blackboard,
  203. )
  204. expect(result).not.toBeNull()
  205. expect(result!.parsed.action.type).toBe("observe")
  206. })
  207. it("passes injectionEvent and modeHint to user message", async () => {
  208. const agentA = makeAgent("a", "甲")
  209. const agents = [agentA]
  210. const node = makeNode()
  211. const state = makeState(agents)
  212. const extraction = makeExtraction()
  213. const blackboard = createSimulationBlackboard({ agents })
  214. mockRun = vi.fn().mockResolvedValue({
  215. toolCalls: [],
  216. roundsUsed: 1,
  217. finalText: JSON.stringify({
  218. type: "observe",
  219. content: "观察周围",
  220. visibility: "self",
  221. }),
  222. })
  223. await agentDecideAndActWithReact(
  224. agentA,
  225. node,
  226. state,
  227. {} as any,
  228. extraction,
  229. [],
  230. "突发地震",
  231. blackboard,
  232. undefined,
  233. "主动出击",
  234. )
  235. expect(mockRun).toHaveBeenCalledTimes(1)
  236. const callArgs = mockRun.mock.calls[0]
  237. const messages = callArgs[2]
  238. const userMessage = messages.find((m: any) => m.role === "user")
  239. expect(userMessage.content).toContain("突发地震")
  240. expect(userMessage.content).toContain("主动出击")
  241. })
  242. })