mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-10-07 16:48:09 +08:00
196 lines
6.3 KiB
JavaScript
196 lines
6.3 KiB
JavaScript
// End-to-end tests for the standalone viewer (packages/viewer/bin/viewer.mjs).
|
|
// Spawns the real server against a fixture project and exercises the token
|
|
// gate, graph sanitisation, file-content allowlist, and .ua/legacy resolution.
|
|
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
import { execFileSync, spawn } from "node:child_process";
|
|
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
const VIEWER_BIN = join(
|
|
REPO_ROOT,
|
|
"understand-anything-plugin",
|
|
"packages",
|
|
"viewer",
|
|
"bin",
|
|
"viewer.mjs",
|
|
);
|
|
const VIEWER_DIST = join(REPO_ROOT, "understand-anything-plugin", "packages", "viewer", "dist");
|
|
|
|
function fixtureGraph(gitCommitHash) {
|
|
return {
|
|
version: "1.0.0",
|
|
project: {
|
|
name: "fixture", languages: ["ts"], frameworks: [], description: "d",
|
|
analyzedAt: "2026-07-17T00:00:00.000Z", gitCommitHash,
|
|
},
|
|
nodes: [
|
|
{
|
|
id: "file:src/a.ts", type: "file", name: "a.ts", filePath: "src/a.ts",
|
|
summary: "s", tags: [], complexity: "simple",
|
|
},
|
|
],
|
|
edges: [],
|
|
layers: [],
|
|
tour: [],
|
|
};
|
|
}
|
|
|
|
function git(projectRoot, ...args) {
|
|
return execFileSync("git", args, {
|
|
cwd: projectRoot,
|
|
encoding: "utf8",
|
|
}).trim();
|
|
}
|
|
|
|
function setupProject(dataDirName) {
|
|
const root = mkdtempSync(join(tmpdir(), "ua-viewer-"));
|
|
mkdirSync(join(root, "src"), { recursive: true });
|
|
writeFileSync(join(root, "src", "a.ts"), "export const a = 1;\n");
|
|
writeFileSync(join(root, "secret.txt"), "not in graph\n");
|
|
git(root, "init");
|
|
git(root, "config", "user.email", "viewer-tests@example.com");
|
|
git(root, "config", "user.name", "Viewer Tests");
|
|
git(root, "add", "--all");
|
|
git(root, "commit", "-m", "baseline");
|
|
|
|
const dataDir = join(root, dataDirName);
|
|
mkdirSync(dataDir, { recursive: true });
|
|
writeFileSync(
|
|
join(dataDir, "knowledge-graph.json"),
|
|
JSON.stringify(fixtureGraph(git(root, "rev-parse", "HEAD"))),
|
|
);
|
|
return root;
|
|
}
|
|
|
|
/** Start the viewer and wait for the printed URL. Returns { proc, url, token, port }. */
|
|
function startViewer(projectRoot) {
|
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
const proc = spawn(
|
|
process.execPath,
|
|
[VIEWER_BIN, projectRoot, "--no-open", "--port", "0"],
|
|
{ env: { ...process.env }, stdio: ["ignore", "pipe", "pipe"] },
|
|
);
|
|
let out = "";
|
|
const timer = setTimeout(() => {
|
|
proc.kill();
|
|
rejectPromise(new Error(`viewer did not start.\n${out}`));
|
|
}, 10_000);
|
|
const onData = (chunk) => {
|
|
out += String(chunk);
|
|
const m = out.match(/http:\/\/127\.0\.0\.1:(\d+)\/\?token=([a-f0-9]+)/);
|
|
if (m) {
|
|
clearTimeout(timer);
|
|
resolvePromise({ proc, url: m[0], port: Number(m[1]), token: m[2] });
|
|
}
|
|
};
|
|
proc.stdout.on("data", onData);
|
|
proc.stderr.on("data", onData);
|
|
proc.on("exit", (code) => {
|
|
clearTimeout(timer);
|
|
rejectPromise(new Error(`viewer exited with ${code}.\n${out}`));
|
|
});
|
|
});
|
|
}
|
|
|
|
describe.skipIf(!existsSync(VIEWER_DIST))("understand-anything-viewer", () => {
|
|
let root;
|
|
let viewer;
|
|
|
|
beforeAll(async () => {
|
|
root = setupProject(".ua");
|
|
viewer = await startViewer(root);
|
|
}, 15_000);
|
|
|
|
afterAll(() => {
|
|
viewer?.proc.kill();
|
|
if (root) rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
const base = () => `http://127.0.0.1:${viewer.port}`;
|
|
|
|
it("serves the embedded dashboard index", async () => {
|
|
const res = await fetch(`${base()}/`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toContain("<!doctype html>");
|
|
});
|
|
|
|
it("rejects data requests without the token", async () => {
|
|
const res = await fetch(`${base()}/knowledge-graph.json`);
|
|
expect(res.status).toBe(403);
|
|
});
|
|
|
|
it("serves the graph from .ua/ with a valid token", async () => {
|
|
const res = await fetch(`${base()}/knowledge-graph.json?token=${viewer.token}`);
|
|
expect(res.status).toBe(200);
|
|
const graph = await res.json();
|
|
expect(graph.nodes).toHaveLength(1);
|
|
expect(graph.nodes[0].filePath).toBe("src/a.ts");
|
|
});
|
|
|
|
it("serves a protected no-store freshness report", async () => {
|
|
const denied = await fetch(`${base()}/staleness.json`);
|
|
expect(denied.status).toBe(403);
|
|
expect(denied.headers.get("cache-control")).toBe("no-store");
|
|
|
|
const res = await fetch(
|
|
`${base()}/staleness.json?token=${viewer.token}`,
|
|
);
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers.get("cache-control")).toBe("no-store");
|
|
expect(await res.json()).toMatchObject({
|
|
graphs: {
|
|
knowledge: {
|
|
status: "fresh",
|
|
graphCommitHash: git(root, "rev-parse", "HEAD"),
|
|
headCommitHash: git(root, "rev-parse", "HEAD"),
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("serves file content only for files listed in the graph", async () => {
|
|
const ok = await fetch(
|
|
`${base()}/file-content.json?token=${viewer.token}&path=${encodeURIComponent("src/a.ts")}`,
|
|
);
|
|
expect(ok.status).toBe(200);
|
|
const body = await ok.json();
|
|
expect(body.content).toContain("export const a");
|
|
expect(body.language).toBe("typescript");
|
|
|
|
const denied = await fetch(
|
|
`${base()}/file-content.json?token=${viewer.token}&path=secret.txt`,
|
|
);
|
|
expect(denied.status).toBe(404);
|
|
});
|
|
|
|
it("rejects path traversal in file-content", async () => {
|
|
const res = await fetch(
|
|
`${base()}/file-content.json?token=${viewer.token}&path=${encodeURIComponent("../outside.txt")}`,
|
|
);
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it("blocks static requests escaping dist/", async () => {
|
|
const res = await fetch(`${base()}/%2e%2e/package.json`);
|
|
expect([403, 404]).toContain(res.status);
|
|
});
|
|
|
|
it("falls back to legacy .understand-anything/ projects", async () => {
|
|
const legacyRoot = setupProject(".understand-anything");
|
|
const legacyViewer = await startViewer(legacyRoot);
|
|
try {
|
|
const res = await fetch(
|
|
`http://127.0.0.1:${legacyViewer.port}/knowledge-graph.json?token=${legacyViewer.token}`,
|
|
);
|
|
expect(res.status).toBe(200);
|
|
expect((await res.json()).nodes).toHaveLength(1);
|
|
} finally {
|
|
legacyViewer.proc.kill();
|
|
rmSync(legacyRoot, { recursive: true, force: true });
|
|
}
|
|
}, 15_000);
|
|
});
|