Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions scripts/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ if [ -d /app/public-defaults ]; then
echo "[phantom] Syncing public assets from image..."
rm -rf /app/public/chat 2>/dev/null || true
cp -r /app/public-defaults/chat /app/public/chat 2>/dev/null || true
if [ -d /app/public-defaults/dashboard ]; then
rm -rf /app/public/dashboard 2>/dev/null || true
cp -r /app/public-defaults/dashboard /app/public/dashboard 2>/dev/null || true
chown -R 999:999 /app/public/dashboard 2>/dev/null || true
fi
for f in _base.html _components.html _agent-name.js index.html phantom-logo.svg; do
[ -f "/app/public-defaults/$f" ] && cp "/app/public-defaults/$f" "/app/public/$f"
done
Expand Down
95 changes: 95 additions & 0 deletions src/core/__tests__/server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import YAML from "yaml";
import { hashTokenSync } from "../../mcp/config.ts";
import type { McpConfig } from "../../mcp/types.ts";
import { startServer } from "../server.ts";

/**
* End-to-end routing tests: bare-root redirect and /health content negotiation.
* A real Bun.serve instance is started on an ephemeral port so we exercise the
* same fetch pipeline production traffic sees.
*/
describe("server routing", () => {
const mcpConfigPath = "config/mcp.yaml";
let originalMcpYaml: string | null = null;
let server: ReturnType<typeof Bun.serve>;
let baseUrl: string;

beforeAll(() => {
if (existsSync(mcpConfigPath)) {
originalMcpYaml = readFileSync(mcpConfigPath, "utf-8");
}
const mcpConfig: McpConfig = {
tokens: [{ name: "admin", hash: hashTokenSync("test-admin"), scopes: ["read", "operator", "admin"] }],
rate_limit: { requests_per_minute: 60, burst: 10 },
};
mkdirSync("config", { recursive: true });
writeFileSync(mcpConfigPath, YAML.stringify(mcpConfig), "utf-8");

server = startServer({ name: "phantom", port: 0, role: "base" } as never, Date.now());
baseUrl = `http://localhost:${server.port}`;
});

afterAll(() => {
server?.stop(true);
if (originalMcpYaml !== null) {
writeFileSync(mcpConfigPath, originalMcpYaml, "utf-8");
}
});

describe("GET /", () => {
test("redirects to /ui/ with 302", async () => {
const res = await fetch(`${baseUrl}/`, { redirect: "manual" });
expect(res.status).toBe(302);
expect(res.headers.get("Location")).toBe("/ui/");
});
});

describe("GET /health", () => {
test("no Accept header returns JSON", async () => {
const res = await fetch(`${baseUrl}/health`);
expect(res.status).toBe(200);
expect(res.headers.get("Content-Type")).toContain("application/json");
const body = (await res.json()) as { agent: string; status: string };
expect(body.agent).toBe("phantom");
expect(body.status).toBe("ok");
});

test("Accept: application/json returns JSON", async () => {
const res = await fetch(`${baseUrl}/health`, { headers: { Accept: "application/json" } });
expect(res.status).toBe(200);
expect(res.headers.get("Content-Type")).toContain("application/json");
const body = (await res.json()) as { agent: string };
expect(body.agent).toBe("phantom");
});

test("Accept: text/html returns the HTML page", async () => {
const res = await fetch(`${baseUrl}/health`, { headers: { Accept: "text/html" } });
expect(res.status).toBe(200);
expect(res.headers.get("Content-Type")).toContain("text/html");
const body = await res.text();
expect(body).toContain("<!DOCTYPE html");
expect(body).toContain("phantom");
expect(body).toContain("phantom-nav-brand");
});

test("browser-style Accept returns the HTML page", async () => {
const res = await fetch(`${baseUrl}/health`, {
headers: { Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" },
});
expect(res.status).toBe(200);
expect(res.headers.get("Content-Type")).toContain("text/html");
const body = await res.text();
expect(body).toContain("<!DOCTYPE html");
});

test("?format=json overrides HTML Accept", async () => {
const res = await fetch(`${baseUrl}/health?format=json`, { headers: { Accept: "text/html" } });
expect(res.status).toBe(200);
expect(res.headers.get("Content-Type")).toContain("application/json");
const body = (await res.json()) as { agent: string };
expect(body.agent).toBe("phantom");
});
});
});
Loading
Loading