Files
kxkm_clown/apps/api/src/app.test.ts
T
L'électron rare d2fed6087a feat: Add MCP server and smoke test scripts
- Introduced `mcp-server.js` to expose KXKM personas as MCP tools, supporting chat, persona listing, web search, and status checks.
- Implemented `mcp-server-smoke.js` for testing the MCP server functionality, ensuring compatibility with both new and legacy message formats.
- Created `setup-voice-clone.sh` for managing voice cloning environment setup, including bootstrapping, sample generation, and smoke testing.
- Added `state.json` to track project status and task outputs for various batches.
- Generated summary files for deep cycle and overall project status, capturing performance and security findings.
2026-03-19 16:18:44 +01:00

493 lines
17 KiB
TypeScript

import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
import { readFile, rm } from "node:fs/promises";
import supertest from "supertest";
import { createApp } from "./app.js";
// Ensure no Postgres connection is attempted
delete process.env.DATABASE_URL;
process.env.ADMIN_TOKEN = "test-admin-token";
const TEST_LOCAL_DIR = path.join(process.cwd(), ".tmp-test-v2-local");
process.env.KXKM_LOCAL_DATA_DIR = TEST_LOCAL_DIR;
describe("V2 API", () => {
let request: ReturnType<typeof supertest>;
before(async () => {
await rm(TEST_LOCAL_DIR, { recursive: true, force: true });
const { app } = await createApp();
request = supertest(app);
});
after(async () => {
await rm(TEST_LOCAL_DIR, { recursive: true, force: true });
delete process.env.KXKM_LOCAL_DATA_DIR;
delete process.env.ADMIN_TOKEN;
});
// ---------------------------------------------------------------------------
// Helper: login and return session cookie string
// ---------------------------------------------------------------------------
async function loginAs(role: string, username = `test_${role}`): Promise<string> {
const payload: Record<string, string> = { username, role };
if (["admin", "operator", "editor"].includes(role) && process.env.ADMIN_TOKEN) {
payload.token = process.env.ADMIN_TOKEN;
}
const res = await request
.post("/api/session/login")
.send(payload)
.expect(200);
const setCookie = res.headers["set-cookie"];
const raw = Array.isArray(setCookie) ? setCookie[0] : setCookie;
assert.ok(raw, "expected Set-Cookie header after login");
// Return just the cookie key=value portion
return raw.split(";")[0];
}
// ---------------------------------------------------------------------------
// Storage contract
// ---------------------------------------------------------------------------
describe("Storage contract", () => {
it("throws in production without DATABASE_URL", async () => {
const savedEnv = process.env.NODE_ENV;
const savedDb = process.env.DATABASE_URL;
try {
delete process.env.DATABASE_URL;
process.env.NODE_ENV = "production";
await assert.rejects(
createApp(),
/DATABASE_URL is required when NODE_ENV=production/,
);
} finally {
if (savedEnv !== undefined) { process.env.NODE_ENV = savedEnv; } else { delete process.env.NODE_ENV; }
if (savedDb !== undefined) { process.env.DATABASE_URL = savedDb; } else { delete process.env.DATABASE_URL; }
}
});
});
// ---------------------------------------------------------------------------
// Health
// ---------------------------------------------------------------------------
describe("Health", () => {
it("GET /api/v2/health returns 200", async () => {
const res = await request.get("/api/v2/health").expect(200);
assert.equal(res.body.ok, true);
assert.equal(res.body.data.app, "@kxkm/api");
});
});
// ---------------------------------------------------------------------------
// Session
// ---------------------------------------------------------------------------
describe("Session", () => {
let sessionCookie: string;
it("POST /api/session/login creates session", async () => {
const res = await request
.post("/api/session/login")
.send({ username: "admin_user", role: "admin", token: process.env.ADMIN_TOKEN })
.expect(200);
assert.equal(res.body.ok, true);
assert.equal(res.body.data.role, "admin");
assert.equal(res.body.data.username, "admin_user");
assert.ok(res.body.data.id, "session should have an id");
const setCookie = res.headers["set-cookie"];
const raw = Array.isArray(setCookie) ? setCookie[0] : setCookie;
assert.ok(raw, "expected Set-Cookie header");
sessionCookie = raw.split(";")[0];
});
it("GET /api/session returns current session", async () => {
const res = await request
.get("/api/session")
.set("Cookie", sessionCookie)
.expect(200);
assert.equal(res.body.ok, true);
assert.equal(res.body.data.username, "admin_user");
assert.equal(res.body.data.role, "admin");
});
it("POST /api/session/logout destroys session", async () => {
const res = await request
.post("/api/session/logout")
.set("Cookie", sessionCookie)
.expect(200);
assert.equal(res.body.ok, true);
assert.equal(res.body.data.loggedOut, true);
});
it("GET /api/session returns 401 after logout", async () => {
const res = await request
.get("/api/session")
.set("Cookie", sessionCookie)
.expect(401);
assert.equal(res.body.ok, false);
assert.equal(res.body.error, "session_required");
});
it("POST /api/session/login rejects invalid payload", async () => {
await request
.post("/api/session/login")
.send({ username: "", role: "admin", token: process.env.ADMIN_TOKEN })
.expect(400);
});
it("POST /api/session/login rejects invalid role", async () => {
await request
.post("/api/session/login")
.send({ username: "tester", role: "superadmin", token: process.env.ADMIN_TOKEN })
.expect(400);
});
});
// ---------------------------------------------------------------------------
// Personas
// ---------------------------------------------------------------------------
describe("Personas", () => {
let cookie: string;
before(async () => {
cookie = await loginAs("admin");
});
it("GET /api/personas lists seeded personas", async () => {
const res = await request
.get("/api/personas")
.set("Cookie", cookie)
.expect(200);
assert.equal(res.body.ok, true);
assert.ok(Array.isArray(res.body.data));
assert.ok(res.body.data.length >= 5, `expected >= 5 personas, got ${res.body.data.length}`);
});
it("GET /api/personas/:id returns persona", async () => {
const res = await request
.get("/api/personas/schaeffer")
.set("Cookie", cookie)
.expect(200);
assert.equal(res.body.ok, true);
assert.equal(res.body.data.id, "schaeffer");
assert.equal(res.body.data.name, "Schaeffer");
});
it("GET /api/personas/:id returns 404 for unknown id", async () => {
await request
.get("/api/personas/nonexistent")
.set("Cookie", cookie)
.expect(404);
});
it("PUT /api/admin/personas/:id updates persona", async () => {
const res = await request
.put("/api/admin/personas/batty")
.set("Cookie", cookie)
.send({ name: "Batty Updated", summary: "Updated summary" })
.expect(200);
assert.equal(res.body.ok, true);
assert.equal(res.body.data.name, "Batty Updated");
assert.equal(res.body.data.summary, "Updated summary");
});
it("GET /api/admin/personas/:id/source returns source", async () => {
const res = await request
.get("/api/admin/personas/schaeffer/source")
.set("Cookie", cookie)
.expect(200);
assert.equal(res.body.ok, true);
assert.equal(res.body.data.personaId, "schaeffer");
});
it("GET /api/admin/personas/:id/feedback returns array", async () => {
const res = await request
.get("/api/admin/personas/batty/feedback")
.set("Cookie", cookie)
.expect(200);
assert.equal(res.body.ok, true);
assert.ok(Array.isArray(res.body.data));
// After the PUT above, there should be at least 1 feedback entry
assert.ok(res.body.data.length >= 1, "expected feedback after admin edit");
});
it("GET /api/admin/personas/:id/proposals returns array", async () => {
const res = await request
.get("/api/admin/personas/schaeffer/proposals")
.set("Cookie", cookie)
.expect(200);
assert.equal(res.body.ok, true);
assert.ok(Array.isArray(res.body.data));
});
it("uploads, reports and deletes a voice sample using the local data dir", async () => {
const audio = Buffer.from("RIFF-test-voice-sample").toString("base64");
const uploadRes = await request
.post("/api/admin/personas/schaeffer/voice-sample")
.set("Cookie", cookie)
.send({ audio })
.expect(200);
assert.equal(uploadRes.body.ok, true);
assert.equal(uploadRes.body.data.samplePath, path.join(".tmp-test-v2-local", "voice-samples", "schaeffer.wav"));
const persisted = await readFile(path.join(TEST_LOCAL_DIR, "voice-samples", "schaeffer.wav"));
assert.equal(persisted.toString("utf8"), "RIFF-test-voice-sample");
const statusRes = await request
.get("/api/admin/personas/schaeffer/voice-sample")
.set("Cookie", cookie)
.expect(200);
assert.equal(statusRes.body.ok, true);
assert.equal(statusRes.body.data.hasVoiceSample, true);
assert.equal(statusRes.body.data.samplePath, path.join(".tmp-test-v2-local", "voice-samples", "schaeffer.wav"));
const deleteRes = await request
.delete("/api/admin/personas/schaeffer/voice-sample")
.set("Cookie", cookie)
.expect(200);
assert.equal(deleteRes.body.ok, true);
assert.equal(deleteRes.body.data.deleted, true);
const missingStatusRes = await request
.get("/api/admin/personas/schaeffer/voice-sample")
.set("Cookie", cookie)
.expect(200);
assert.equal(missingStatusRes.body.ok, true);
assert.equal(missingStatusRes.body.data.hasVoiceSample, false);
});
it("persists local persona updates across app recreation", async () => {
const { app: app2 } = await createApp();
const request2 = supertest(app2);
const login2 = await request2
.post("/api/session/login")
.send({ username: "reload_admin", role: "admin", token: process.env.ADMIN_TOKEN })
.expect(200);
const setCookie = login2.headers["set-cookie"];
const raw = Array.isArray(setCookie) ? setCookie[0] : setCookie;
assert.ok(raw, "expected Set-Cookie header after app recreation login");
const cookie2 = raw.split(";")[0];
const res = await request2
.get("/api/personas/batty")
.set("Cookie", cookie2)
.expect(200);
assert.equal(res.body.ok, true);
assert.equal(res.body.data.summary, "Updated summary");
});
});
// ---------------------------------------------------------------------------
// Node Engine
// ---------------------------------------------------------------------------
describe("Node Engine", () => {
let cookie: string;
let createdGraphId: string;
let createdRunId: string;
before(async () => {
// operator has node_engine:read and node_engine:operate
cookie = await loginAs("operator");
});
it("GET /api/admin/node-engine/overview returns overview", async () => {
const res = await request
.get("/api/admin/node-engine/overview")
.set("Cookie", cookie)
.expect(200);
assert.equal(res.body.ok, true);
assert.ok(res.body.data !== undefined);
});
it("POST /api/admin/node-engine/graphs creates graph", async () => {
const res = await request
.post("/api/admin/node-engine/graphs")
.set("Cookie", cookie)
.send({ name: "test_graph", description: "Integration test graph" })
.expect(201);
assert.equal(res.body.ok, true);
assert.ok(res.body.data.id);
assert.equal(res.body.data.name, "test_graph");
createdGraphId = res.body.data.id;
});
it("GET /api/admin/node-engine/graphs lists graphs", async () => {
const res = await request
.get("/api/admin/node-engine/graphs")
.set("Cookie", cookie)
.expect(200);
assert.equal(res.body.ok, true);
assert.ok(Array.isArray(res.body.data));
// At least the starter graph + the one we just created
assert.ok(res.body.data.length >= 2, `expected >= 2 graphs, got ${res.body.data.length}`);
});
it("PUT /api/admin/node-engine/graphs/:id updates graph", async () => {
const res = await request
.put(`/api/admin/node-engine/graphs/${createdGraphId}`)
.set("Cookie", cookie)
.send({ name: "test_graph_updated", description: "Updated description" })
.expect(200);
assert.equal(res.body.ok, true);
assert.equal(res.body.data.name, "test_graph_updated");
});
it("POST /api/admin/node-engine/graphs/:id/run starts run", async () => {
const res = await request
.post(`/api/admin/node-engine/graphs/${createdGraphId}/run`)
.set("Cookie", cookie)
.send({ hold: true }) // prevent auto-transition timers in tests
.expect(201);
assert.equal(res.body.ok, true);
assert.ok(res.body.data.id);
assert.equal(res.body.data.graphId, createdGraphId);
assert.equal(res.body.data.status, "queued");
createdRunId = res.body.data.id;
});
it("GET /api/admin/node-engine/runs/:id returns run", async () => {
const res = await request
.get(`/api/admin/node-engine/runs/${createdRunId}`)
.set("Cookie", cookie)
.expect(200);
assert.equal(res.body.ok, true);
assert.equal(res.body.data.id, createdRunId);
});
it("POST /api/admin/node-engine/runs/:id/cancel cancels run", async () => {
const res = await request
.post(`/api/admin/node-engine/runs/${createdRunId}/cancel`)
.set("Cookie", cookie)
.expect(200);
assert.equal(res.body.ok, true);
assert.equal(res.body.data.status, "cancelled");
});
it("GET /api/admin/node-engine/models returns models", async () => {
const res = await request
.get("/api/admin/node-engine/models")
.set("Cookie", cookie)
.expect(200);
assert.equal(res.body.ok, true);
assert.ok(Array.isArray(res.body.data));
assert.ok(res.body.data.length >= 1, "expected at least 1 model");
});
it("PUT /api/admin/node-engine/graphs/:id returns 404 for unknown graph", async () => {
await request
.put("/api/admin/node-engine/graphs/nonexistent")
.set("Cookie", cookie)
.send({ name: "nope" })
.expect(404);
});
it("GET /api/admin/node-engine/runs/:id returns 404 for unknown run", async () => {
await request
.get("/api/admin/node-engine/runs/nonexistent")
.set("Cookie", cookie)
.expect(404);
});
});
// ---------------------------------------------------------------------------
// Chat
// ---------------------------------------------------------------------------
describe("Chat", () => {
let cookie: string;
before(async () => {
cookie = await loginAs("viewer");
});
it("GET /api/chat/channels returns channels", async () => {
const res = await request
.get("/api/chat/channels")
.set("Cookie", cookie)
.expect(200);
assert.equal(res.body.ok, true);
assert.ok(Array.isArray(res.body.data));
// DEFAULT_CHANNELS (2) + 3 model-dedicated channels = 5
assert.ok(res.body.data.length >= 2, `expected >= 2 channels, got ${res.body.data.length}`);
});
});
// ---------------------------------------------------------------------------
// Auth enforcement
// ---------------------------------------------------------------------------
describe("Auth enforcement", () => {
it("admin routes return 401 without session", async () => {
const res = await request
.get("/api/admin/node-engine/overview")
.expect(401);
assert.equal(res.body.ok, false);
assert.equal(res.body.error, "session_required");
});
it("session-required routes return 401 without session", async () => {
await request.get("/api/personas").expect(401);
await request.get("/api/chat/channels").expect(401);
});
it("admin routes return 403 for viewer role", async () => {
const viewerCookie = await loginAs("viewer", "viewer_authtest");
// viewer has persona:read but NOT persona:write
const res = await request
.put("/api/admin/personas/schaeffer")
.set("Cookie", viewerCookie)
.send({ name: "Should Fail" })
.expect(403);
assert.equal(res.body.ok, false);
assert.equal(res.body.error, "permission_denied");
});
it("operator cannot write personas (no persona:write)", async () => {
const opCookie = await loginAs("operator", "op_authtest");
await request
.put("/api/admin/personas/schaeffer")
.set("Cookie", opCookie)
.send({ name: "Should Fail" })
.expect(403);
});
it("editor cannot operate node-engine (no node_engine:operate)", async () => {
const editorCookie = await loginAs("editor", "ed_authtest");
await request
.post("/api/admin/node-engine/graphs")
.set("Cookie", editorCookie)
.send({ name: "nope" })
.expect(403);
});
});
});