11import { assertEquals } from "@std/assert" ;
22import { InsightsPageController } from "./insights.tsx" ;
3- import type { GitHubApiClient , WorkflowRun } from "@/lib/github-api-client.ts" ;
3+ import type {
4+ GitHubApiClient ,
5+ WorkflowJob ,
6+ WorkflowRun ,
7+ } from "@/lib/github-api-client.ts" ;
48import type {
59 JobTestResults ,
610 TestResultsDownloader ,
@@ -12,19 +16,29 @@ interface RunsWithCount {
1216 runs : WorkflowRun [ ] ;
1317}
1418
15- class MockGitHubApiClient implements Pick < GitHubApiClient , "listWorkflowRuns" > {
19+ class MockGitHubApiClient
20+ implements Pick < GitHubApiClient , "listWorkflowRuns" | "listJobs" > {
1621 #runs: RunsWithCount = { totalCount : 0 , runs : [ ] } ;
22+ #jobs: Map < number , WorkflowJob [ ] > = new Map ( ) ;
1723
1824 mockRuns ( runs : RunsWithCount ) {
1925 this . #runs = runs ;
2026 }
2127
28+ mockJobs ( runId : number , jobs : WorkflowJob [ ] ) {
29+ this . #jobs. set ( runId , jobs ) ;
30+ }
31+
2232 listWorkflowRuns (
2333 _perPage ?: number ,
2434 _page ?: number ,
2535 ) {
2636 return Promise . resolve ( this . #runs) ;
2737 }
38+
39+ listJobs ( runId : number ) {
40+ return Promise . resolve ( this . #jobs. get ( runId ) || [ ] ) ;
41+ }
2842}
2943
3044class MockTestResultsDownloader implements TestResultsDownloader {
@@ -77,6 +91,8 @@ Deno.test("filters main branch completed CI runs", async () => {
7791 ] ;
7892
7993 mockGithub . mockRuns ( { totalCount : 5 , runs } ) ;
94+ mockGithub . mockJobs ( 1 , [ ] ) ;
95+ mockGithub . mockJobs ( 5 , [ ] ) ;
8096 mockDownloader . mockResults ( 1 , [ ] ) ;
8197 mockDownloader . mockResults ( 5 , [ ] ) ;
8298
@@ -106,6 +122,7 @@ Deno.test("limits to 20 main branch runs", async () => {
106122
107123 // Mock results for first 20 runs only
108124 for ( let i = 1 ; i <= 20 ; i ++ ) {
125+ mockGithub . mockJobs ( i , [ ] ) ;
109126 mockDownloader . mockResults ( i , [ ] ) ;
110127 }
111128
@@ -129,6 +146,8 @@ Deno.test("tracks flaky tests", async () => {
129146 ] ;
130147
131148 mockGithub . mockRuns ( { totalCount : 2 , runs } ) ;
149+ mockGithub . mockJobs ( 1 , [ ] ) ;
150+ mockGithub . mockJobs ( 2 , [ ] ) ;
132151
133152 mockDownloader . mockResults ( 1 , [
134153 {
@@ -534,6 +553,8 @@ Deno.test("tracks flaky job counts", async () => {
534553 ] ;
535554
536555 mockGithub . mockRuns ( { totalCount : 2 , runs } ) ;
556+ mockGithub . mockJobs ( 1 , [ ] ) ;
557+ mockGithub . mockJobs ( 2 , [ ] ) ;
537558
538559 mockDownloader . mockResults ( 1 , [
539560 {
@@ -624,3 +645,210 @@ Deno.test("tracks flaky job counts", async () => {
624645 assertEquals ( result . data . flakyJobs [ 2 ] . name , "windows-x64" ) ;
625646 assertEquals ( result . data . flakyJobs [ 2 ] . count , 3 ) ;
626647} ) ;
648+
649+ Deno . test ( "tracks job performance metrics" , async ( ) => {
650+ const mockGithub = new MockGitHubApiClient ( ) ;
651+ const mockDownloader = new MockTestResultsDownloader ( ) ;
652+
653+ const runs = [
654+ createMockRun ( 1 , "CI" , "completed" , "main" ) ,
655+ createMockRun ( 2 , "CI" , "completed" , "main" ) ,
656+ ] ;
657+
658+ mockGithub . mockRuns ( { totalCount : 2 , runs } ) ;
659+
660+ // Mock jobs with timing data for run 1
661+ mockGithub . mockJobs ( 1 , [
662+ {
663+ id : 101 ,
664+ run_id : 1 ,
665+ name : "test-linux" ,
666+ status : "completed" ,
667+ conclusion : "success" ,
668+ started_at : "2024-01-01T10:00:00Z" ,
669+ completed_at : "2024-01-01T10:05:00Z" , // 5 minutes = 300 seconds
670+ } ,
671+ {
672+ id : 102 ,
673+ run_id : 1 ,
674+ name : "test-windows" ,
675+ status : "completed" ,
676+ conclusion : "success" ,
677+ started_at : "2024-01-01T10:00:00Z" ,
678+ completed_at : "2024-01-01T10:10:00Z" , // 10 minutes = 600 seconds
679+ } ,
680+ ] ) ;
681+
682+ // Mock jobs with timing data for run 2
683+ mockGithub . mockJobs ( 2 , [
684+ {
685+ id : 201 ,
686+ run_id : 2 ,
687+ name : "test-linux" ,
688+ status : "completed" ,
689+ conclusion : "success" ,
690+ started_at : "2024-01-02T10:00:00Z" ,
691+ completed_at : "2024-01-02T10:07:00Z" , // 7 minutes = 420 seconds
692+ } ,
693+ {
694+ id : 202 ,
695+ run_id : 2 ,
696+ name : "test-windows" ,
697+ status : "completed" ,
698+ conclusion : "success" ,
699+ started_at : "2024-01-02T10:00:00Z" ,
700+ completed_at : "2024-01-02T10:08:00Z" , // 8 minutes = 480 seconds
701+ } ,
702+ ] ) ;
703+
704+ mockDownloader . mockResults ( 1 , [ ] ) ;
705+ mockDownloader . mockResults ( 2 , [ ] ) ;
706+
707+ const controller = new InsightsPageController (
708+ new NullLogger ( ) ,
709+ mockGithub ,
710+ mockDownloader ,
711+ ) ;
712+ const result = await controller . get ( ) ;
713+
714+ // Verify jobPerformance is returned and sorted by average duration descending
715+ assertEquals ( result . data . jobPerformance . length , 2 ) ;
716+
717+ // test-windows: avg=(600+480)/2=540s, min=480s, max=600s
718+ assertEquals ( result . data . jobPerformance [ 0 ] . name , "test-windows" ) ;
719+ assertEquals ( result . data . jobPerformance [ 0 ] . avgDuration , 540 ) ;
720+ assertEquals ( result . data . jobPerformance [ 0 ] . minDuration , 480 ) ;
721+ assertEquals ( result . data . jobPerformance [ 0 ] . maxDuration , 600 ) ;
722+ assertEquals ( result . data . jobPerformance [ 0 ] . count , 2 ) ;
723+
724+ // test-linux: avg=(300+420)/2=360s, min=300s, max=420s
725+ assertEquals ( result . data . jobPerformance [ 1 ] . name , "test-linux" ) ;
726+ assertEquals ( result . data . jobPerformance [ 1 ] . avgDuration , 360 ) ;
727+ assertEquals ( result . data . jobPerformance [ 1 ] . minDuration , 300 ) ;
728+ assertEquals ( result . data . jobPerformance [ 1 ] . maxDuration , 420 ) ;
729+ assertEquals ( result . data . jobPerformance [ 1 ] . count , 2 ) ;
730+ } ) ;
731+
732+ Deno . test ( "tracks step performance metrics" , async ( ) => {
733+ const mockGithub = new MockGitHubApiClient ( ) ;
734+ const mockDownloader = new MockTestResultsDownloader ( ) ;
735+
736+ const runs = [
737+ createMockRun ( 1 , "CI" , "completed" , "main" ) ,
738+ createMockRun ( 2 , "CI" , "completed" , "main" ) ,
739+ ] ;
740+
741+ mockGithub . mockRuns ( { totalCount : 2 , runs } ) ;
742+
743+ // Mock jobs with step timing data for run 1
744+ mockGithub . mockJobs ( 1 , [
745+ {
746+ id : 101 ,
747+ run_id : 1 ,
748+ name : "test-job" ,
749+ status : "completed" ,
750+ conclusion : "success" ,
751+ started_at : "2024-01-01T10:00:00Z" ,
752+ completed_at : "2024-01-01T10:10:00Z" ,
753+ steps : [
754+ {
755+ name : "Checkout code" ,
756+ status : "completed" ,
757+ conclusion : "success" ,
758+ number : 1 ,
759+ started_at : "2024-01-01T10:00:00Z" ,
760+ completed_at : "2024-01-01T10:00:30Z" , // 30 seconds
761+ } ,
762+ {
763+ name : "Run tests" ,
764+ status : "completed" ,
765+ conclusion : "success" ,
766+ number : 2 ,
767+ started_at : "2024-01-01T10:00:30Z" ,
768+ completed_at : "2024-01-01T10:05:30Z" , // 5 minutes = 300 seconds
769+ } ,
770+ {
771+ name : "Upload results" ,
772+ status : "completed" ,
773+ conclusion : "success" ,
774+ number : 3 ,
775+ started_at : "2024-01-01T10:05:30Z" ,
776+ completed_at : "2024-01-01T10:06:00Z" , // 30 seconds
777+ } ,
778+ ] ,
779+ } ,
780+ ] ) ;
781+
782+ // Mock jobs with step timing data for run 2
783+ mockGithub . mockJobs ( 2 , [
784+ {
785+ id : 201 ,
786+ run_id : 2 ,
787+ name : "test-job" ,
788+ status : "completed" ,
789+ conclusion : "success" ,
790+ started_at : "2024-01-02T10:00:00Z" ,
791+ completed_at : "2024-01-02T10:08:00Z" ,
792+ steps : [
793+ {
794+ name : "Checkout code" ,
795+ status : "completed" ,
796+ conclusion : "success" ,
797+ number : 1 ,
798+ started_at : "2024-01-02T10:00:00Z" ,
799+ completed_at : "2024-01-02T10:00:20Z" , // 20 seconds
800+ } ,
801+ {
802+ name : "Run tests" ,
803+ status : "completed" ,
804+ conclusion : "success" ,
805+ number : 2 ,
806+ started_at : "2024-01-02T10:00:20Z" ,
807+ completed_at : "2024-01-02T10:04:20Z" , // 4 minutes = 240 seconds
808+ } ,
809+ {
810+ name : "Upload results" ,
811+ status : "completed" ,
812+ conclusion : "success" ,
813+ number : 3 ,
814+ started_at : "2024-01-02T10:04:20Z" ,
815+ completed_at : "2024-01-02T10:05:00Z" , // 40 seconds
816+ } ,
817+ ] ,
818+ } ,
819+ ] ) ;
820+
821+ mockDownloader . mockResults ( 1 , [ ] ) ;
822+ mockDownloader . mockResults ( 2 , [ ] ) ;
823+
824+ const controller = new InsightsPageController (
825+ new NullLogger ( ) ,
826+ mockGithub ,
827+ mockDownloader ,
828+ ) ;
829+ const result = await controller . get ( ) ;
830+
831+ // Verify stepPerformance is returned and sorted by average duration descending
832+ assertEquals ( result . data . stepPerformance . length , 3 ) ;
833+
834+ // Run tests: avg=(300+240)/2=270s, min=240s, max=300s
835+ assertEquals ( result . data . stepPerformance [ 0 ] . name , "Run tests" ) ;
836+ assertEquals ( result . data . stepPerformance [ 0 ] . avgDuration , 270 ) ;
837+ assertEquals ( result . data . stepPerformance [ 0 ] . minDuration , 240 ) ;
838+ assertEquals ( result . data . stepPerformance [ 0 ] . maxDuration , 300 ) ;
839+ assertEquals ( result . data . stepPerformance [ 0 ] . count , 2 ) ;
840+
841+ // Upload results: avg=(30+40)/2=35s, min=30s, max=40s
842+ assertEquals ( result . data . stepPerformance [ 1 ] . name , "Upload results" ) ;
843+ assertEquals ( result . data . stepPerformance [ 1 ] . avgDuration , 35 ) ;
844+ assertEquals ( result . data . stepPerformance [ 1 ] . minDuration , 30 ) ;
845+ assertEquals ( result . data . stepPerformance [ 1 ] . maxDuration , 40 ) ;
846+ assertEquals ( result . data . stepPerformance [ 1 ] . count , 2 ) ;
847+
848+ // Checkout code: avg=(30+20)/2=25s, min=20s, max=30s
849+ assertEquals ( result . data . stepPerformance [ 2 ] . name , "Checkout code" ) ;
850+ assertEquals ( result . data . stepPerformance [ 2 ] . avgDuration , 25 ) ;
851+ assertEquals ( result . data . stepPerformance [ 2 ] . minDuration , 20 ) ;
852+ assertEquals ( result . data . stepPerformance [ 2 ] . maxDuration , 30 ) ;
853+ assertEquals ( result . data . stepPerformance [ 2 ] . count , 2 ) ;
854+ } ) ;
0 commit comments