import { describe, it, expect } from "vitest"; import { manifestView, type Manifest } from "../manifest"; import { inspectKinds, nodesOfKind, matchesNode, nodeToDetail, relationships, kindLabel, isDispatchable, } from "../inspect"; const manifest = { version: 3, // flat arrays — nodeToDetail prefers these for schema actions: [ { name: "orders.place", app: "orders", public: true, description: "Place an order", inputSchema: { type: "object" }, emits: ["orders.placed"], source: { file: "orders/place.ts", line: 3 }, }, ], events: [{ name: "orders.placed", app: "orders" }], model: { nodes: [ { id: "action:orders.place", kind: "action", name: "orders.place", source: { file: "orders/place.ts", line: 3 }, intent: { description: "Place an order", public: true }, data: { emits: ["orders.placed"] }, }, { id: "event:orders.placed", kind: "event", name: "orders.placed" }, { id: "plugin:auth", kind: "plugin", name: "auth", source: { file: "plugins/auth.ts", line: 1 }, intent: { description: "Auth plugin" }, data: { contributes: ["binding:user"] }, }, { id: "query:orders.by-id", kind: "query", name: "orders.by-id" }, { id: "app:orders", kind: "app", name: "orders", intent: { description: "Orders BC" }, data: { plugins: ["forge.actions"], env: ["DATABASE_URL", "PORT"], config: [{ file: "/abs/app/config/orders.ts", keys: ["orders", "limits"] }], }, }, ], edges: [ { from: "action:orders.place", to: "event:orders.placed", type: "emits" }, { from: "query:orders.by-id", to: "action:orders.place", type: "dispatches" }, ], }, } as unknown as Manifest; const view = manifestView(manifest); describe("kindLabel", () => { it("maps kinds to friendly labels, falling back to the raw kind", () => { expect(kindLabel("action")).toBe("Actions"); expect(kindLabel("sinkStage")).toBe("Sinks"); expect(kindLabel("mystery")).toBe("mystery"); }); }); describe("inspectKinds", () => { it("lists present kinds with counts in curated order", () => { const out = inspectKinds(view); expect(out.map((k) => k.kind)).toEqual(["action", "query", "event", "app", "plugin"]); expect(out.find((k) => k.kind === "action")!.count).toBe(1); }); it("returns [] for a null view", () => { expect(inspectKinds(null)).toEqual([]); }); }); describe("nodesOfKind", () => { it("returns the kind's nodes, name-sorted", () => { expect(nodesOfKind(view, "action").map((n) => n.name)).toEqual(["orders.place"]); expect(nodesOfKind(view, "nope")).toEqual([]); }); }); describe("matchesNode", () => { const node = nodesOfKind(view, "action")[0]!; it("matches name + description, case-insensitively", () => { expect(matchesNode(node, "PLACE")).toBe(true); expect(matchesNode(node, "place an order")).toBe(true); expect(matchesNode(node, "zzz")).toBe(false); }); it("empty query matches", () => { expect(matchesNode(node, "")).toBe(true); }); }); describe("nodeToDetail", () => { it("prefers the flat entry (carries the schema) for member kinds", () => { const node = nodesOfKind(view, "action")[0]!; const d = nodeToDetail(view, manifest, node)!; expect(d.name).toBe("orders.place"); expect(d.schema).toEqual({ type: "object" }); expect(d.emits).toEqual(["orders.placed"]); expect(d.public).toBe(true); }); it("builds detail from the graph node for structural kinds (no flat entry)", () => { const node = nodesOfKind(view, "plugin")[0]!; const d = nodeToDetail(view, manifest, node)!; expect(d.kind).toBe("plugin"); expect(d.description).toBe("Auth plugin"); expect(d.source).toEqual({ file: "plugins/auth.ts", line: 1 }); expect(d.extra.contributes).toEqual(["binding:user"]); }); it("returns null for a null node", () => { expect(nodeToDetail(view, manifest, null)).toBeNull(); }); it("surfaces env + config on app nodes (not in generic extra)", () => { const node = nodesOfKind(view, "app")[0]!; const d = nodeToDetail(view, manifest, node)!; expect(d.kind).toBe("app"); expect(d.env).toEqual(["DATABASE_URL", "PORT"]); expect(d.config).toEqual([{ file: "/abs/app/config/orders.ts", keys: ["orders", "limits"] }]); // env/config render as their own sections — never duplicated into Details. expect(d.extra.env).toBeUndefined(); expect(d.extra.config).toBeUndefined(); expect(d.extra.plugins).toEqual(["forge.actions"]); }); }); describe("relationships", () => { it("groups a node's edges by humanized, direction-aware relation", () => { const groups = relationships(view, "action:orders.place"); const byLabel = Object.fromEntries(groups.map((g) => [g.label, g.items.map((i) => i.id)])); expect(byLabel["Emits"]).toEqual(["event:orders.placed"]); expect(byLabel["Dispatched by"]).toEqual(["query:orders.by-id"]); }); it("is empty for a node with no edges", () => { expect(relationships(view, "event:orders.placed").every((g) => g.items.length)).toBe(true); expect(relationships(view, "nope:x")).toEqual([]); }); }); describe("isDispatchable", () => { it("is true for action / command / query", () => { expect(isDispatchable("action")).toBe(true); expect(isDispatchable("command")).toBe(true); expect(isDispatchable("query")).toBe(true); expect(isDispatchable("event")).toBe(false); }); });