import { describe, it, expect } from "vitest"; import { manifestView, type Manifest } from "../manifest"; import type { TelemetryRecord } from "../telemetry"; import { hookRegistry, registeringPlugin, pluginRegistry, pluginInternals, liveFireTally, } from "../topology-view"; /** Build a tolerant view over a minimal manifest with a given topology + model. */ function viewOf(topology?: object, model?: object) { const manifest = { version: 3, actions: [], events: [], model: model ?? { nodes: [{ id: "_seed", kind: "_seed", name: "_seed" }], edges: [] }, ...(topology ? { topology } : {}), } as unknown as Manifest; return manifestView(manifest); } describe("topology-view: hookRegistry", () => { it("shapes topology.hooks into name-sorted rows with defaulted counts", () => { const view = viewOf({ hooks: [ { name: "zeta", chain: 3, listeners: 2 }, { name: "alpha" }, // missing counts default to 0 ], plugins: [], }); const rows = hookRegistry(view); expect(rows.map((r) => r.name)).toEqual(["alpha", "zeta"]); expect(rows[0]).toMatchObject({ id: "hook:alpha", chain: 0, listeners: 0 }); expect(rows[1]).toMatchObject({ chain: 3, listeners: 2 }); }); it("uses the topology hook id when present", () => { const view = viewOf({ hooks: [{ name: "h", id: "custom-id" }], plugins: [] }); expect(hookRegistry(view)[0]!.id).toBe("custom-id"); }); it("carries the hook's source location through to the row (the SourcePill)", () => { const source = { file: "/app/orders.ts", line: 12, column: 4 }; const view = viewOf({ hooks: [{ name: "h", source }], plugins: [] }); expect(hookRegistry(view)[0]!.source).toEqual(source); }); it("returns [] for a null view or a topology-less manifest", () => { expect(hookRegistry(null)).toEqual([]); expect(hookRegistry(viewOf(undefined))).toEqual([]); }); }); describe("topology-view: registeringPlugin", () => { it("extracts the plugin name from boot/shutdown lifecycle hooks", () => { expect(registeringPlugin("plugin.boot:auth")).toBe("auth"); expect(registeringPlugin("plugin.shutdown:storage")).toBe("storage"); }); it("returns null for non-lifecycle hooks and empty input", () => { expect(registeringPlugin("action.before:x")).toBeNull(); expect(registeringPlugin("")).toBeNull(); expect(registeringPlugin(undefined)).toBeNull(); }); }); describe("topology-view: pluginRegistry", () => { it("shapes topology.plugins into name-sorted rows", () => { const view = viewOf({ hooks: [], plugins: [{ name: "storage" }, { name: "auth" }] }); const rows = pluginRegistry(view); expect(rows.map((r) => r.name)).toEqual(["auth", "storage"]); expect(rows[0]!.id).toBe("plugin:auth"); }); it("returns [] without a topology", () => { expect(pluginRegistry(viewOf(undefined))).toEqual([]); }); }); describe("topology-view: pluginInternals", () => { it("collects boot/shutdown lifecycle hooks (boot first)", () => { const view = viewOf({ plugins: [{ name: "auth" }], hooks: [ { name: "plugin.shutdown:auth", chain: 1, listeners: 0 }, { name: "plugin.boot:auth", chain: 2, listeners: 1 }, { name: "plugin.boot:other" }, // a different plugin — excluded ], }); const internals = pluginInternals(view, "auth")!; expect(internals.lifecycleHooks.map((h) => h.phase)).toEqual(["boot", "shutdown"]); expect(internals.lifecycleHooks[0]).toMatchObject({ name: "plugin.boot:auth", chain: 2, listeners: 1, }); }); it("reads contributes/mounts edges and a same-named capability's ctx-by-kind", () => { const model = { nodes: [ { id: "plugin:auth", kind: "plugin", name: "auth" }, { id: "binding:tokenStore", kind: "binding", name: "tokenStore" }, { id: "route:GET /me", kind: "route", name: "GET /me" }, { id: "capability:auth", kind: "capability", name: "auth", data: { kinds: ["action"], ctxKeys: ["currentUser", "can"] }, }, ], edges: [ { from: "plugin:auth", to: "binding:tokenStore", type: "contributes" }, { from: "plugin:auth", to: "route:GET /me", type: "mounts" }, ], }; const view = viewOf({ plugins: [{ name: "auth" }], hooks: [] }, model); const internals = pluginInternals(view, "auth")!; expect(internals.contributes).toEqual(["GET /me", "tokenStore"]); expect(internals.ctxByKind).toEqual({ action: ["currentUser", "can"] }); }); it("returns null for a null view or no name", () => { expect(pluginInternals(null, "auth")).toBeNull(); expect(pluginInternals(viewOf({ plugins: [], hooks: [] }), null)).toBeNull(); }); }); describe("topology-view: liveFireTally", () => { const step = (hookName: string, phase: string, ts: string): TelemetryRecord => ({ kind: "hook.step", hookName, phase, ts }) as unknown as TelemetryRecord; it("counts one fire per start phase and tracks the last timestamp", () => { const recs = [ step("h", "start", "2026-01-01T00:00:00Z"), step("h", "end", "2026-01-01T00:00:01Z"), step("h", "start", "2026-01-01T00:00:02Z"), ]; const tally = liveFireTally(recs); expect(tally.get("h")).toEqual({ fires: 2, lastTs: "2026-01-01T00:00:02Z" }); }); it("ignores non-hook.step records and records without a hook name", () => { const recs = [ { kind: "action.completed", ts: "t" } as unknown as TelemetryRecord, { kind: "hook.step", phase: "start", ts: "t" } as unknown as TelemetryRecord, ]; expect(liveFireTally(recs).size).toBe(0); }); it("returns an empty map for no records (graceful idle)", () => { expect(liveFireTally([]).size).toBe(0); }); });