101 lines
5.4 KiB
JavaScript
101 lines
5.4 KiB
JavaScript
import { createInterface } from "node:readline";
|
|
import assert from "node:assert/strict";
|
|
|
|
// Test the web proxy/API and real model without sending licensed CMS content to it.
|
|
// Supply credentials only over hidden stdin. Never log response bodies or secrets.
|
|
const base = "http://127.0.0.1:5173";
|
|
const reader = createInterface({ input: process.stdin });
|
|
let sessionId;
|
|
let stage = "input";
|
|
const abort = new AbortController();
|
|
async function request(path, method = "GET", body) {
|
|
const response = await fetch(base + path, {
|
|
method, redirect: "error", signal: AbortSignal.timeout(30_000),
|
|
headers: { Origin: base, ...(body === undefined ? {} : { "Content-Type": "application/json" }) },
|
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
});
|
|
const result = await response.json();
|
|
if (!response.ok) {
|
|
const error = new Error("API request failed");
|
|
error.code = /^[A-Z_]{1,80}$/.test(result.error?.code ?? "") ? result.error.code : "API_REQUEST_FAILED";
|
|
throw error;
|
|
}
|
|
return result;
|
|
}
|
|
console.log("Provide CMS email/password as one JSON line on hidden stdin.");
|
|
try {
|
|
const input = JSON.parse((await reader[Symbol.asyncIterator]().next()).value);
|
|
reader.close();
|
|
stage = "discovery";
|
|
const catalog = await request("/api/skills");
|
|
assert(catalog.skills.some((skill) => skill.name === "swads-short-drama-editor"));
|
|
({ sessionId } = await request("/api/sessions", "POST"));
|
|
stage = "login";
|
|
let login;
|
|
try { login = await request(`/api/sessions/${sessionId}/cms/connection`, "POST", { email: input.email, password: input.password }); }
|
|
finally { input.email = ""; input.password = ""; }
|
|
assert.equal(login.connection.connected, true);
|
|
stage = "catalog";
|
|
const dramas = await request(`/api/sessions/${sessionId}/cms/dramas`);
|
|
assert(dramas.dramas.length > 0);
|
|
console.log(JSON.stringify({ stage, connected: true, dramaCount: dramas.dramas.length, skillDiscovered: true }));
|
|
stage = "model";
|
|
const timeout = setTimeout(() => abort.abort(), 150_000);
|
|
const stream = await fetch(`${base}/api/sessions/${sessionId}/events`, { signal: abort.signal });
|
|
const streamReader = stream.body.getReader();
|
|
const sent = await request(`/api/sessions/${sessionId}/messages`, "POST", {
|
|
text: "做 swads-short-drama-editor 接入验收:先完整读取该 Skill 的 SKILL.md 和 references/cms-source-contract.md,然后调用 cms_connection_status。不要读取 CMS 剧集、字幕、任何业务数据,不要调用其他工具或生成文件。最后用中文简短说明已经连接和仍然缺失的能力,不要声称成片功能已经完成。",
|
|
attachmentIds: [],
|
|
});
|
|
let pending = "";
|
|
let terminal = false;
|
|
let skillLoaded = false;
|
|
let referenceRead = false;
|
|
const referenceCalls = new Set();
|
|
let statusCompleted = false;
|
|
let answerReceived = false;
|
|
const decoder = new TextDecoder();
|
|
try {
|
|
while (!terminal) {
|
|
const { value, done } = await streamReader.read();
|
|
assert(!done, "Unexpected stream end");
|
|
pending += decoder.decode(value, { stream: true });
|
|
let boundary;
|
|
while ((boundary = pending.indexOf("\n\n")) !== -1) {
|
|
const frame = pending.slice(0, boundary);
|
|
pending = pending.slice(boundary + 2);
|
|
const data = frame.split("\n").find((line) => line.startsWith("data: "))?.slice(6);
|
|
if (!data) continue;
|
|
const event = JSON.parse(data);
|
|
if (event.runId !== sent.runId) continue;
|
|
assert.notEqual(event.type, "approval.required", "Unexpected content access request");
|
|
if (event.type === "skill.loaded") skillLoaded = true;
|
|
if (event.type === "tool.completed" && event.payload.toolName === "cms_connection_status") {
|
|
const result = event.payload.result?.details ?? event.payload.result;
|
|
statusCompleted = result?.connected === true && result?.capabilities?.rendering === false;
|
|
}
|
|
if (event.type === "tool.requested" && event.payload.toolName === "read" && JSON.stringify(event.payload.arguments).includes("references/cms-source-contract.md")) referenceCalls.add(event.payload.toolCallId);
|
|
if (event.type === "tool.completed" && referenceCalls.has(event.payload.toolCallId)) referenceRead = true;
|
|
if (event.type === "assistant.delta" && event.payload.delta?.trim()) answerReceived = true;
|
|
if (["run.completed", "run.failed", "run.cancelled"].includes(event.type)) {
|
|
console.log(JSON.stringify({ terminal: event.type, skillLoaded, referenceRead, statusCompleted, answerReceived }));
|
|
assert.equal(event.type, "run.completed");
|
|
terminal = true;
|
|
}
|
|
}
|
|
}
|
|
assert(skillLoaded && referenceRead && statusCompleted && answerReceived, "Required Skill/tool events missing");
|
|
console.log(JSON.stringify({ result: "PASS", skillLoaded, referenceRead, statusCompleted, answerReceived, cmsContentSentToModel: false, videoPipelineVerified: false }));
|
|
} finally { clearTimeout(timeout); abort.abort(); }
|
|
} catch (error) {
|
|
console.error(JSON.stringify({ result: "FAIL", stage, code: error.code ?? "ASSERTION_OR_NETWORK_FAILURE" }));
|
|
process.exitCode = 1;
|
|
} finally {
|
|
reader.close(); abort.abort();
|
|
if (sessionId) {
|
|
await request(`/api/sessions/${sessionId}/cancel`, "POST").catch(() => {});
|
|
await request(`/api/sessions/${sessionId}/cms/connection`, "DELETE").catch(() => {});
|
|
await request(`/api/sessions/${sessionId}`, "DELETE").catch(() => {});
|
|
}
|
|
}
|