diff --git a/src/pipeline/store_reducer.rs b/src/pipeline/store_reducer.rs index 4a451dc..6898f32 100644 --- a/src/pipeline/store_reducer.rs +++ b/src/pipeline/store_reducer.rs @@ -1962,6 +1962,7 @@ fn finalize_source_dependencies( WHERE is_active = 1 AND language_id = 'go' AND relative_path IS NOT NULL + ORDER BY relative_path ASC ", ) .map_err(StoreReducerError::WriteDatabase)?; @@ -2277,7 +2278,8 @@ fn materialize_file_facts( GROUP BY source_path ) source_out ON file_analysis.relative_path = source_out.source_path - WHERE file_analysis.is_active = 1; + WHERE file_analysis.is_active = 1 + ORDER BY file_analysis.relative_path ASC; INSERT OR REPLACE INTO stage_metadata (key, value) VALUES @@ -4008,6 +4010,50 @@ mod tests { ); } + #[test] + fn materialized_package_and_file_lists_are_path_sorted() { + let fixture = Fixture::new("deterministic-lists"); + write_fixture_file(&fixture.path, "zeta/z.go"); + write_fixture_file(&fixture.path, "alpha/a.go"); + + let (event_sender, _event_receiver) = mpsc::channel(); + let reducer = + StoreReducer::start(&fixture.path, StoreReducerOptions::default(), event_sender) + .expect("reducer should start"); + let handle = reducer.handle(); + + handle + .store_file_analysis(file_result_with_imports( + fixture.path.join("zeta/z.go"), + &[], + )) + .expect("zeta file should enqueue"); + handle + .store_file_analysis(file_result_with_imports( + fixture.path.join("alpha/a.go"), + &[], + )) + .expect("alpha file should enqueue"); + + reducer.finish().expect("reducer should finish"); + + let connection = Connection::open(fixture.db_path()).expect("db should open"); + assert_eq!( + text_rows( + &connection, + "SELECT file_path FROM source_file_packages ORDER BY rowid" + ), + vec!["alpha/a.go".to_owned(), "zeta/z.go".to_owned()] + ); + assert_eq!( + text_rows( + &connection, + "SELECT relative_path FROM file_facts ORDER BY rowid" + ), + vec!["alpha/a.go".to_owned(), "zeta/z.go".to_owned()] + ); + } + #[test] fn materializes_go_file_risk_scores_with_terms_facts_and_flags() { let fixture = Fixture::new("file-risk"); @@ -4208,6 +4254,51 @@ mod tests { ); } + #[test] + fn materialized_risk_rows_break_score_ties_by_path() { + let fixture = Fixture::new("file-risk-tie-sort"); + write_fixture_file(&fixture.path, "zeta.go"); + write_fixture_file(&fixture.path, "alpha.go"); + + let (event_sender, _event_receiver) = mpsc::channel(); + let reducer = + StoreReducer::start(&fixture.path, StoreReducerOptions::default(), event_sender) + .expect("reducer should start"); + let handle = reducer.handle(); + + handle + .store_file_analysis(go_result_with_metrics( + fixture.path.join("zeta.go"), + 100, + 10, + 3, + false, + false, + )) + .expect("zeta go file should enqueue"); + handle + .store_file_analysis(go_result_with_metrics( + fixture.path.join("alpha.go"), + 100, + 10, + 3, + false, + false, + )) + .expect("alpha go file should enqueue"); + + reducer.finish().expect("reducer should finish"); + + let connection = Connection::open(fixture.db_path()).expect("db should open"); + assert_eq!( + text_rows( + &connection, + "SELECT relative_path FROM file_risk_scores ORDER BY rank ASC", + ), + vec!["alpha.go".to_owned(), "zeta.go".to_owned()] + ); + } + #[test] fn excludes_generated_and_vendor_go_files_from_risk_scores() { let fixture = Fixture::new("file-risk-generated-vendor"); @@ -4452,6 +4543,14 @@ mod tests { .expect("scalar query should run") } + fn text_rows(connection: &Connection, sql: &str) -> Vec { + let mut statement = connection.prepare(sql).expect("text rows should prepare"); + let rows = statement + .query_map([], |row| row.get(0)) + .expect("text rows should query"); + rows.map(|row| row.expect("text row should read")).collect() + } + fn scalar_f64(connection: &Connection, sql: &str) -> f64 { connection .query_row(sql, [], |row| row.get(0)) diff --git a/src/tui.rs b/src/tui.rs index d82a4b4..58effe1 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -869,7 +869,7 @@ fn load_risk_rows(connection: &Connection) -> rusqlite::Result> { FROM file_risk_scores score LEFT JOIN file_facts facts ON facts.relative_path = score.relative_path - ORDER BY score.rank ASC + ORDER BY score.score DESC, score.relative_path ASC ", )?; let rows = statement.query_map([], |row| { @@ -1664,6 +1664,31 @@ mod tests { assert_eq!(snapshot.rows[0].owners.len(), 1); } + #[test] + fn loads_risk_rows_by_score_descending_then_path_ascending() { + let fixture = Fixture::new("risk-sort"); + create_tui_db(&fixture.db_path()); + let connection = Connection::open(fixture.db_path()).expect("db should open"); + connection + .execute( + "UPDATE file_risk_scores SET rank = 1, score = 0.8, risk_10 = 8.0 WHERE relative_path = 'src/safe.go'", + [], + ) + .expect("safe row should update"); + connection + .execute( + "UPDATE file_risk_scores SET rank = 2, score = 0.8, risk_10 = 8.0 WHERE relative_path = 'src/risky.go'", + [], + ) + .expect("risky row should update"); + + let snapshot = TuiDatabaseSnapshot::load_from_dir(&fixture.path); + + assert_eq!(snapshot.rows.len(), 2); + assert_eq!(snapshot.rows[0].relative_path, "src/risky.go"); + assert_eq!(snapshot.rows[1].relative_path, "src/safe.go"); + } + #[test] fn empty_risk_tables_return_scored_go_empty_state() { let fixture = Fixture::new("empty-risk");