tui-control-flow.test.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
  2. import { tmpdir } from "node:os";
  3. import { join } from "node:path";
  4. import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
  5. import { createProjectSession, persistProjectSession } from "../tui/session-store.js";
  6. import { processTuiInput } from "../tui/app.js";
  7. let projectRoot: string;
  8. describe("tui control flow", () => {
  9. beforeAll(async () => {
  10. projectRoot = await mkdtemp(join(tmpdir(), "inkos-tui-control-"));
  11. await mkdir(join(projectRoot, "books", "harbor"), { recursive: true });
  12. await writeFile(join(projectRoot, "books", "harbor", "book.json"), "{}", "utf-8");
  13. await writeFile(join(projectRoot, "inkos.json"), JSON.stringify({ language: "zh" }), "utf-8");
  14. });
  15. afterAll(async () => {
  16. // tmpdir cleanup omitted
  17. });
  18. it("routes continue through the active project book", async () => {
  19. await persistProjectSession(projectRoot, {
  20. ...createProjectSession(projectRoot),
  21. activeBookId: "harbor",
  22. });
  23. const tools = {
  24. listBooks: vi.fn(async () => ["harbor"]),
  25. writeNextChapter: vi.fn(async () => ({ ok: true })),
  26. reviseDraft: vi.fn(async () => ({ ok: true })),
  27. patchChapterText: vi.fn(async () => ({ ok: true })),
  28. renameEntity: vi.fn(async () => ({ ok: true })),
  29. updateCurrentFocus: vi.fn(async () => ({ ok: true })),
  30. updateAuthorIntent: vi.fn(async () => ({ ok: true })),
  31. writeTruthFile: vi.fn(async () => ({ ok: true })),
  32. };
  33. const result = await processTuiInput(projectRoot, "continue", tools);
  34. expect(tools.writeNextChapter).toHaveBeenCalledWith("harbor");
  35. expect(result.session.activeBookId).toBe("harbor");
  36. expect(result.session.currentExecution?.status).toBe("waiting_human");
  37. expect(result.session.pendingDecision?.kind).toBe("review-next-step");
  38. expect(result.session.pendingDecision?.summary).toContain("等待");
  39. expect(result.session.messages.at(0)?.role).toBe("user");
  40. expect(result.session.messages.at(-1)?.role).toBe("assistant");
  41. });
  42. it("routes focus updates through the interaction runtime", async () => {
  43. await persistProjectSession(projectRoot, {
  44. ...createProjectSession(projectRoot),
  45. activeBookId: "harbor",
  46. });
  47. const tools = {
  48. listBooks: vi.fn(async () => ["harbor"]),
  49. writeNextChapter: vi.fn(async () => ({ ok: true })),
  50. reviseDraft: vi.fn(async () => ({ ok: true })),
  51. patchChapterText: vi.fn(async () => ({ ok: true })),
  52. renameEntity: vi.fn(async () => ({ ok: true })),
  53. updateCurrentFocus: vi.fn(async () => ({ ok: true })),
  54. updateAuthorIntent: vi.fn(async () => ({ ok: true })),
  55. writeTruthFile: vi.fn(async () => ({ ok: true })),
  56. };
  57. await processTuiInput(projectRoot, "把 focus 拉回旧案线", tools);
  58. expect(tools.updateCurrentFocus).toHaveBeenCalledWith("harbor", "把 focus 拉回旧案线");
  59. });
  60. it("persists automation mode changes through the session", async () => {
  61. await persistProjectSession(projectRoot, {
  62. ...createProjectSession(projectRoot),
  63. activeBookId: "harbor",
  64. });
  65. const tools = {
  66. listBooks: vi.fn(async () => ["harbor"]),
  67. writeNextChapter: vi.fn(async () => ({ ok: true })),
  68. reviseDraft: vi.fn(async () => ({ ok: true })),
  69. patchChapterText: vi.fn(async () => ({ ok: true })),
  70. renameEntity: vi.fn(async () => ({ ok: true })),
  71. updateCurrentFocus: vi.fn(async () => ({ ok: true })),
  72. updateAuthorIntent: vi.fn(async () => ({ ok: true })),
  73. writeTruthFile: vi.fn(async () => ({ ok: true })),
  74. };
  75. const result = await processTuiInput(projectRoot, "切换到全自动", tools);
  76. expect(result.session.automationMode).toBe("auto");
  77. });
  78. it("routes truth-file edits through the shared control flow", async () => {
  79. await persistProjectSession(projectRoot, {
  80. ...createProjectSession(projectRoot),
  81. activeBookId: "harbor",
  82. });
  83. const tools = {
  84. listBooks: vi.fn(async () => ["harbor"]),
  85. writeNextChapter: vi.fn(async () => ({ ok: true })),
  86. reviseDraft: vi.fn(async () => ({ ok: true })),
  87. patchChapterText: vi.fn(async () => ({ ok: true })),
  88. renameEntity: vi.fn(async () => ({ ok: true })),
  89. updateCurrentFocus: vi.fn(async () => ({ ok: true })),
  90. updateAuthorIntent: vi.fn(async () => ({ ok: true })),
  91. writeTruthFile: vi.fn(async () => ({ ok: true })),
  92. };
  93. await processTuiInput(projectRoot, "/truth current_focus.md Bring it back", tools);
  94. expect(tools.writeTruthFile).toHaveBeenCalledWith(
  95. "harbor",
  96. "current_focus.md",
  97. "Bring it back",
  98. );
  99. });
  100. it("binds the active book through /open in multi-book projects", async () => {
  101. await mkdir(join(projectRoot, "books", "beta"), { recursive: true });
  102. await writeFile(join(projectRoot, "books", "beta", "book.json"), "{}", "utf-8");
  103. await persistProjectSession(projectRoot, createProjectSession(projectRoot));
  104. const tools = {
  105. listBooks: vi.fn(async () => ["harbor", "beta"]),
  106. writeNextChapter: vi.fn(async () => ({ ok: true })),
  107. reviseDraft: vi.fn(async () => ({ ok: true })),
  108. patchChapterText: vi.fn(async () => ({ ok: true })),
  109. renameEntity: vi.fn(async () => ({ ok: true })),
  110. updateCurrentFocus: vi.fn(async () => ({ ok: true })),
  111. updateAuthorIntent: vi.fn(async () => ({ ok: true })),
  112. writeTruthFile: vi.fn(async () => ({ ok: true })),
  113. };
  114. const result = await processTuiInput(projectRoot, "/open beta", tools);
  115. expect(result.session.activeBookId).toBe("beta");
  116. expect(result.session.messages.at(-1)?.content).toContain("beta");
  117. });
  118. it("formats interactive summaries in English when the project language is en", async () => {
  119. await writeFile(join(projectRoot, "inkos.json"), JSON.stringify({ language: "en" }), "utf-8");
  120. await persistProjectSession(projectRoot, createProjectSession(projectRoot));
  121. const tools = {
  122. listBooks: vi.fn(async () => ["harbor", "beta"]),
  123. writeNextChapter: vi.fn(async () => ({ ok: true })),
  124. reviseDraft: vi.fn(async () => ({ ok: true })),
  125. patchChapterText: vi.fn(async () => ({ ok: true })),
  126. renameEntity: vi.fn(async () => ({ ok: true })),
  127. updateCurrentFocus: vi.fn(async () => ({ ok: true })),
  128. updateAuthorIntent: vi.fn(async () => ({ ok: true })),
  129. writeTruthFile: vi.fn(async () => ({ ok: true })),
  130. };
  131. const result = await processTuiInput(projectRoot, "/open beta", tools);
  132. expect(result.session.messages.at(-1)?.content).toBe("Active book: beta");
  133. await writeFile(join(projectRoot, "inkos.json"), JSON.stringify({ language: "zh" }), "utf-8");
  134. });
  135. });