@@ -10,6 +10,8 @@ import type {
1010 SourceDiversitySnapshot ,
1111 TopicCentroidObservabilitySummary ,
1212 TopicCentroidSnapshot ,
13+ TrendTaskRun ,
14+ TrendTaskRunObservabilitySummary ,
1315} from "@/lib/types"
1416
1517const {
@@ -18,17 +20,21 @@ const {
1820 getProjectSourceDiversitySnapshotsMock,
1921 getProjectSourceDiversitySummaryMock,
2022 getProjectSourceConfigsMock,
23+ getProjectTrendTaskRunsMock,
2124 getProjectTopicCentroidSnapshotsMock,
2225 getProjectTopicCentroidSummaryMock,
26+ getProjectTrendTaskRunSummaryMock,
2327 selectProjectMock,
2428} = vi . hoisted ( ( ) => ( {
2529 getProjectIngestionRunsMock : vi . fn ( ) ,
2630 getProjectsMock : vi . fn ( ) ,
2731 getProjectSourceDiversitySnapshotsMock : vi . fn ( ) ,
2832 getProjectSourceDiversitySummaryMock : vi . fn ( ) ,
2933 getProjectSourceConfigsMock : vi . fn ( ) ,
34+ getProjectTrendTaskRunsMock : vi . fn ( ) ,
3035 getProjectTopicCentroidSnapshotsMock : vi . fn ( ) ,
3136 getProjectTopicCentroidSummaryMock : vi . fn ( ) ,
37+ getProjectTrendTaskRunSummaryMock : vi . fn ( ) ,
3238 selectProjectMock : vi . fn ( ) ,
3339} ) )
3440
@@ -70,8 +76,10 @@ vi.mock("@/lib/api", () => ({
7076 getProjectSourceDiversitySnapshots : getProjectSourceDiversitySnapshotsMock ,
7177 getProjectSourceDiversitySummary : getProjectSourceDiversitySummaryMock ,
7278 getProjectSourceConfigs : getProjectSourceConfigsMock ,
79+ getProjectTrendTaskRuns : getProjectTrendTaskRunsMock ,
7380 getProjectTopicCentroidSnapshots : getProjectTopicCentroidSnapshotsMock ,
7481 getProjectTopicCentroidSummary : getProjectTopicCentroidSummaryMock ,
82+ getProjectTrendTaskRunSummary : getProjectTrendTaskRunSummaryMock ,
7583} ) )
7684
7785vi . mock ( "@/lib/view-helpers" , async ( ) => {
@@ -200,6 +208,42 @@ function createSourceDiversitySummary(
200208 }
201209}
202210
211+ function createTrendTaskRun (
212+ overrides : Partial < TrendTaskRun > = { } ,
213+ ) : TrendTaskRun {
214+ return {
215+ id : 41 ,
216+ project : 1 ,
217+ task_name : "recompute_topic_centroid" ,
218+ task_run_id : "95ae5b14-5d7d-498e-9adc-1dbaab4dd4b8" ,
219+ status : "completed" ,
220+ started_at : "2026-04-28T08:00:00Z" ,
221+ finished_at : "2026-04-28T08:00:01Z" ,
222+ latency_ms : 523 ,
223+ error_message : "" ,
224+ summary : {
225+ project_id : 1 ,
226+ feedback_count : 12 ,
227+ upvote_count : 10 ,
228+ downvote_count : 2 ,
229+ centroid_active : true ,
230+ } ,
231+ ...overrides ,
232+ }
233+ }
234+
235+ function createTrendTaskRunSummary (
236+ overrides : Partial < TrendTaskRunObservabilitySummary > = { } ,
237+ ) : TrendTaskRunObservabilitySummary {
238+ return {
239+ project : 1 ,
240+ run_count : 0 ,
241+ failed_run_count : 0 ,
242+ latest_runs : [ ] ,
243+ ...overrides ,
244+ }
245+ }
246+
203247async function loadHealthPageModule ( ) {
204248 return import ( "./page" )
205249}
@@ -288,8 +332,10 @@ describe("HealthPage", () => {
288332 getProjectIngestionRunsMock . mockReset ( )
289333 getProjectSourceDiversitySnapshotsMock . mockReset ( )
290334 getProjectSourceDiversitySummaryMock . mockReset ( )
335+ getProjectTrendTaskRunsMock . mockReset ( )
291336 getProjectTopicCentroidSnapshotsMock . mockReset ( )
292337 getProjectTopicCentroidSummaryMock . mockReset ( )
338+ getProjectTrendTaskRunSummaryMock . mockReset ( )
293339 selectProjectMock . mockReset ( )
294340
295341 getProjectsMock . mockResolvedValue ( [ defaultProject ] )
@@ -299,10 +345,14 @@ describe("HealthPage", () => {
299345 getProjectSourceDiversitySummaryMock . mockResolvedValue (
300346 createSourceDiversitySummary ( ) ,
301347 )
348+ getProjectTrendTaskRunsMock . mockResolvedValue ( [ ] )
302349 getProjectTopicCentroidSnapshotsMock . mockResolvedValue ( [ ] )
303350 getProjectTopicCentroidSummaryMock . mockResolvedValue (
304351 createTopicCentroidSummary ( ) ,
305352 )
353+ getProjectTrendTaskRunSummaryMock . mockResolvedValue (
354+ createTrendTaskRunSummary ( ) ,
355+ )
306356 selectProjectMock . mockImplementation ( ( projects : Project [ ] ) => {
307357 return projects [ 0 ] ?? null
308358 } )
@@ -325,8 +375,10 @@ describe("HealthPage", () => {
325375 expect ( getProjectIngestionRunsMock ) . not . toHaveBeenCalled ( )
326376 expect ( getProjectSourceDiversitySnapshotsMock ) . not . toHaveBeenCalled ( )
327377 expect ( getProjectSourceDiversitySummaryMock ) . not . toHaveBeenCalled ( )
378+ expect ( getProjectTrendTaskRunsMock ) . not . toHaveBeenCalled ( )
328379 expect ( getProjectTopicCentroidSnapshotsMock ) . not . toHaveBeenCalled ( )
329380 expect ( getProjectTopicCentroidSummaryMock ) . not . toHaveBeenCalled ( )
381+ expect ( getProjectTrendTaskRunSummaryMock ) . not . toHaveBeenCalled ( )
330382 } )
331383
332384 it ( "renders an empty source-configurations row when the project has no sources" , async ( ) => {
@@ -341,12 +393,20 @@ describe("HealthPage", () => {
341393 expect (
342394 screen . getByText ( "No source configurations exist for this project yet." ) ,
343395 ) . toBeInTheDocument ( )
396+ expect (
397+ screen . getByText ( "No trend pipeline runs have been persisted for this project yet." ) ,
398+ ) . toBeInTheDocument ( )
399+ expect (
400+ screen . getByText ( "No trend task run history exists for this project yet." ) ,
401+ ) . toBeInTheDocument ( )
344402 expect ( getProjectSourceConfigsMock ) . toHaveBeenCalledWith ( 1 )
345403 expect ( getProjectIngestionRunsMock ) . toHaveBeenCalledWith ( 1 )
346404 expect ( getProjectSourceDiversitySnapshotsMock ) . toHaveBeenCalledWith ( 1 )
347405 expect ( getProjectSourceDiversitySummaryMock ) . toHaveBeenCalledWith ( 1 )
406+ expect ( getProjectTrendTaskRunsMock ) . toHaveBeenCalledWith ( 1 )
348407 expect ( getProjectTopicCentroidSnapshotsMock ) . toHaveBeenCalledWith ( 1 )
349408 expect ( getProjectTopicCentroidSummaryMock ) . toHaveBeenCalledWith ( 1 )
409+ expect ( getProjectTrendTaskRunSummaryMock ) . toHaveBeenCalledWith ( 1 )
350410 } )
351411
352412 it ( "shows a no-runs message for sources without ingestion history" , async ( ) => {
@@ -519,4 +579,65 @@ describe("HealthPage", () => {
519579 expect ( screen . getByText ( "View raw breakdown JSON" ) ) . toBeInTheDocument ( )
520580 expect ( screen . getByLabelText ( "Source diversity trend" ) ) . toBeInTheDocument ( )
521581 } )
582+
583+ it ( "renders the latest trend pipeline task runs" , async ( ) => {
584+ getProjectTrendTaskRunsMock . mockResolvedValue ( [
585+ createTrendTaskRun ( {
586+ id : 43 ,
587+ task_name : "generate_theme_suggestions" ,
588+ started_at : "2026-04-28T08:20:00Z" ,
589+ finished_at : "2026-04-28T08:20:01Z" ,
590+ latency_ms : 1480 ,
591+ status : "failed" ,
592+ error_message : "OpenRouter timeout" ,
593+ summary : { project_id : 1 , created : 0 , updated : 0 , skipped : 2 } ,
594+ } ) ,
595+ createTrendTaskRun ( {
596+ id : 41 ,
597+ started_at : "2026-04-28T08:00:00Z" ,
598+ finished_at : "2026-04-28T08:00:01Z" ,
599+ } ) ,
600+ ] )
601+ getProjectTrendTaskRunSummaryMock . mockResolvedValue (
602+ createTrendTaskRunSummary ( {
603+ run_count : 8 ,
604+ failed_run_count : 1 ,
605+ latest_runs : [
606+ createTrendTaskRun ( ) ,
607+ createTrendTaskRun ( {
608+ id : 42 ,
609+ task_name : "generate_theme_suggestions" ,
610+ status : "failed" ,
611+ latency_ms : 1480 ,
612+ error_message : "OpenRouter timeout" ,
613+ summary : { project_id : 1 , created : 0 , updated : 0 , skipped : 2 } ,
614+ } ) ,
615+ ] ,
616+ } ) ,
617+ )
618+
619+ await renderHealthPage ( )
620+
621+ expect (
622+ screen . getByRole ( "heading" , { level : 2 , name : "Trend pipeline runs" } ) ,
623+ ) . toBeInTheDocument ( )
624+ expect ( screen . getAllByText ( "Topic centroid" ) . length ) . toBeGreaterThan ( 0 )
625+ expect ( screen . getAllByText ( "Theme suggestions" ) . length ) . toBeGreaterThan ( 0 )
626+ expect (
627+ screen . getAllByText ( "feedback 12 • upvotes 10 • downvotes 2" ) . length ,
628+ ) . toBeGreaterThan ( 0 )
629+ expect ( screen . getAllByText ( "OpenRouter timeout" ) . length ) . toBeGreaterThan ( 0 )
630+ expect ( screen . getAllByText ( "1.5s" ) . length ) . toBeGreaterThan ( 0 )
631+ expect ( screen . getByText ( "8" ) ) . toBeInTheDocument ( )
632+ expect (
633+ screen . getByRole ( "link" , { name : "Open trend task run history" } ) ,
634+ ) . toHaveAttribute (
635+ "href" ,
636+ "/admin/health?project=1#trend-task-run-history" ,
637+ )
638+ expect (
639+ screen . getByRole ( "heading" , { level : 2 , name : "Trend task run history" } ) ,
640+ ) . toBeInTheDocument ( )
641+ expect ( screen . getByText ( "Showing 2 of 8 runs" ) ) . toBeInTheDocument ( )
642+ } )
522643} )
0 commit comments