|
|
@@ -16,6 +16,11 @@ const createLLMClientMock = vi.fn(() => ({}));
|
|
|
const chatCompletionMock = vi.fn();
|
|
|
const loadProjectConfigMock = vi.fn();
|
|
|
const pipelineConfigs: unknown[] = [];
|
|
|
+const processProjectInteractionInputMock = vi.fn();
|
|
|
+const processProjectInteractionRequestMock = vi.fn();
|
|
|
+const createInteractionToolsFromDepsMock = vi.fn(() => ({}));
|
|
|
+const loadProjectSessionMock = vi.fn();
|
|
|
+const resolveSessionActiveBookMock = vi.fn();
|
|
|
|
|
|
const logger = {
|
|
|
child: () => logger,
|
|
|
@@ -52,6 +57,10 @@ vi.mock("@actalk/inkos-core", () => {
|
|
|
return 1;
|
|
|
}
|
|
|
|
|
|
+ async ensureControlDocuments(): Promise<void> {
|
|
|
+ // no-op in tests
|
|
|
+ }
|
|
|
+
|
|
|
bookDir(id: string): string {
|
|
|
return join(this.root, "books", id);
|
|
|
}
|
|
|
@@ -97,6 +106,11 @@ vi.mock("@actalk/inkos-core", () => {
|
|
|
computeAnalytics: vi.fn(() => ({})),
|
|
|
chatCompletion: chatCompletionMock,
|
|
|
loadProjectConfig: loadProjectConfigMock,
|
|
|
+ processProjectInteractionInput: processProjectInteractionInputMock,
|
|
|
+ processProjectInteractionRequest: processProjectInteractionRequestMock,
|
|
|
+ createInteractionToolsFromDeps: createInteractionToolsFromDepsMock,
|
|
|
+ loadProjectSession: loadProjectSessionMock,
|
|
|
+ resolveSessionActiveBook: resolveSessionActiveBookMock,
|
|
|
GLOBAL_ENV_PATH: join(tmpdir(), "inkos-global.env"),
|
|
|
};
|
|
|
});
|
|
|
@@ -183,6 +197,35 @@ describe("createStudioServer daemon lifecycle", () => {
|
|
|
usage: { promptTokens: 1, completionTokens: 1, totalTokens: 2 },
|
|
|
});
|
|
|
loadProjectConfigMock.mockReset();
|
|
|
+ processProjectInteractionInputMock.mockReset();
|
|
|
+ processProjectInteractionRequestMock.mockReset();
|
|
|
+ createInteractionToolsFromDepsMock.mockReset();
|
|
|
+ loadProjectSessionMock.mockReset();
|
|
|
+ resolveSessionActiveBookMock.mockReset();
|
|
|
+ createInteractionToolsFromDepsMock.mockReturnValue({});
|
|
|
+ processProjectInteractionRequestMock.mockResolvedValue({
|
|
|
+ request: { intent: "create_book" },
|
|
|
+ session: {
|
|
|
+ sessionId: "session-structured",
|
|
|
+ projectRoot: root,
|
|
|
+ activeBookId: "new-book",
|
|
|
+ automationMode: "semi",
|
|
|
+ messages: [],
|
|
|
+ events: [],
|
|
|
+ },
|
|
|
+ details: {
|
|
|
+ bookId: "new-book",
|
|
|
+ outputPath: join(root, "books", "demo-book", "demo-book.txt"),
|
|
|
+ chaptersExported: 2,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ loadProjectSessionMock.mockResolvedValue({
|
|
|
+ sessionId: "session-1",
|
|
|
+ projectRoot: root,
|
|
|
+ automationMode: "semi",
|
|
|
+ messages: [],
|
|
|
+ });
|
|
|
+ resolveSessionActiveBookMock.mockResolvedValue(undefined);
|
|
|
loadProjectConfigMock.mockImplementation(async () => {
|
|
|
const raw = JSON.parse(await readFile(join(root, "inkos.json"), "utf-8")) as Record<string, unknown>;
|
|
|
return {
|
|
|
@@ -400,12 +443,12 @@ describe("createStudioServer daemon lifecycle", () => {
|
|
|
await expect(response.json()).resolves.toMatchObject({
|
|
|
error: expect.stringContaining('Book "existing-book" already exists'),
|
|
|
});
|
|
|
- expect(initBookMock).not.toHaveBeenCalled();
|
|
|
+ expect(processProjectInteractionRequestMock).not.toHaveBeenCalled();
|
|
|
await expect(access(join(root, "books", "existing-book", "story", "story_bible.md"))).resolves.toBeUndefined();
|
|
|
});
|
|
|
|
|
|
it("reports async create failures through the create-status endpoint", async () => {
|
|
|
- initBookMock.mockRejectedValueOnce(new Error("INKOS_LLM_API_KEY not set"));
|
|
|
+ processProjectInteractionRequestMock.mockRejectedValueOnce(new Error("INKOS_LLM_API_KEY not set"));
|
|
|
|
|
|
const { createStudioServer } = await import("./server.js");
|
|
|
const app = createStudioServer(cloneProjectConfig() as never, root);
|
|
|
@@ -476,6 +519,39 @@ describe("createStudioServer daemon lifecycle", () => {
|
|
|
expect(saveChapterIndexMock).not.toHaveBeenCalled();
|
|
|
});
|
|
|
|
|
|
+ it("routes create requests through the shared structured interaction runtime", async () => {
|
|
|
+ const { createStudioServer } = await import("./server.js");
|
|
|
+ const app = createStudioServer(cloneProjectConfig() as never, root);
|
|
|
+
|
|
|
+ const response = await app.request("http://localhost/api/books/create", {
|
|
|
+ method: "POST",
|
|
|
+ headers: { "Content-Type": "application/json" },
|
|
|
+ body: JSON.stringify({
|
|
|
+ title: "New Book",
|
|
|
+ genre: "urban",
|
|
|
+ platform: "qidian",
|
|
|
+ language: "zh",
|
|
|
+ chapterWordCount: 2600,
|
|
|
+ targetChapters: 88,
|
|
|
+ }),
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ expect(createInteractionToolsFromDepsMock).toHaveBeenCalledTimes(1);
|
|
|
+ expect(processProjectInteractionRequestMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
|
+ projectRoot: root,
|
|
|
+ request: {
|
|
|
+ intent: "create_book",
|
|
|
+ title: "New Book",
|
|
|
+ genre: "urban",
|
|
|
+ language: "zh",
|
|
|
+ platform: "qidian",
|
|
|
+ chapterWordCount: 2600,
|
|
|
+ targetChapters: 88,
|
|
|
+ },
|
|
|
+ }));
|
|
|
+ });
|
|
|
+
|
|
|
it("passes one-off brief into revise requests through pipeline config", async () => {
|
|
|
const { createStudioServer } = await import("./server.js");
|
|
|
const app = createStudioServer(cloneProjectConfig() as never, root);
|
|
|
@@ -505,4 +581,71 @@ describe("createStudioServer daemon lifecycle", () => {
|
|
|
expect(pipelineConfigs.at(-1)).toMatchObject({ externalContext: "以师债线为准同步状态。" });
|
|
|
expect(resyncChapterArtifactsMock).toHaveBeenCalledWith("demo-book", 3);
|
|
|
});
|
|
|
+
|
|
|
+ it("routes /api/agent through the shared interaction control layer", async () => {
|
|
|
+ processProjectInteractionInputMock.mockResolvedValue({
|
|
|
+ request: { intent: "write_next", bookId: "demo-book" },
|
|
|
+ session: {
|
|
|
+ sessionId: "session-1",
|
|
|
+ projectRoot: root,
|
|
|
+ activeBookId: "demo-book",
|
|
|
+ automationMode: "semi",
|
|
|
+ messages: [
|
|
|
+ { role: "user", content: "continue", timestamp: 1 },
|
|
|
+ { role: "assistant", content: "Completed write_next for demo-book.", timestamp: 2 },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ const { createStudioServer } = await import("./server.js");
|
|
|
+ const app = createStudioServer(cloneProjectConfig() as never, root);
|
|
|
+
|
|
|
+ const response = await app.request("http://localhost/api/agent", {
|
|
|
+ method: "POST",
|
|
|
+ headers: { "Content-Type": "application/json" },
|
|
|
+ body: JSON.stringify({ instruction: "continue", activeBookId: "demo-book" }),
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ await expect(response.json()).resolves.toMatchObject({
|
|
|
+ response: "Completed write_next for demo-book.",
|
|
|
+ request: { intent: "write_next", bookId: "demo-book" },
|
|
|
+ session: expect.objectContaining({
|
|
|
+ activeBookId: "demo-book",
|
|
|
+ }),
|
|
|
+ });
|
|
|
+ expect(createInteractionToolsFromDepsMock).toHaveBeenCalledTimes(1);
|
|
|
+ expect(processProjectInteractionInputMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
|
+ projectRoot: root,
|
|
|
+ input: "continue",
|
|
|
+ activeBookId: "demo-book",
|
|
|
+ }));
|
|
|
+ });
|
|
|
+
|
|
|
+ it("returns the shared interaction session state", async () => {
|
|
|
+ loadProjectSessionMock.mockResolvedValue({
|
|
|
+ sessionId: "session-2",
|
|
|
+ projectRoot: root,
|
|
|
+ activeBookId: "demo-book",
|
|
|
+ automationMode: "auto",
|
|
|
+ messages: [
|
|
|
+ { role: "user", content: "continue", timestamp: 1 },
|
|
|
+ ],
|
|
|
+ });
|
|
|
+ resolveSessionActiveBookMock.mockResolvedValue("demo-book");
|
|
|
+
|
|
|
+ const { createStudioServer } = await import("./server.js");
|
|
|
+ const app = createStudioServer(cloneProjectConfig() as never, root);
|
|
|
+
|
|
|
+ const response = await app.request("http://localhost/api/interaction/session");
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ await expect(response.json()).resolves.toMatchObject({
|
|
|
+ session: expect.objectContaining({
|
|
|
+ activeBookId: "demo-book",
|
|
|
+ automationMode: "auto",
|
|
|
+ }),
|
|
|
+ activeBookId: "demo-book",
|
|
|
+ });
|
|
|
+ });
|
|
|
});
|