import { describe, it, expect } from "vitest" import { GRAPH_LABEL_COLOURS, pickShortLabel, type GraphNodeLike } from "../index" // --------------------------------------------------------------------------- // Caption-coverage reconciliation gate (Task 1879, Part B). // // GRAPH_LABEL_COLOURS is one of three hand-synced per-label registries (colour, // icon, caption). A label given a colour but no caption decision renders as its // bare type on the canvas — a silent failure that only a human reading the graph // catches. This fixture is the third registry made explicit: every styled label // must either resolve to a human caption or be declared an intentional // bare-type ("fallback grey") child/annotation label. Adding a colour key // without a fixture entry fails the build. // --------------------------------------------------------------------------- const node = (label: string, properties: Record): GraphNodeLike => ({ id: "n", labels: [label], properties, }) type Coverage = { node: GraphNodeLike; expected: string } | { fallbackGrey: true } const CAPTION_COVERAGE: Record = { // ── Business identity — captioned via the generic `name`/`title` fallback ── LocalBusiness: { node: node("LocalBusiness", { name: "Ruby Tech" }), expected: "Ruby Tech" }, Service: { node: node("Service", { name: "Boiler service" }), expected: "Boiler service" }, Organization: { node: node("Organization", { name: "BuildCo" }), expected: "BuildCo" }, Customer: { node: node("Customer", { name: "Acme Ltd" }), expected: "Acme Ltd" }, Site: { node: node("Site", { name: "North Depot" }), expected: "North Depot" }, Asset: { node: node("Asset", { name: "Chiller 1" }), expected: "Chiller 1" }, Part: { node: node("Part", { name: "Filter" }), expected: "Filter" }, PpmContract: { node: node("PpmContract", { name: "PPM 2024" }), expected: "PPM 2024" }, DigitalDocument: { node: node("DigitalDocument", { name: "Spec.pdf" }), expected: "Spec.pdf" }, Question: { node: node("Question", { name: "What time?" }), expected: "What time?" }, FAQPage: { node: node("FAQPage", { name: "FAQ" }), expected: "FAQ" }, DefinedTerm: { node: node("DefinedTerm", { name: "SLA" }), expected: "SLA" }, Task: { node: node("Task", { name: "Call client" }), expected: "Call client" }, Event: { node: node("Event", { name: "Kickoff" }), expected: "Kickoff" }, ImageObject: { node: node("ImageObject", { name: "hero.jpg" }), expected: "hero.jpg" }, CreativeWork: { node: node("CreativeWork", { title: "Summary" }), expected: "Summary" }, ConversationArchive: { node: node("ConversationArchive", { title: "Re: quote" }), expected: "Re: quote" }, Objective: { node: node("Objective", { title: "Grow revenue" }), expected: "Grow revenue" }, KeyResult: { node: node("KeyResult", { title: "10 new clients" }), expected: "10 new clients" }, Decision: { node: node("Decision", { title: "Adopt X" }), expected: "Adopt X" }, Source: { node: node("Source", { title: "Market report" }), expected: "Market report" }, Note: { node: node("Note", { text: "Called back" }), expected: "Called back" }, KnowledgeDocument: { node: node("KnowledgeDocument", { name: "CV.pdf" }), expected: "CV.pdf" }, Project: { node: node("Project", { name: "Rebrand" }), expected: "Rebrand" }, Workflow: { node: node("Workflow", { name: "Viewing follow-up" }), expected: "Viewing follow-up" }, // ── Existing per-label branches ── ToolCall: { node: node("ToolCall", { toolName: "graph-search" }), expected: "graph-search" }, WorkflowRun: { node: node("WorkflowRun", { startedAt: "2024-01-15T09:30:00Z" }), expected: "2024-01-15 09:30" }, WorkflowStep: { node: node("WorkflowStep", { label: "send email" }), expected: "send email" }, Person: { node: node("Person", { givenName: "Ada", familyName: "Lovelace" }), expected: "Ada Lovelace" }, Agent: { node: node("Agent", { displayName: "Listing Curator" }), expected: "Listing Curator" }, Job: { node: node("Job", { client: "Acme", jobId: "42" }), expected: "Acme #42" }, Quote: { node: node("Quote", { ref: "Q-1001" }), expected: "Q-1001" }, InboundInvoice: { node: node("InboundInvoice", { supplier: "BuildCo", confirmationNumber: "INV-9" }), expected: "BuildCo #INV-9" }, WhatsAppGroup: { node: node("WhatsAppGroup", { clientName: "Smith Job" }), expected: "Smith Job" }, CashEntry: { node: node("CashEntry", { description: "Fuel" }), expected: "Fuel" }, Message: { node: node("Message", { role: "user", createdAt: "2024-01-15T09:30:45Z" }), expected: "user 09:30:45" }, Conversation: { node: node("Conversation", { sessionId: "abcdef1234567890" }), expected: "Conv abcdef12" }, // ── New branches (Part A) — top-level operator-entry labels that previously // fell through to their bare type ── Property: { node: node("Property", { slug: "x", address: "18 Rowney Gardens" }), expected: "18 Rowney Gardens" }, Listing: { node: node("Listing", { slug: "x", displayName: "Griffin House" }), expected: "Griffin House" }, Viewing: { node: node("Viewing", { date: "2024-02-01" }), expected: "2024-02-01" }, Offer: { node: node("Offer", { status: "accepted", price: 250000 }), expected: "accepted £250000" }, Visit: { node: node("Visit", { purpose: "Annual service", status: "scheduled" }), expected: "Annual service" }, PurchaseOrder: { node: node("PurchaseOrder", { poNumber: "PO-77" }), expected: "PO-77" }, Invoice: { node: node("Invoice", { confirmationNumber: "INV-100" }), expected: "INV-100" }, Risk: { node: node("Risk", { statement: "Budget overrun" }), expected: "Budget overrun" }, Finding: { node: node("Finding", { statement: "Leak in unit 3" }), expected: "Leak in unit 3" }, Hypothesis: { node: node("Hypothesis", { statement: "Price too high" }), expected: "Price too high" }, // ── Intentional bare-type labels — specification, child, or platform-plumbing // labels that are not first-class operator-entry entities on the canvas ── PriceSpecification: { fallbackGrey: true }, OpeningHoursSpecification: { fallbackGrey: true }, Section: { fallbackGrey: true }, Chunk: { fallbackGrey: true }, Review: { fallbackGrey: true }, Preference: { fallbackGrey: true }, UserProfile: { fallbackGrey: true }, AdminUser: { fallbackGrey: true }, AccessGrant: { fallbackGrey: true }, StepResult: { fallbackGrey: true }, Email: { fallbackGrey: true }, // retired label (moved to ConversationArchive{source:'email'}) EmailAccount: { fallbackGrey: true }, UserMessage: { fallbackGrey: true }, AssistantMessage: { fallbackGrey: true }, AdminConversation: { fallbackGrey: true }, PublicConversation: { fallbackGrey: true }, // ── Task 1965: the 60 labels that gained a colour + glyph. Each now needs a // caption decision. Thirteen carry an identity property in the generic // fallback set (name/title/summary/subject/text) and caption to it with no // resolver change; the rest have no such property and render their bare // type as the text caption (they still get the coloured glyph disc — icon // and text caption are independent channels). Adding a per-label caption // branch for the bare-type ones is Task 1879's resolver scope. ── Content: { node: node("Content", { title: "Launch reel" }), expected: "Launch reel" }, Contact: { node: node("Contact", { name: "Jo Bloggs" }), expected: "Jo Bloggs" }, Lead: { node: node("Lead", { subject: "Roof enquiry" }), expected: "Roof enquiry" }, Position: { node: node("Position", { title: "Site Manager" }), expected: "Site Manager" }, Room: { node: node("Room", { name: "Sea View Suite" }), expected: "Sea View Suite" }, StoreLocation: { node: node("StoreLocation", { name: "High Street" }), expected: "High Street" }, Menu: { node: node("Menu", { name: "Winter Menu" }), expected: "Winter Menu" }, MenuItem: { node: node("MenuItem", { name: "Ribeye" }), expected: "Ribeye" }, Product: { node: node("Product", { name: "Widget A" }), expected: "Widget A" }, Milestone: { node: node("Milestone", { name: "Foundations" }), expected: "Foundations" }, Deliverable: { node: node("Deliverable", { name: "Wireframes" }), expected: "Wireframes" }, Case: { node: node("Case", { title: "Boiler warranty" }), expected: "Boiler warranty" }, TimelineEvent: { node: node("TimelineEvent", { summary: "Contract signed" }), expected: "Contract signed" }, InvoiceLine: { fallbackGrey: true }, InvoicePayment: { fallbackGrey: true }, QuoteLine: { fallbackGrey: true }, PurchaseOrderLine: { fallbackGrey: true }, JobCost: { fallbackGrey: true }, Credit: { fallbackGrey: true }, Retainer: { fallbackGrey: true }, Sale: { fallbackGrey: true }, Valuation: { fallbackGrey: true }, Order: { fallbackGrey: true }, DiningOrder: { fallbackGrey: true }, Page: { fallbackGrey: true }, Report: { fallbackGrey: true }, Post: { fallbackGrey: true }, SocialPost: { fallbackGrey: true }, VariationNote: { fallbackGrey: true }, Audience: { fallbackGrey: true }, PostalAddress: { fallbackGrey: true }, Vehicle: { fallbackGrey: true }, Route: { fallbackGrey: true }, Stop: { fallbackGrey: true }, Shipment: { fallbackGrey: true }, Reservation: { fallbackGrey: true }, TableReservation: { fallbackGrey: true }, StockMovement: { fallbackGrey: true }, TimeEntry: { fallbackGrey: true }, Engagement: { fallbackGrey: true }, Meeting: { fallbackGrey: true }, Idea: { fallbackGrey: true }, Campaign: { fallbackGrey: true }, BrandDeal: { fallbackGrey: true }, Recommendation: { fallbackGrey: true }, Session: { fallbackGrey: true }, PageView: { fallbackGrey: true }, Click: { fallbackGrey: true }, ScrollMilestone: { fallbackGrey: true }, AnonVisitor: { fallbackGrey: true }, AEOAudit: { fallbackGrey: true }, WhatsAppConversation: { fallbackGrey: true }, WhatsAppMessage: { fallbackGrey: true }, CloudflareHostname: { fallbackGrey: true }, CloudflareTunnel: { fallbackGrey: true }, Chain: { fallbackGrey: true }, ChainLink: { fallbackGrey: true }, FileArtifact: { fallbackGrey: true }, VoiceProfile: { fallbackGrey: true }, VoiceEdit: { fallbackGrey: true }, } describe("caption coverage reconciles against GRAPH_LABEL_COLOURS", () => { it("declares exactly the styled labels — no gaps, no strays", () => { const styled = Object.keys(GRAPH_LABEL_COLOURS).sort() const declared = Object.keys(CAPTION_COVERAGE).sort() const missing = styled.filter((l) => !(l in CAPTION_COVERAGE)) const stray = declared.filter((l) => !(l in GRAPH_LABEL_COLOURS)) expect(missing, "styled labels with no caption decision").toEqual([]) expect(stray, "caption entries for unstyled labels").toEqual([]) }) for (const [label, cov] of Object.entries(CAPTION_COVERAGE)) { if ("fallbackGrey" in cov) continue it(`captions ${label} to a non-type label`, () => { const got = pickShortLabel(cov.node) expect(got).toBe(cov.expected) expect(got).not.toBe(label) }) } })