test(studio): server integration for authoring confirm flow; fix authoring session wiring

This commit is contained in:
Ma
2026-06-24 14:49:14 +08:00
parent fd3b9f34e9
commit eca7fe523c
3 changed files with 98 additions and 4 deletions
+23 -3
View File
@@ -35,9 +35,29 @@ function lastUserText(context: { messages?: Array<{ role: string; content: unkno
function alreadyProposed(
context: { messages?: Array<{ role: string; content: unknown; toolName?: string }> },
): boolean {
return (context.messages ?? []).some(
(m) => m.role === "toolResult" && (m as { toolName?: string }).toolName === "propose_action",
);
return (context.messages ?? []).some((m) => {
// Agent format (non-openai-completions): role="toolResult" with toolName
if (m.role === "toolResult" && (m as { toolName?: string }).toolName === "propose_action") {
return true;
}
// LLM format (openai-completions): assistant message with toolCall content
if (m.role === "assistant" && Array.isArray(m.content)) {
if (
(m.content as Array<{ type: string; name?: string }>).some(
(c) => c.type === "toolCall" && c.name === "propose_action",
)
) {
return true;
}
}
// LLM format (openai-completions folded): tool result folded into user message string
if (m.role === "user" && typeof m.content === "string") {
if (/- propose_action \(/.test(m.content as string)) {
return true;
}
}
return false;
});
}
/**
@@ -0,0 +1,74 @@
import { describe, expect, it, beforeAll, afterAll, beforeEach, afterEach } from "vitest";
import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createStudioServer } from "../api/server.js";
import { createAndPersistBookSession, loadStoryGraph } from "@actalk/inkos-core";
const INKOS_CONFIG = JSON.stringify({
name: "test-project",
version: "0.1.0",
language: "zh",
llm: { model: "test-model", provider: "anthropic" },
notify: [],
});
describe("interactive-film-authoring confirm flow (stubbed LLM)", () => {
let root: string;
const prev = process.env.INKOS_AGENT_LLM_STUB;
beforeAll(() => { process.env.INKOS_AGENT_LLM_STUB = "1"; });
afterAll(() => {
if (prev === undefined) delete process.env.INKOS_AGENT_LLM_STUB;
else process.env.INKOS_AGENT_LLM_STUB = prev;
});
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), "if-confirm-"));
await writeFile(join(root, "inkos.json"), INKOS_CONFIG, "utf-8");
await mkdir(join(root, "interactive-films", "p"), { recursive: true });
});
afterEach(async () => { await rm(root, { recursive: true, force: true }); });
it("free-text proposes draft_structure, confirm creates the graph", async () => {
const app = createStudioServer({} as never, root);
const sessionId = "1000000000-test";
const bookId = "p";
// Pre-create the session so the agent endpoint can load it
await createAndPersistBookSession(root, bookId, sessionId, "interactive-film-authoring");
// Step 1: free-text instruction → stubbed agent proposes draft_structure via propose_action
const propose = await app.request("/api/v1/agent", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
instruction: "帮我搭一个三幕结构",
activeBookId: bookId,
sessionKind: "interactive-film-authoring",
actionSource: "free-text",
sessionId,
}),
});
expect(propose.status).toBe(200);
// Step 2: confirm the proposed action → executeConfirmedProductionAction runs draft_structure
// stubChatCompletion returns STRUCTURE_JSON (4 nodes) when prompt mentions "骨架/nodes/结构"
const confirm = await app.request("/api/v1/agent", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
instruction: "搭建三幕分支结构",
activeBookId: bookId,
sessionKind: "interactive-film-authoring",
actionSource: "button",
requestedIntent: "draft_structure",
actionPayload: { draftStructure: { instruction: "三幕分支结构", projectId: bookId } },
sessionId,
}),
});
expect(confirm.status).toBe(200);
// Assert the story graph was created with at least 4 nodes
const graph = await loadStoryGraph(root, bookId);
expect(graph?.nodes.length).toBeGreaterThanOrEqual(4);
});
});
+1 -1
View File
@@ -3458,7 +3458,7 @@ export function createStudioServer(initialConfig: ProjectConfig, root: string, o
bookSession = updatedSession;
}
let activeBookConfig: { readonly language?: string } | null = null;
if (agentBookId) {
if (agentBookId && sessionKind !== "interactive-film-authoring") {
try {
activeBookConfig = await state.loadBookConfig(agentBookId);
} catch {