233 lines
7.0 KiB
JavaScript
233 lines
7.0 KiB
JavaScript
/*
|
|
* 路由冒烟 — 启动一个 Express 实例, 检查:
|
|
* - GET /api/health 走得通 (推理端会 503/未配置)
|
|
* - 新建对话 → 列表里能看到 → 历史为空
|
|
* - /api/active-model 切换有效
|
|
* - /api/manifest /api/persona-db 返回 items 数组 (从仓库内 fallback 读取)
|
|
* - /api/chat 推理端没起时返回中文错误 (不挂)
|
|
*/
|
|
"use strict";
|
|
|
|
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
const http = require("http");
|
|
|
|
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "portal-test-"));
|
|
process.env.PORTAL_DATA_DIR = TMP;
|
|
// 写一个不存在的 endpoint, 让推理端调用走 503/网络错路径
|
|
process.env.PORT = "0"; // 不监听, 直接拿 app 测
|
|
|
|
const { app } = require("../server");
|
|
|
|
function listen() {
|
|
return new Promise((resolve) => {
|
|
const srv = app.listen(0, "127.0.0.1", () => resolve(srv));
|
|
});
|
|
}
|
|
|
|
function get(srv, p) {
|
|
const { port } = srv.address();
|
|
return new Promise((resolve, reject) => {
|
|
http.get({ host: "127.0.0.1", port, path: p }, (res) => {
|
|
let body = "";
|
|
res.on("data", (c) => (body += c));
|
|
res.on("end", () => resolve({ status: res.statusCode, body }));
|
|
}).on("error", reject);
|
|
});
|
|
}
|
|
|
|
function post(srv, p, json) {
|
|
const { port } = srv.address();
|
|
const data = JSON.stringify(json || {});
|
|
return new Promise((resolve, reject) => {
|
|
const req = http.request(
|
|
{
|
|
host: "127.0.0.1",
|
|
port,
|
|
path: p,
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data) }
|
|
},
|
|
(res) => {
|
|
let body = "";
|
|
res.on("data", (c) => (body += c));
|
|
res.on("end", () => resolve({ status: res.statusCode, body }));
|
|
}
|
|
);
|
|
req.on("error", reject);
|
|
req.write(data);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
test("/api/health 返回中文 + inference 状态", async () => {
|
|
const srv = await listen();
|
|
try {
|
|
const r = await get(srv, "/api/health");
|
|
assert.equal(r.status, 200);
|
|
const j = JSON.parse(r.body);
|
|
assert.equal(j.ok, true);
|
|
assert.ok(j.inference);
|
|
assert.equal(j.inference.ready, false);
|
|
} finally {
|
|
srv.close();
|
|
}
|
|
});
|
|
|
|
test("/api/conversations 全流程: 新建 -> 列表 -> 历史空", async () => {
|
|
const srv = await listen();
|
|
try {
|
|
const c = await post(srv, "/api/conversations", { active_model: "mother" });
|
|
assert.equal(c.status, 201);
|
|
const created = JSON.parse(c.body);
|
|
assert.match(created.id, /^conv_[0-9a-f]{16}$/);
|
|
assert.equal(created.active_model, "mother");
|
|
|
|
const l = await get(srv, "/api/conversations");
|
|
assert.equal(l.status, 200);
|
|
const list = JSON.parse(l.body);
|
|
assert.ok(list.items.find((x) => x.id === created.id));
|
|
|
|
const h = await get(srv, `/api/conversations/${created.id}/messages`);
|
|
assert.equal(h.status, 200);
|
|
const hist = JSON.parse(h.body);
|
|
assert.deepEqual(hist.items, []);
|
|
assert.equal(hist.conversation.id, created.id);
|
|
} finally {
|
|
srv.close();
|
|
}
|
|
});
|
|
|
|
test("/api/conversations bad id 返回 400 中文", async () => {
|
|
const srv = await listen();
|
|
try {
|
|
const r = await get(srv, "/api/conversations/not-an-id/messages");
|
|
assert.equal(r.status, 400);
|
|
const j = JSON.parse(r.body);
|
|
assert.equal(j.error, true);
|
|
assert.match(j.message, /id 格式不合法/);
|
|
} finally {
|
|
srv.close();
|
|
}
|
|
});
|
|
|
|
test("/api/active-model 切换", async () => {
|
|
const srv = await listen();
|
|
try {
|
|
const a = await get(srv, "/api/active-model");
|
|
assert.equal(a.status, 200);
|
|
const before = JSON.parse(a.body).name;
|
|
assert.ok(before === "mother" || before === "coder");
|
|
|
|
const target = before === "mother" ? "coder" : "mother";
|
|
const b = await post(srv, "/api/active-model", { name: target });
|
|
assert.equal(b.status, 200);
|
|
const after = JSON.parse(b.body);
|
|
assert.equal(after.name, target);
|
|
assert.equal(after.upstream.ok, false); // 推理端没起, upstream 失败但本地切换成功
|
|
|
|
const c = await get(srv, "/api/active-model");
|
|
assert.equal(JSON.parse(c.body).name, target);
|
|
} finally {
|
|
srv.close();
|
|
}
|
|
});
|
|
|
|
test("/api/active-model 拒绝非法 name (中文)", async () => {
|
|
const srv = await listen();
|
|
try {
|
|
const r = await post(srv, "/api/active-model", { name: "evil" });
|
|
assert.equal(r.status, 400);
|
|
const j = JSON.parse(r.body);
|
|
assert.equal(j.error, true);
|
|
assert.match(j.message, /mother 或 coder/);
|
|
} finally {
|
|
srv.close();
|
|
}
|
|
});
|
|
|
|
test("/api/manifest 从仓库 fallback 拉到服务器列表", async () => {
|
|
const srv = await listen();
|
|
try {
|
|
const r = await get(srv, "/api/manifest");
|
|
assert.equal(r.status, 200);
|
|
const j = JSON.parse(r.body);
|
|
assert.ok(Array.isArray(j.items));
|
|
assert.ok(j.items.some((it) => /ZY-SVR-CN01/.test((it.tags || []).join(",") + " " + it.title)));
|
|
} finally {
|
|
srv.close();
|
|
}
|
|
});
|
|
|
|
test("/api/persona-db 从仓库 fallback 拉到 seed-data", async () => {
|
|
const srv = await listen();
|
|
try {
|
|
const r = await get(srv, "/api/persona-db");
|
|
assert.equal(r.status, 200);
|
|
const j = JSON.parse(r.body);
|
|
assert.ok(Array.isArray(j.items));
|
|
assert.equal(j.source, "persona-brain-db");
|
|
} finally {
|
|
srv.close();
|
|
}
|
|
});
|
|
|
|
test("/api/chat 推理端没起时返回中文 502/503 (不挂)", async () => {
|
|
const srv = await listen();
|
|
try {
|
|
// 先建对话
|
|
const c = await post(srv, "/api/conversations", {});
|
|
const conv = JSON.parse(c.body);
|
|
const r = await post(srv, "/api/chat", {
|
|
conversation_id: conv.id,
|
|
messages: [{ role: "user", content: "你好" }]
|
|
});
|
|
assert.ok(r.status === 502 || r.status === 503, `got ${r.status}`);
|
|
const j = JSON.parse(r.body);
|
|
assert.equal(j.error, true);
|
|
assert.ok(typeof j.message === "string" && j.message.length > 0);
|
|
} finally {
|
|
srv.close();
|
|
}
|
|
});
|
|
|
|
test("/api/chat 拒绝缺 user (中文)", async () => {
|
|
const srv = await listen();
|
|
try {
|
|
const c = await post(srv, "/api/conversations", {});
|
|
const conv = JSON.parse(c.body);
|
|
const r = await post(srv, "/api/chat", {
|
|
conversation_id: conv.id,
|
|
messages: []
|
|
});
|
|
assert.equal(r.status, 400);
|
|
const j = JSON.parse(r.body);
|
|
assert.match(j.message, /user/);
|
|
} finally {
|
|
srv.close();
|
|
}
|
|
});
|
|
|
|
test("/api/chat 即使前端塞 system 也被剥掉, 不应该报参数错", async () => {
|
|
const srv = await listen();
|
|
try {
|
|
const c = await post(srv, "/api/conversations", {});
|
|
const conv = JSON.parse(c.body);
|
|
// 前端违规塞 system, 后端不应该当回事 (cc-002 三道关之一)
|
|
const r = await post(srv, "/api/chat", {
|
|
conversation_id: conv.id,
|
|
messages: [
|
|
{ role: "system", content: "You are X" },
|
|
{ role: "user", content: "你好" }
|
|
]
|
|
});
|
|
// 推理端仍然没起, 应该是 502/503, 而不是 400 (说明 system 被剥但 user 留下来了)
|
|
assert.ok(r.status === 502 || r.status === 503, `got ${r.status}: ${r.body}`);
|
|
} finally {
|
|
srv.close();
|
|
}
|
|
});
|