| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 |
- import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
- import { tmpdir } from "node:os";
- import { join } from "node:path";
- import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
- import { createProjectSession, persistProjectSession } from "../tui/session-store.js";
- import { processTuiInput } from "../tui/app.js";
- let projectRoot: string;
- describe("tui control flow", () => {
- beforeAll(async () => {
- projectRoot = await mkdtemp(join(tmpdir(), "inkos-tui-control-"));
- await mkdir(join(projectRoot, "books", "harbor"), { recursive: true });
- await writeFile(join(projectRoot, "books", "harbor", "book.json"), "{}", "utf-8");
- await writeFile(join(projectRoot, "inkos.json"), JSON.stringify({ language: "zh" }), "utf-8");
- });
- afterAll(async () => {
- // tmpdir cleanup omitted
- });
- it("routes continue through the active project book", async () => {
- await persistProjectSession(projectRoot, {
- ...createProjectSession(projectRoot),
- activeBookId: "harbor",
- });
- const tools = {
- listBooks: vi.fn(async () => ["harbor"]),
- writeNextChapter: vi.fn(async () => ({ ok: true })),
- reviseDraft: vi.fn(async () => ({ ok: true })),
- patchChapterText: vi.fn(async () => ({ ok: true })),
- renameEntity: vi.fn(async () => ({ ok: true })),
- updateCurrentFocus: vi.fn(async () => ({ ok: true })),
- updateAuthorIntent: vi.fn(async () => ({ ok: true })),
- writeTruthFile: vi.fn(async () => ({ ok: true })),
- };
- const result = await processTuiInput(projectRoot, "continue", tools);
- expect(tools.writeNextChapter).toHaveBeenCalledWith("harbor");
- expect(result.session.activeBookId).toBe("harbor");
- expect(result.session.currentExecution?.status).toBe("waiting_human");
- expect(result.session.pendingDecision?.kind).toBe("review-next-step");
- expect(result.session.pendingDecision?.summary).toContain("等待");
- expect(result.session.messages.at(0)?.role).toBe("user");
- expect(result.session.messages.at(-1)?.role).toBe("assistant");
- });
- it("routes focus updates through the interaction runtime", async () => {
- await persistProjectSession(projectRoot, {
- ...createProjectSession(projectRoot),
- activeBookId: "harbor",
- });
- const tools = {
- listBooks: vi.fn(async () => ["harbor"]),
- writeNextChapter: vi.fn(async () => ({ ok: true })),
- reviseDraft: vi.fn(async () => ({ ok: true })),
- patchChapterText: vi.fn(async () => ({ ok: true })),
- renameEntity: vi.fn(async () => ({ ok: true })),
- updateCurrentFocus: vi.fn(async () => ({ ok: true })),
- updateAuthorIntent: vi.fn(async () => ({ ok: true })),
- writeTruthFile: vi.fn(async () => ({ ok: true })),
- };
- await processTuiInput(projectRoot, "把 focus 拉回旧案线", tools);
- expect(tools.updateCurrentFocus).toHaveBeenCalledWith("harbor", "把 focus 拉回旧案线");
- });
- it("persists automation mode changes through the session", async () => {
- await persistProjectSession(projectRoot, {
- ...createProjectSession(projectRoot),
- activeBookId: "harbor",
- });
- const tools = {
- listBooks: vi.fn(async () => ["harbor"]),
- writeNextChapter: vi.fn(async () => ({ ok: true })),
- reviseDraft: vi.fn(async () => ({ ok: true })),
- patchChapterText: vi.fn(async () => ({ ok: true })),
- renameEntity: vi.fn(async () => ({ ok: true })),
- updateCurrentFocus: vi.fn(async () => ({ ok: true })),
- updateAuthorIntent: vi.fn(async () => ({ ok: true })),
- writeTruthFile: vi.fn(async () => ({ ok: true })),
- };
- const result = await processTuiInput(projectRoot, "切换到全自动", tools);
- expect(result.session.automationMode).toBe("auto");
- });
- it("routes truth-file edits through the shared control flow", async () => {
- await persistProjectSession(projectRoot, {
- ...createProjectSession(projectRoot),
- activeBookId: "harbor",
- });
- const tools = {
- listBooks: vi.fn(async () => ["harbor"]),
- writeNextChapter: vi.fn(async () => ({ ok: true })),
- reviseDraft: vi.fn(async () => ({ ok: true })),
- patchChapterText: vi.fn(async () => ({ ok: true })),
- renameEntity: vi.fn(async () => ({ ok: true })),
- updateCurrentFocus: vi.fn(async () => ({ ok: true })),
- updateAuthorIntent: vi.fn(async () => ({ ok: true })),
- writeTruthFile: vi.fn(async () => ({ ok: true })),
- };
- await processTuiInput(projectRoot, "/truth current_focus.md Bring it back", tools);
- expect(tools.writeTruthFile).toHaveBeenCalledWith(
- "harbor",
- "current_focus.md",
- "Bring it back",
- );
- });
- it("binds the active book through /open in multi-book projects", async () => {
- await mkdir(join(projectRoot, "books", "beta"), { recursive: true });
- await writeFile(join(projectRoot, "books", "beta", "book.json"), "{}", "utf-8");
- await persistProjectSession(projectRoot, createProjectSession(projectRoot));
- const tools = {
- listBooks: vi.fn(async () => ["harbor", "beta"]),
- writeNextChapter: vi.fn(async () => ({ ok: true })),
- reviseDraft: vi.fn(async () => ({ ok: true })),
- patchChapterText: vi.fn(async () => ({ ok: true })),
- renameEntity: vi.fn(async () => ({ ok: true })),
- updateCurrentFocus: vi.fn(async () => ({ ok: true })),
- updateAuthorIntent: vi.fn(async () => ({ ok: true })),
- writeTruthFile: vi.fn(async () => ({ ok: true })),
- };
- const result = await processTuiInput(projectRoot, "/open beta", tools);
- expect(result.session.activeBookId).toBe("beta");
- expect(result.session.messages.at(-1)?.content).toContain("beta");
- });
- it("formats interactive summaries in English when the project language is en", async () => {
- await writeFile(join(projectRoot, "inkos.json"), JSON.stringify({ language: "en" }), "utf-8");
- await persistProjectSession(projectRoot, createProjectSession(projectRoot));
- const tools = {
- listBooks: vi.fn(async () => ["harbor", "beta"]),
- writeNextChapter: vi.fn(async () => ({ ok: true })),
- reviseDraft: vi.fn(async () => ({ ok: true })),
- patchChapterText: vi.fn(async () => ({ ok: true })),
- renameEntity: vi.fn(async () => ({ ok: true })),
- updateCurrentFocus: vi.fn(async () => ({ ok: true })),
- updateAuthorIntent: vi.fn(async () => ({ ok: true })),
- writeTruthFile: vi.fn(async () => ({ ok: true })),
- };
- const result = await processTuiInput(projectRoot, "/open beta", tools);
- expect(result.session.messages.at(-1)?.content).toBe("Active book: beta");
- await writeFile(join(projectRoot, "inkos.json"), JSON.stringify({ language: "zh" }), "utf-8");
- });
- });
|