diff --git a/src/components/layout/workflow-card-meta.ts b/src/components/layout/workflow-card-meta.ts index c7618cc..8e5c271 100644 --- a/src/components/layout/workflow-card-meta.ts +++ b/src/components/layout/workflow-card-meta.ts @@ -1,6 +1,6 @@ import type { RadarSourceType, JanitorSourceType } from "@/lib/graph-api" import type { LucideIcon } from "lucide-react" -import { AtSign, Video, Rss, Hash, Sparkles, Layers, ShieldCheck } from "lucide-react" +import { AtSign, Video, Rss, Hash, Sparkles, Layers, ShieldCheck, FileQuestion } from "lucide-react" export interface WorkflowTypeMeta { icon: LucideIcon @@ -16,7 +16,8 @@ export const WORKFLOW_TYPE_META: Record = { emerald: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", fuchsia: "bg-fuchsia-500/10 text-fuchsia-400 border-fuchsia-500/20", slate: "bg-slate-500/10 text-slate-400 border-slate-500/20", + teal: "bg-teal-500/10 text-teal-400 border-teal-500/20", } // Returns item.label if set, otherwise the human-friendly label from meta diff --git a/src/lib/__tests__/janitor-settings.test.tsx b/src/lib/__tests__/janitor-settings.test.tsx index e9ec551..ab13834 100644 --- a/src/lib/__tests__/janitor-settings.test.tsx +++ b/src/lib/__tests__/janitor-settings.test.tsx @@ -358,4 +358,40 @@ describe("JanitorSettings", () => { expect(await screen.findByText("Orphan node cleanup")).toBeInTheDocument() }) + + it("renders scratchpad_review label from config.label", async () => { + const { getCronConfig, getCronRuns } = await import("@/lib/graph-api") + vi.mocked(getCronConfig).mockResolvedValue({ + configs: [ + { ...mockJanitorConfig, ref_id: "rc-scratchpad-review", source_type: "scratchpad_review" as const, label: "Scratchpad review" }, + ], + }) + vi.mocked(getCronRuns).mockResolvedValue({ runs: [] }) + + render() + + expect(await screen.findByText("Scratchpad review")).toBeInTheDocument() + }) + + it("renders all five janitor types including scratchpad_review", async () => { + const { getCronConfig, getCronRuns } = await import("@/lib/graph-api") + vi.mocked(getCronConfig).mockResolvedValue({ + configs: [ + { ...mockJanitorConfig, source_type: "deduplication", label: "Deduplication" }, + { ...mockJanitorConfig, ref_id: "rc-content-review", source_type: "content_review" as const, label: "Content review" }, + { ...mockJanitorConfig, ref_id: "rc-topic-review", source_type: "topic_review" as const, label: "Topic review" }, + { ...mockJanitorConfig, ref_id: "rc-orphan-node", source_type: "orphan_node" as const, label: "Orphan node cleanup" }, + { ...mockJanitorConfig, ref_id: "rc-scratchpad-review", source_type: "scratchpad_review" as const, label: "Scratchpad review" }, + ], + }) + vi.mocked(getCronRuns).mockResolvedValue({ runs: [] }) + + render() + + expect(await screen.findByText("Deduplication")).toBeInTheDocument() + expect(await screen.findByText("Content review")).toBeInTheDocument() + expect(await screen.findByText("Topic review")).toBeInTheDocument() + expect(await screen.findByText("Orphan node cleanup")).toBeInTheDocument() + expect(await screen.findByText("Scratchpad review")).toBeInTheDocument() + }) }) diff --git a/src/lib/__tests__/reviews.test.tsx b/src/lib/__tests__/reviews.test.tsx index c8db770..06c62f2 100644 --- a/src/lib/__tests__/reviews.test.tsx +++ b/src/lib/__tests__/reviews.test.tsx @@ -399,10 +399,10 @@ describe("ReviewRow", () => { onRefresh={noop} /> ) - // soft_delete action uses rowLabel: "Hide [displayName]" — the subject node - // has name "Mock Episode", so the row label becomes "Hide Mock Episode". + // soft_delete rowLabel renders just the subject displayName (no verb prefix). + // The "Hide" verb appears only on the approve button, not in the row label. expect(container.querySelector("svg")).toBeTruthy() - expect(getByText(/Hide Mock Episode/)).toBeTruthy() + expect(getByText("Mock Episode")).toBeTruthy() }) // ── add_source / new_source_candidate ───────────────────────────────────── @@ -560,10 +560,10 @@ describe("ReviewRow", () => { onRefresh={noop} /> ) - // soft_delete action uses rowLabel: "Hide [displayName]" — the subject node - // has name "Orphaned Topic", so the row label becomes "Hide Orphaned Topic". + // soft_delete rowLabel renders just the subject displayName (no verb prefix). + // The "Hide" verb appears only on the approve button, not in the row label. expect(container.querySelector("svg")).toBeTruthy() - expect(getByText(/Hide Orphaned Topic/)).toBeTruthy() + expect(getByText("Orphaned Topic")).toBeTruthy() }) // ── add_node ───────────────────────────────────────────────────────────── @@ -681,8 +681,8 @@ describe("ReviewRow", () => { onRefresh={noop} /> ) - // Compact row shows rowLabel "Add new edge" (not display_label) when subject_nodes is non-empty - await user.click(getByText("Add new edge")) + // Compact row shows rowLabel "New edge" (not display_label) when subject_nodes is non-empty + await user.click(getByText("New edge")) expect(getByText("Proposed Edge")).toBeTruthy() expect(getByText("AUTHORED_BY")).toBeTruthy() }) @@ -752,8 +752,8 @@ describe("ReviewRow", () => { onRefresh={noop} /> ) - // Compact row uses rowLabel: "Edit Bitcoin Explained (Clip)" from subject node name - await user.click(getByText(/Edit Bitcoin Explained/)) + // Compact row uses rowLabel from subject node name (no verb prefix) + await user.click(getByText("Bitcoin Explained (Clip)")) expect(getByText("Type Change")).toBeTruthy() expect(getByText("Clip")).toBeTruthy() expect(getByText("Episode")).toBeTruthy() @@ -774,7 +774,8 @@ describe("ReviewRow", () => { onRefresh={noop} /> ) - await user.click(getByText("Edit Some Clip")) + // edit_node rowLabel renders just the subject displayName (no verb prefix) + await user.click(getByText("Some Clip")) expect(getByText("Node Being Edited")).toBeTruthy() }) @@ -800,7 +801,8 @@ describe("ReviewRow", () => { onRefresh={noop} /> ) - await user.click(getByText("Edit Alice")) + // edit_node rowLabel renders just the subject displayName (no verb prefix) + await user.click(getByText("Alice")) const img = container.querySelector("img[alt='image preview']") as HTMLImageElement expect(img).toBeTruthy() expect(img.src).toBe(imgUrl) @@ -825,7 +827,8 @@ describe("ReviewRow", () => { onRefresh={noop} /> ) - await user.click(getByText("Edit Bob")) + // edit_node rowLabel renders just the subject displayName (no verb prefix) + await user.click(getByText("Bob")) expect(getByText("Bob Updated")).toBeTruthy() }) @@ -849,7 +852,8 @@ describe("ReviewRow", () => { onRefresh={noop} /> ) - await user.click(getByText("Edit Charlie")) + // edit_node rowLabel renders just the subject displayName (no verb prefix) + await user.click(getByText("Charlie")) // Trigger the error handler const img = container.querySelector("img[alt='image preview']") as HTMLImageElement expect(img).toBeTruthy() diff --git a/src/lib/graph-api.ts b/src/lib/graph-api.ts index ae6ccc2..b1c7023 100644 --- a/src/lib/graph-api.ts +++ b/src/lib/graph-api.ts @@ -399,7 +399,7 @@ export const RADAR_SOURCE_TYPES: RadarSourceType[] = [ "topic", ] -export type JanitorSourceType = "deduplication" | "content_review" | "topic_review" | "orphan_node" +export type JanitorSourceType = "deduplication" | "content_review" | "topic_review" | "orphan_node" | "scratchpad_review" export type CronKind = "source" | "janitor" diff --git a/src/lib/mock-data.ts b/src/lib/mock-data.ts index a367ab3..bef557e 100644 --- a/src/lib/mock-data.ts +++ b/src/lib/mock-data.ts @@ -778,6 +778,7 @@ export const MOCK_CRON_CONFIGS = [ { ...baseCronConfig, ref_id: "rc-content-review", source_type: "content_review" as const, kind: "janitor" as const, enabled: false, cadence: "0 * * * *", label: "Content review" }, { ...baseCronConfig, ref_id: "rc-topic-review", source_type: "topic_review" as const, kind: "janitor" as const, enabled: false, cadence: "0 * * * *", label: "Topic review" }, { ...baseCronConfig, ref_id: "rc-orphan-node", source_type: "orphan_node" as const, kind: "janitor" as const, enabled: false, cadence: "0 * * * *", label: "Orphan node cleanup" }, + { ...baseCronConfig, ref_id: "rc-scratchpad-review", source_type: "scratchpad_review" as const, kind: "janitor" as const, enabled: false, cadence: "0 * * * *", label: "Scratchpad review" }, ] /** @deprecated Use MOCK_CRON_CONFIGS */ @@ -1358,4 +1359,50 @@ export const MOCK_REVIEWS: Review[] = [ icon: "network", created_at: new Date(Date.now()).toISOString(), }, + { + ref_id: "mock-scratchpad-entry-1", + type: "scratchpad_entry", + status: "pending", + priority: 1, + rationale: "Ingested node with intended_type 'LegalContract' was rejected: no matching schema type exists. rejection_reason: unknown_type. rejection_detail: Type 'LegalContract' is not registered in the current ontology. Confidence: High.", + subject_ids: ["scratchpad-node-ref-001"], + subject_nodes: [ + { + ref_id: "scratchpad-node-ref-001", + node_type: "ScratchpadEntry", + properties: { name: "LegalContract (scratchpad)", intended_type: "LegalContract" }, + }, + ], + action_name: "add_schema_node_type", + action_payload: { type: "LegalContract", parent: "Thing" }, + fingerprint: "fp-mock-scratchpad-entry-1", + display_label: "Scratchpad Entry", + accent: "yellow", + action_verb: "Create", + icon: "file-question", + created_at: new Date(Date.now() - 3_600_000).toISOString(), + }, + { + ref_id: "mock-scratchpad-entry-2", + type: "scratchpad_entry", + status: "pending", + priority: 0, + rationale: "Ingested node with intended_type 'ResearchInstitution' was rejected: no matching schema type exists. rejection_reason: unknown_type. rejection_detail: Type 'ResearchInstitution' is not registered in the current ontology. Confidence: Medium.", + subject_ids: ["scratchpad-node-ref-002"], + subject_nodes: [ + { + ref_id: "scratchpad-node-ref-002", + node_type: "ScratchpadEntry", + properties: { name: "ResearchInstitution (scratchpad)", intended_type: "ResearchInstitution" }, + }, + ], + action_name: "add_schema_node_type", + action_payload: { type: "ResearchInstitution", parent: "Thing" }, + fingerprint: "fp-mock-scratchpad-entry-2", + display_label: "Scratchpad Entry", + accent: "yellow", + action_verb: "Create", + icon: "file-question", + created_at: new Date(Date.now() - 7_200_000).toISOString(), + }, ]