import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll, describe, expect, it } from "vitest"; import { createOntologyEngine, initOntologyPackage, loadOntologyPackage, localActor } from "@logicsrc/openontology"; import { renderOntologyKeyHelp, renderOntologyTui, PANEL_KEYS } from "./openontology.js"; const dirs: string[] = []; afterAll(() => { for (const dir of dirs) rmSync(dir, { recursive: true, force: true }); }); function engine() { const dir = mkdtempSync(join(tmpdir(), "tui-ontology-")); dirs.push(dir); initOntologyPackage(dir, { id: "test-ecosystem", now: "2026-07-26T00:00:00Z" }); return createOntologyEngine({ package: loadOntologyPackage(dir), actor: localActor("curator"), clock: () => "2026-07-26T00:00:00Z" }); } describe("ontology TUI", () => { it("renders a bordered frame with the key bar and status line", () => { const output = renderOntologyTui(engine()); const lines = output.split("\n"); expect(lines[0]!.startsWith("┌")).toBe(true); expect(lines.at(-1)!.startsWith("└")).toBe(true); expect(output).toContain("e entities"); expect(output).toContain("q quit"); expect(output).toContain("asserted claims"); }); it("never exceeds the requested width, and stays usable when narrow", () => { for (const width of [60, 78, 120]) { const lines = renderOntologyTui(engine(), { width }).split("\n"); const widths = new Set(lines.map((line) => [...line].length)); expect(widths.size).toBe(1); expect([...widths][0]).toBe(Math.max(width, 60)); } }); it("shows every panel", () => { const e = engine(); expect(renderOntologyTui(e, { panel: "types" })).toContain("Person"); expect(renderOntologyTui(e, { panel: "entities" })).toContain("Alice Reyes"); expect(renderOntologyTui(e, { panel: "claims" })).toContain("worksOn"); expect(renderOntologyTui(e, { panel: "sources" })).toContain("web-page"); expect(renderOntologyTui(e, { panel: "queries" })).toContain("contributors"); expect(renderOntologyTui(e, { panel: "changesets" })).toContain("no change sets"); expect(renderOntologyTui(e, { panel: "violations" })).toContain("no errors"); expect(renderOntologyTui(e, { panel: "audit" })).toContain("no events"); // Rendering is read-only: repainting must not append to the audit log. expect(renderOntologyTui(e, { panel: "audit" })).toContain("no events"); }); it("distinguishes claim status by glyph and word, not colour", () => { const claims = renderOntologyTui(engine(), { panel: "claims", rows: 20 }); expect(claims).toMatch(/✓ asserted/); // No ANSI escapes: the panel must survive a monochrome terminal. expect(claims.includes("[")).toBe(false); }); it("surfaces proposed change sets and disputes in the status bar", () => { const e = engine(); e.createOntologyChangeSet({ title: "pending", operations: [ { op: "assert-claim", value: { subject: "test:person:alice", predicate: "worksOn", object: { entity: "test:project:docs-portal" }, sources: ["test:source:repo"] } } ] }); expect(renderOntologyTui(e)).toContain("1 proposed"); expect(renderOntologyTui(e, { panel: "changesets" })).toContain("pending"); }); it("lists the keys it binds", () => { expect(renderOntologyKeyHelp()).toContain("v: validate"); expect(PANEL_KEYS.map((entry) => entry.key)).toContain("q"); }); });