mirror of
https://github.com/Narcooo/inkos.git
synced 2026-09-24 15:29:47 +08:00
test(studio): server integration for authoring confirm flow; fix authoring session wiring
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user