Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/components/layout/workflow-card-meta.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,7 +16,8 @@ export const WORKFLOW_TYPE_META: Record<RadarSourceType | JanitorSourceType, Wor
deduplication: { icon: Layers, label: "Deduplication", tone: "violet" },
content_review: { icon: ShieldCheck, label: "Content Review", tone: "emerald" },
topic_review: { icon: Sparkles, label: "Topic Review", tone: "fuchsia" },
orphan_node: { icon: Layers, label: "Orphan Node", tone: "slate" },
orphan_node: { icon: Layers, label: "Orphan Node", tone: "slate" },
scratchpad_review: { icon: FileQuestion, label: "Scratchpad Review", tone: "teal" },
}

// Static tone → Tailwind class mapping to avoid Tailwind purge issues
Expand All @@ -29,6 +30,7 @@ export const TONE_CLASSES: Record<string, string> = {
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
Expand Down
36 changes: 36 additions & 0 deletions src/lib/__tests__/janitor-settings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<JanitorSettings open={true} />)

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(<JanitorSettings open={true} />)

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()
})
})
32 changes: 18 additions & 14 deletions src/lib/__tests__/reviews.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ─────────────────────────────────────
Expand Down Expand Up @@ -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 ─────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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()
})
Expand Down Expand Up @@ -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()
Expand All @@ -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()
})

Expand All @@ -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)
Expand All @@ -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()
})

Expand All @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion src/lib/graph-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
47 changes: 47 additions & 0 deletions src/lib/mock-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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(),
},
]
Loading