* Live smoke: trigger ask_user via CopilotKit and verify suspend stream events.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { randomUUID } from "node:crypto";
import { createAuthenticatedTestClient } from "./lib/authenticated-test-client.mjs";
const envPath = join(process.cwd(), ".env");
try {
for (const line of readFileSync(envPath, "utf8").split("\n")) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq === -1) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1).trim();
if (!process.env[key]) process.env[key] = value;
}
} catch {
}
if (!process.env.LLM_API_KEY) {
console.error("LLM_API_KEY missing — cannot run live ask_user smoke.");
process.exit(1);
}
const apiBase = `http://127.0.0.1:${process.env.API_PORT ?? "8787"}`;
const client = createAuthenticatedTestClient({ baseUrl: apiBase });
await client.registerAndLogin({ displayName: "Ask User Smoke" });
const threadId = `thread-ask-smoke-${Date.now()}`;
const runId = randomUUID();
const payload = {
method: "agent/run",
params: { agentId: "dataFoundry" },
body: {
threadId,
runId,
messages: [
{
id: randomUUID(),
role: "user",
content:
"你必须调用 ask_user 工具,向用户提问「你想分析哪个数据源?」并提供两个选项 orders 和 customers。不要自行假设答案,不要执行 SQL,只调用 ask_user 然后等待。",
},
],
tools: [],
context: [{ description: "datasource_id", value: "api-duckdb-demo" }],
state: {},
forwardedProps: { datasourceId: "api-duckdb-demo" },
},
};
const response = await client.fetch("/api/copilotkit", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
body: JSON.stringify(payload),
});
if (!response.ok) {
console.error("HTTP", response.status, await response.text());
process.exit(1);
}
const events = [];
const decoder = new TextDecoder();
let buffer = "";
for await (const chunk of response.body) {
buffer += decoder.decode(chunk, { stream: true });
const parts = buffer.split("\n\n");
buffer = parts.pop() ?? "";
for (const part of parts) {
for (const line of part.split("\n")) {
if (!line.startsWith("data: ")) continue;
const raw = line.slice(6).trim();
if (!raw || raw === "[DONE]") continue;
try {
events.push(JSON.parse(raw));
} catch {
}
}
}
}
const customNames = events
.filter((event) => event.type === "CUSTOM")
.map((event) => event.name);
const hasOnInterrupt = customNames.includes("on_interrupt");
const hasInteractionRequested = customNames.includes("interaction.requested");
const hasRunFinished = events.some((event) => event.type === "RUN_FINISHED");
const runErrors = events.filter((event) => event.type === "RUN_ERROR");
const hasIncompleteStreamError = runErrors.some((event) =>
String(event.message ?? "").includes("Run ended without emitting a terminal event"),
);
const suspendedDelta = events.some(
(event) =>
event.type === "STATE_DELTA" &&
JSON.stringify(event.delta ?? []).includes("suspended"),
);
console.log("Event counts:", {
total: events.length,
custom: customNames.length,
runErrors: runErrors.length,
});
console.log("Custom names:", [...new Set(customNames)]);
console.log("Checks:", {
hasOnInterrupt,
hasInteractionRequested,
hasRunFinished,
hasIncompleteStreamError,
suspendedDelta,
});
if (runErrors.length > 0) {
console.log("RUN_ERROR messages:", runErrors.map((event) => event.message));
}
const ok =
hasOnInterrupt &&
hasInteractionRequested &&
hasRunFinished &&
!hasIncompleteStreamError &&
suspendedDelta;
if (!ok) {
console.error("ask_user interrupt smoke FAILED");
process.exit(1);
}
console.log("ask_user interrupt smoke OK");