interaction-runtime.test.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import { describe, expect, it, vi } from "vitest";
  2. import { InteractionSessionSchema } from "../interaction/session.js";
  3. import { runInteractionRequest } from "../interaction/runtime.js";
  4. describe("interaction runtime", () => {
  5. it("routes continue/write intents to writeNextChapter", async () => {
  6. const writeNextChapter = vi.fn(async () => ({
  7. chapterNumber: 7,
  8. title: "Harbor Ledger",
  9. wordCount: 3210,
  10. revised: false,
  11. status: "ready-for-review" as const,
  12. }));
  13. const reviseDraft = vi.fn();
  14. const updateCurrentFocus = vi.fn();
  15. const updateAuthorIntent = vi.fn();
  16. const session = InteractionSessionSchema.parse({
  17. sessionId: "session-1",
  18. projectRoot: "/tmp/project",
  19. activeBookId: "harbor",
  20. automationMode: "semi",
  21. messages: [],
  22. });
  23. const result = await runInteractionRequest({
  24. session,
  25. request: { intent: "write_next", bookId: "harbor" },
  26. tools: { writeNextChapter, reviseDraft, updateCurrentFocus, updateAuthorIntent },
  27. });
  28. expect(writeNextChapter).toHaveBeenCalledWith("harbor");
  29. expect(reviseDraft).not.toHaveBeenCalled();
  30. expect(result.session.activeBookId).toBe("harbor");
  31. expect(result.session.currentExecution?.status).toBe("completed");
  32. });
  33. it("routes revise_chapter to reviseDraft with local-fix", async () => {
  34. const writeNextChapter = vi.fn();
  35. const reviseDraft = vi.fn(async () => ({
  36. chapterNumber: 3,
  37. wordCount: 2800,
  38. fixedIssues: ["tightened ending"],
  39. applied: true,
  40. status: "ready-for-review" as const,
  41. }));
  42. const session = InteractionSessionSchema.parse({
  43. sessionId: "session-2",
  44. projectRoot: "/tmp/project",
  45. activeBookId: "harbor",
  46. automationMode: "manual",
  47. messages: [],
  48. });
  49. await runInteractionRequest({
  50. session,
  51. request: { intent: "revise_chapter", bookId: "harbor", chapterNumber: 3 },
  52. tools: {
  53. writeNextChapter,
  54. reviseDraft,
  55. updateCurrentFocus: vi.fn(),
  56. updateAuthorIntent: vi.fn(),
  57. },
  58. });
  59. expect(reviseDraft).toHaveBeenCalledWith("harbor", 3, "local-fix");
  60. });
  61. it("routes rewrite_chapter to reviseDraft with rewrite mode", async () => {
  62. const reviseDraft = vi.fn(async () => ({
  63. chapterNumber: 5,
  64. wordCount: 3100,
  65. fixedIssues: ["rewrote chapter"],
  66. applied: true,
  67. status: "ready-for-review" as const,
  68. }));
  69. await runInteractionRequest({
  70. session: InteractionSessionSchema.parse({
  71. sessionId: "session-3",
  72. projectRoot: "/tmp/project",
  73. activeBookId: "harbor",
  74. automationMode: "manual",
  75. messages: [],
  76. }),
  77. request: { intent: "rewrite_chapter", bookId: "harbor", chapterNumber: 5 },
  78. tools: {
  79. writeNextChapter: vi.fn(),
  80. reviseDraft,
  81. updateCurrentFocus: vi.fn(),
  82. updateAuthorIntent: vi.fn(),
  83. },
  84. });
  85. expect(reviseDraft).toHaveBeenCalledWith("harbor", 5, "rewrite");
  86. });
  87. it("routes update_focus to the focus updater", async () => {
  88. const updateCurrentFocus = vi.fn(async () => ({ ok: true }));
  89. await runInteractionRequest({
  90. session: InteractionSessionSchema.parse({
  91. sessionId: "session-4",
  92. projectRoot: "/tmp/project",
  93. activeBookId: "harbor",
  94. automationMode: "semi",
  95. messages: [],
  96. }),
  97. request: {
  98. intent: "update_focus",
  99. bookId: "harbor",
  100. instruction: "Bring the story back to the old harbor debt line.",
  101. },
  102. tools: {
  103. writeNextChapter: vi.fn(),
  104. reviseDraft: vi.fn(),
  105. updateCurrentFocus,
  106. updateAuthorIntent: vi.fn(),
  107. },
  108. });
  109. expect(updateCurrentFocus).toHaveBeenCalledWith(
  110. "harbor",
  111. "Bring the story back to the old harbor debt line.",
  112. );
  113. });
  114. it("updates automation mode without invoking pipeline tools", async () => {
  115. const writeNextChapter = vi.fn();
  116. const reviseDraft = vi.fn();
  117. const updateCurrentFocus = vi.fn();
  118. const updateAuthorIntent = vi.fn();
  119. const result = await runInteractionRequest({
  120. session: InteractionSessionSchema.parse({
  121. sessionId: "session-5",
  122. projectRoot: "/tmp/project",
  123. activeBookId: "harbor",
  124. automationMode: "semi",
  125. messages: [],
  126. }),
  127. request: { intent: "switch_mode", mode: "auto" },
  128. tools: {
  129. writeNextChapter,
  130. reviseDraft,
  131. updateCurrentFocus,
  132. updateAuthorIntent,
  133. },
  134. });
  135. expect(result.session.automationMode).toBe("auto");
  136. expect(writeNextChapter).not.toHaveBeenCalled();
  137. expect(reviseDraft).not.toHaveBeenCalled();
  138. expect(updateCurrentFocus).not.toHaveBeenCalled();
  139. expect(updateAuthorIntent).not.toHaveBeenCalled();
  140. });
  141. it("pauses the active book without invoking pipeline tools", async () => {
  142. const result = await runInteractionRequest({
  143. session: InteractionSessionSchema.parse({
  144. sessionId: "session-6",
  145. projectRoot: "/tmp/project",
  146. activeBookId: "harbor",
  147. automationMode: "auto",
  148. messages: [],
  149. events: [],
  150. }),
  151. request: { intent: "pause_book", bookId: "harbor" },
  152. tools: {
  153. writeNextChapter: vi.fn(),
  154. reviseDraft: vi.fn(),
  155. updateCurrentFocus: vi.fn(),
  156. updateAuthorIntent: vi.fn(),
  157. },
  158. });
  159. expect(result.session.currentExecution?.status).toBe("blocked");
  160. expect(result.responseText).toContain("Paused");
  161. expect(result.session.events.at(-1)?.kind).toBe("task.completed");
  162. });
  163. it("returns a human-readable explanation for explain_status", async () => {
  164. const result = await runInteractionRequest({
  165. session: InteractionSessionSchema.parse({
  166. sessionId: "session-7",
  167. projectRoot: "/tmp/project",
  168. activeBookId: "harbor",
  169. automationMode: "semi",
  170. messages: [],
  171. events: [],
  172. currentExecution: {
  173. status: "repairing",
  174. bookId: "harbor",
  175. chapterNumber: 3,
  176. stageLabel: "repairing chapter 3",
  177. },
  178. }),
  179. request: { intent: "explain_status", bookId: "harbor", instruction: "what are you doing?" },
  180. tools: {
  181. writeNextChapter: vi.fn(),
  182. reviseDraft: vi.fn(),
  183. updateCurrentFocus: vi.fn(),
  184. updateAuthorIntent: vi.fn(),
  185. },
  186. });
  187. expect(result.responseText).toContain("repairing");
  188. expect(result.responseText).toContain("harbor");
  189. });
  190. });