Add KPI filters and detail enrichment for component feedback#700
Add KPI filters and detail enrichment for component feedback#700jvang-rh wants to merge 1 commit into
Conversation
Extend GET /api/v1/analysis/kpi/cve with cve_id, source_component, and multiple_source_components filters plus detail=true component diff fields.
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds ChangesKPI Filtering and Detail Mode
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Hey - I've found 7 issues, and left some high level feedback:
- When
feature='all'is used together withsource_component/multiple_source_componentsfilters, non-component features are still included and filtered based onactual/suggested_valueparsed as JSON lists; consider either restricting these filters to component features or explicitly excluding non-component features to avoid surprising results. - The component diff helpers currently sort accepted/rejected/added lists, which can lose the original ordering from the feedback logs; if analysts rely on the sequence of components, preserving the source order instead of sorting may be more appropriate.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- When `feature='all'` is used together with `source_component`/`multiple_source_components` filters, non-component features are still included and filtered based on `actual`/`suggested_value` parsed as JSON lists; consider either restricting these filters to component features or explicitly excluding non-component features to avoid surprising results.
- The component diff helpers currently sort accepted/rejected/added lists, which can lose the original ordering from the feedback logs; if analysts rely on the sequence of components, preserving the source order instead of sorting may be more appropriate.
## Individual Comments
### Comment 1
<location path="src/aegis_ai_web/tests/test_kpi.py" line_range="1157-1166" />
<code_context>
+class TestComponentKpiHelpers:
</code_context>
<issue_to_address>
**suggestion (testing):** Add edge-case coverage for `_component_diff` (empty, whitespace, duplicates)
Please add tests for `_component_diff` where:
- `suggested` and/or `submitted` are empty lists
- entries are empty strings or whitespace only
- arrays contain duplicate component names (to verify expected handling/order)
These will clarify the intended semantics under noisy input and ensure the helper behaves correctly in common edge cases.
Suggested implementation:
```python
class TestComponentKpiHelpers:
"""Unit tests for component KPI helper functions."""
def test_resolve_feature_aliases_for_source_component(self):
from aegis_ai_web.src.endpoints.kpi import _resolve_feature_aliases
aliases = _resolve_feature_aliases("source_component")
assert aliases == {"source_component", "suggest-affected-components"}
def test_resolve_feature_aliases_for_unrelated_feature(self):
from aegis_ai_web.src.endpoints.kpi import _resolve_feature_aliases
def test_component_diff_with_empty_lists(self):
from aegis_ai_web.src.endpoints.kpi import _component_diff
suggested = []
submitted = []
added, removed = _component_diff(suggested, submitted)
assert added == []
assert removed == []
def test_component_diff_with_empty_suggested(self):
from aegis_ai_web.src.endpoints.kpi import _component_diff
suggested = []
submitted = ["component-a", "component-b"]
added, removed = _component_diff(suggested, submitted)
assert added == []
assert removed == ["component-a", "component-b"]
def test_component_diff_with_empty_submitted(self):
from aegis_ai_web.src.endpoints.kpi import _component_diff
suggested = ["component-a", "component-b"]
submitted = []
added, removed = _component_diff(suggested, submitted)
assert added == ["component-a", "component-b"]
assert removed == []
def test_component_diff_ignores_empty_and_whitespace_entries(self):
from aegis_ai_web.src.endpoints.kpi import _component_diff
suggested = ["", " ", "component-a"]
submitted = [" ", "", "component-b"]
added, removed = _component_diff(suggested, submitted)
# Empty / whitespace-only entries should be ignored
assert added == ["component-a"]
assert removed == ["component-b"]
def test_component_diff_with_duplicate_component_names(self):
from aegis_ai_web.src.endpoints.kpi import _component_diff
suggested = ["component-a", "component-a", "component-b"]
submitted = ["component-b", "component-b", "component-c"]
added, removed = _component_diff(suggested, submitted)
# Duplicates should not result in duplicate diffs, and order should be stable
assert added == ["component-a"]
assert removed == ["component-c"]
```
If `_component_diff` currently returns a different structure (e.g. a dict or a single list), adjust the test unpacking (`added, removed = ...`) and assertions to match the actual return type.
If `_component_diff` does not yet normalize/trim whitespace or de-duplicate component names, implement that behavior in `aegis_ai_web/src/endpoints/kpi.py` so that:
1. Empty (`""`) and whitespace-only strings are filtered out before diffing.
2. Each component name appears at most once in the `added`/`removed` collections, preserving first-seen order.
</issue_to_address>
### Comment 2
<location path="src/aegis_ai_web/tests/test_kpi.py" line_range="1292-1301" />
<code_context>
+ assert data.entries[0].accepted is False
+ assert data.acceptance_percentage == 0.0
+
+ def test_filter_by_source_component(self, feedback_log_setup):
+ from aegis_ai_web.src.endpoints.kpi import get_cve_kpi
+
+ self._write_manual_rows(feedback_log_setup, self._component_rows())
+
+ result = get_cve_kpi(
+ "source_component",
+ source_component="curl",
+ detail=True,
+ )
+ data = result["source_component"]
+
+ assert len(data.entries) == 1
+ assert data.entries[0].cve_id == "CVE-2025-1003"
+ assert data.entries[0].suggested_components == ["kernel", "curl"]
+
+ def test_filter_multiple_source_components(self, feedback_log_setup):
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test for case-insensitive `source_component` filtering
The new `_matches_entry_filters` logic lowercases and trims component names, but `test_filter_by_source_component` only covers an exact-case match (`source_component="curl"`). Add a test where the CSV contains e.g. `"kernel"` and `"curl"`, but the query uses a different casing (e.g. `source_component="Curl"`), and assert the entry is still returned to verify and lock in the case-insensitive behavior.
</issue_to_address>
### Comment 3
<location path="src/aegis_ai_web/tests/test_kpi.py" line_range="1387-1396" />
<code_context>
+ assert data.entries[0].cve_id == "CVE-2025-2001"
+ assert data.entries[0].suggested_components == ["openssl"]
+
+ def test_filters_recompute_acceptance_percentage(self, feedback_log_setup):
+ from aegis_ai_web.src.endpoints.kpi import get_cve_kpi
+
+ self._write_manual_rows(feedback_log_setup, self._component_rows())
+
+ unfiltered = get_cve_kpi("source_component")
+ filtered = get_cve_kpi(
+ "source_component",
+ source_component="curl",
+ )
+
+ assert unfiltered["source_component"].acceptance_percentage == 33.3
+ assert len(unfiltered["source_component"].entries) == 3
+ assert filtered["source_component"].acceptance_percentage == 0.0
+ assert len(filtered["source_component"].entries) == 1
+
+ def test_includes_scored_programmatic_component_feedback(
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a mixed manual+programmatic filter test to verify recomputed acceptance percentages
The current test only exercises manual feedback. Because `_collect_normalized_entries` now mixes manual and deduplicated programmatic entries, please add a case where both exist for the same feature and filters are applied (e.g., one manual accepted, one programmatic rejected, then filter by `source_component` or `cve_id`). Asserting the acceptance percentage and entry count in that mixed scenario will better validate the aggregation logic on realistic data.
Suggested implementation:
```python
def test_filters_recompute_acceptance_percentage_mixed_manual_programmatic(
self, feedback_log_setup, programmatic_feedback_log_setup
):
from aegis_ai_web.src.endpoints.kpi import get_cve_kpi
# One manual accepted feedback for a CVE/component
self._write_manual_rows(
feedback_log_setup,
[
{
"datetime": "2026-03-21 09:00:00.000",
"feature": "source_component",
"value": "curl",
"cve_id": "CVE-2025-2001",
"accepted": True,
},
],
)
# One programmatic rejected feedback for the same CVE/component
self._write_programmatic_rows(
programmatic_feedback_log_setup,
[
{
"datetime": "2026-03-21 10:00:00.123",
"feature": "source_component",
"value": "curl",
"cve_id": "CVE-2025-2001",
"accepted": False,
"score": 0.0,
},
],
)
# Unfiltered KPI should include both manual and programmatic feedback
unfiltered = get_cve_kpi("source_component")
# Filtered KPI should still include both entries, but constrained to the component
filtered = get_cve_kpi(
"source_component",
source_component="curl",
)
assert len(unfiltered["source_component"].entries) == 2
assert len(filtered["source_component"].entries) == 2
# One accepted, one rejected → 50% acceptance in the mixed scenario
assert filtered["source_component"].acceptance_percentage == 50.0
def test_includes_scored_programmatic_component_feedback(
self, feedback_log_setup, programmatic_feedback_log_setup
):
from aegis_ai_web.src.endpoints.kpi import get_cve_kpi
self._write_programmatic_rows(
programmatic_feedback_log_setup,
[
{
"datetime": "2026-03-21 10:00:00.123",
"feature": "source_component",
```
The new mixed manual+programmatic filter test assumes the following, which you should align with the existing helpers and KPI implementation:
1. Ensure `_write_manual_rows` and `_write_programmatic_rows` accept the keys used here (`value`, `cve_id`, `accepted`, `score`). If your existing tests use different keys (e.g., `source_component` instead of `value`, or `is_accepted`), adjust the new test’s dictionaries accordingly.
2. Confirm how acceptance percentage is computed when manual and programmatic entries are mixed. If programmatic entries are deduplicated or merged with manual ones, you may need to tweak the expected `len(...entries)` and `acceptance_percentage` values to reflect the actual aggregation logic.
3. If filtering by `source_component` uses a different parameter name or filter field (e.g., `component` or `suggested_component`), update the `get_cve_kpi` calls in the new test to match the existing API.
</issue_to_address>
### Comment 4
<location path="src/aegis_ai_web/tests/test_kpi.py" line_range="1359-1368" />
<code_context>
+ def test_feature_alias_suggest_affected_components(self, feedback_log_setup):
</code_context>
<issue_to_address>
**suggestion (testing):** Add coverage for `feature='all'` combined with component feature aliases and filters
Since `_get_all_features_kpi` now uses `_collect_normalized_entries` with alias handling and filters, please add a complementary test that exercises the `feature='all'` path. For example, call `get_cve_kpi("all", detail=True, source_component="openssl")` and assert that both `"source_component"` and `"suggest-affected-components"` (or the expected feature set) are present with correctly filtered entries. This will validate alias resolution and filtering when multiple features are requested.
Suggested implementation:
```python
def test_feature_alias_suggest_affected_components(self, feedback_log_setup):
from aegis_ai_web.src.endpoints.kpi import get_cve_kpi
self._write_manual_rows(
feedback_log_setup,
[
{
"datetime": "2026-03-19 21:52:24.452",
"feature": "suggest-affected-components",
"cve_id": "CVE-2025-2001",
"email": "analyst@example.com",
},
],
)
result = get_cve_kpi("suggest-affected-components", detail=True)
suggest_entries = result["suggest-affected-components"].entries
assert len(suggest_entries) > 0
for entry in suggest_entries:
payload = entry.model_dump(exclude_none=True)
assert payload["cve_id"] == "CVE-2025-2001"
def test_feature_all_with_alias_and_filters(self, feedback_log_setup):
from aegis_ai_web.src.endpoints.kpi import get_cve_kpi
# Write mixed feature rows, with component filter and alias
self._write_manual_rows(
feedback_log_setup,
[
{
"datetime": "2026-03-19 21:52:24.452",
"feature": "source_component",
"cve_id": "CVE-2025-2001",
"email": "analyst@example.com",
"source_component": "openssl",
"accepted": True,
"aegis_version": "0.6.1",
},
{
"datetime": "2026-03-19 21:52:24.452",
"feature": "suggest-affected-components",
"cve_id": "CVE-2025-2002",
"email": "analyst@example.com",
"source_component": "openssl",
"accepted": True,
"aegis_version": "0.6.1",
},
{
# This entry should be filtered out by source_component="openssl"
"datetime": "2026-03-19 21:52:24.452",
"feature": "suggest-affected-components",
"cve_id": "CVE-2025-2003",
"email": "analyst@example.com",
"source_component": "nginx",
"accepted": True,
"aegis_version": "0.6.1",
},
],
)
result = get_cve_kpi("all", detail=True, source_component="openssl")
# Ensure both features are present in the aggregated result
assert "source_component" in result
assert "suggest-affected-components" in result
source_entries = result["source_component"].entries
suggest_entries = result["suggest-affected-components"].entries
# All entries must be filtered by the requested source_component
assert len(source_entries) > 0
assert len(suggest_entries) > 0
for entry in source_entries:
payload = entry.model_dump(exclude_none=True)
assert payload["source_component"] == "openssl"
assert payload["cve_id"] == "CVE-2025-2001"
for entry in suggest_entries:
payload = entry.model_dump(exclude_none=True)
assert payload["source_component"] == "openssl"
# Only the openssl entry should remain, nginx should be filtered out
assert payload["cve_id"] == "CVE-2025-2002"
```
I only see part of `test_feature_alias_suggest_affected_components`, so you may need to:
1. Remove any duplicated or conflicting code inside that test if it already has assertions or a different setup further down in the file.
2. Ensure that the structure of `feedback_log_setup` and `self._write_manual_rows` matches these fields (`source_component`, `accepted`, `aegis_version`). If your test helpers use different field names, adjust the dictionaries accordingly.
3. If `get_cve_kpi("all", ...)` returns a different shape (e.g., attributes instead of dict keys, or different feature names/aliases), update the assertions to match your actual return type and alias resolution.
4. If the datetime/aegis_version/email fields are mandatory or validated in other tests, keep them consistent here or reuse existing test fixtures/constants instead of hardcoding values.
</issue_to_address>
### Comment 5
<location path="src/aegis_ai_web/tests/test_kpi.py" line_range="1403-1342" />
<code_context>
+ def test_includes_scored_programmatic_component_feedback(
</code_context>
<issue_to_address>
**suggestion (testing):** Add an explicit test to confirm unscored programmatic entries are excluded
To fully validate `_normalize_programmatic_entry`, please add a test CSV fixture that includes both a valid scored programmatic entry and entries with empty or non-numeric `acceptance_score`. The test should assert that only the scored entry appears in `get_cve_kpi("source_component", detail=True)` results, confirming unscored programmatic feedback is excluded.
</issue_to_address>
### Comment 6
<location path="src/aegis_ai_web/tests/test_kpi.py" line_range="1484-1493" />
<code_context>
+ "aegis_version",
+ }
+
+ def test_api_detail_and_filters(self, feedback_log_setup):
+ TestComponentKpiFilters._write_manual_rows(
+ feedback_log_setup, TestComponentKpiFilters._component_rows()
+ )
+
+ response = client.get(
+ "/api/v1/analysis/kpi/cve"
+ "?feature=source_component"
+ "&detail=true"
+ "&cve_id=CVE-2025-1002"
+ )
+ assert response.status_code == 200
+ entry = response.json()["source_component"]["entries"][0]
+ assert entry["cve_id"] == "CVE-2025-1002"
+ assert entry["suggested_components"] == ["kernel"]
+ assert entry["submitted_components"] == ["linux-kernel"]
+ assert entry["rejected_suggestions"] == ["kernel"]
+ assert entry["added_components"] == ["linux-kernel"]
+
+ def test_api_multiple_source_components_filter(self, feedback_log_setup):
</code_context>
<issue_to_address>
**suggestion (testing):** Consider an API-level test to assert `response_model_exclude_none` behavior with `detail=true`
This test verifies enriched fields with `detail=true` and filters, but does not assert that `None` fields are excluded from the JSON when `response_model_exclude_none=True`. Please add a focused API test where some optional detail fields are unset (e.g., `email`, `accepted_components`), and assert that these keys are absent from the response to cover the null-omission behavior.
Suggested implementation:
```python
def test_api_detail_and_filters(self, feedback_log_setup):
TestComponentKpiFilters._write_manual_rows(
feedback_log_setup, TestComponentKpiFilters._component_rows()
)
response = client.get(
"/api/v1/analysis/kpi/cve"
"?feature=source_component"
"&detail=true"
"&cve_id=CVE-2025-1002"
)
assert response.status_code == 200
entry = response.json()["source_component"]["entries"][0]
assert entry["cve_id"] == "CVE-2025-1002"
assert entry["suggested_components"] == ["kernel"]
assert entry["submitted_components"] == ["linux-kernel"]
assert entry["rejected_suggestions"] == ["kernel"]
assert entry["added_components"] == ["linux-kernel"]
def test_api_detail_excludes_none_fields(self, feedback_log_setup):
"""
Assert response_model_exclude_none behavior with detail=true:
optional fields that are unset/None (e.g., email, accepted_components)
must be omitted from the JSON payload.
"""
TestComponentKpiFilters._write_manual_rows(
feedback_log_setup,
TestComponentKpiFilters._component_rows_with_missing_optional_fields(),
)
response = client.get(
"/api/v1/analysis/kpi/cve"
"?feature=source_component"
"&detail=true"
)
assert response.status_code == 200
data = response.json()
assert "source_component" in data
assert data["source_component"]["entries"], "Expected at least one entry"
entry = data["source_component"]["entries"][0]
# Optional fields that are unset/None in the test data
# must not be present when response_model_exclude_none=True.
assert "email" not in entry
assert "accepted_components" not in entry
def test_api_multiple_source_components_filter(self, feedback_log_setup):
TestComponentKpiFilters._write_manual_rows(
feedback_log_setup, TestComponentKpiFilters._component_rows()
)
response = client.get(
"/api/v1/analysis/kpi/cve"
"?feature=source_component"
"&multiple_source_components=true"
"&detail=true"
)
```
To make `test_api_detail_excludes_none_fields` pass and correctly exercise `response_model_exclude_none`, you will need to:
1. Implement `TestComponentKpiFilters._component_rows_with_missing_optional_fields()` in the same test module (or wherever `TestComponentKpiFilters` is defined). It should return rows where the detail-level optional fields such as `email` and `accepted_components` are explicitly unset or set to `None` for at least one entry.
2. Ensure that these rows, when processed by the API, would normally populate `email` and `accepted_components` when present, so that the absence of these keys in the response is specifically due to `response_model_exclude_none=True` and not because the fields don't exist in the model.
</issue_to_address>
### Comment 7
<location path="src/aegis_ai_web/tests/test_kpi.py" line_range="1503-1512" />
<code_context>
+ assert entry["rejected_suggestions"] == ["kernel"]
+ assert entry["added_components"] == ["linux-kernel"]
+
+ def test_api_multiple_source_components_filter(self, feedback_log_setup):
+ TestComponentKpiFilters._write_manual_rows(
+ feedback_log_setup, TestComponentKpiFilters._component_rows()
+ )
+
+ response = client.get(
+ "/api/v1/analysis/kpi/cve"
+ "?feature=source_component"
+ "&multiple_source_components=true"
+ "&detail=true"
+ )
+ assert response.status_code == 200
+ entries = response.json()["source_component"]["entries"]
+ assert len(entries) == 1
+ assert entries[0]["cve_id"] == "CVE-2025-1003"
+
+ def test_api_invalid_cve_id_returns_422(self, feedback_log_setup):
</code_context>
<issue_to_address>
**suggestion (testing):** Add a negative API test for `multiple_source_components` with single-component suggestions
This test currently exercises the positive case where `multiple_source_components=true` yields an entry with two suggested components. Please also add an API test combining `multiple_source_components=true` with a `source_component` or `cve_id` that only occurs in single-component suggestions, and assert that the response has an empty `entries` list and `acceptance_percentage` 0.0. This will mirror `test_no_matches_returns_empty_result` at the HTTP layer and verify consistent behavior for this filter.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| class TestComponentKpiHelpers: | ||
| """Unit tests for component KPI helper functions.""" | ||
|
|
||
| def test_resolve_feature_aliases_for_source_component(self): | ||
| from aegis_ai_web.src.endpoints.kpi import _resolve_feature_aliases | ||
|
|
||
| aliases = _resolve_feature_aliases("source_component") | ||
| assert aliases == {"source_component", "suggest-affected-components"} | ||
|
|
||
| def test_resolve_feature_aliases_for_unrelated_feature(self): |
There was a problem hiding this comment.
suggestion (testing): Add edge-case coverage for _component_diff (empty, whitespace, duplicates)
Please add tests for _component_diff where:
suggestedand/orsubmittedare empty lists- entries are empty strings or whitespace only
- arrays contain duplicate component names (to verify expected handling/order)
These will clarify the intended semantics under noisy input and ensure the helper behaves correctly in common edge cases.
Suggested implementation:
class TestComponentKpiHelpers:
"""Unit tests for component KPI helper functions."""
def test_resolve_feature_aliases_for_source_component(self):
from aegis_ai_web.src.endpoints.kpi import _resolve_feature_aliases
aliases = _resolve_feature_aliases("source_component")
assert aliases == {"source_component", "suggest-affected-components"}
def test_resolve_feature_aliases_for_unrelated_feature(self):
from aegis_ai_web.src.endpoints.kpi import _resolve_feature_aliases
def test_component_diff_with_empty_lists(self):
from aegis_ai_web.src.endpoints.kpi import _component_diff
suggested = []
submitted = []
added, removed = _component_diff(suggested, submitted)
assert added == []
assert removed == []
def test_component_diff_with_empty_suggested(self):
from aegis_ai_web.src.endpoints.kpi import _component_diff
suggested = []
submitted = ["component-a", "component-b"]
added, removed = _component_diff(suggested, submitted)
assert added == []
assert removed == ["component-a", "component-b"]
def test_component_diff_with_empty_submitted(self):
from aegis_ai_web.src.endpoints.kpi import _component_diff
suggested = ["component-a", "component-b"]
submitted = []
added, removed = _component_diff(suggested, submitted)
assert added == ["component-a", "component-b"]
assert removed == []
def test_component_diff_ignores_empty_and_whitespace_entries(self):
from aegis_ai_web.src.endpoints.kpi import _component_diff
suggested = ["", " ", "component-a"]
submitted = [" ", "", "component-b"]
added, removed = _component_diff(suggested, submitted)
# Empty / whitespace-only entries should be ignored
assert added == ["component-a"]
assert removed == ["component-b"]
def test_component_diff_with_duplicate_component_names(self):
from aegis_ai_web.src.endpoints.kpi import _component_diff
suggested = ["component-a", "component-a", "component-b"]
submitted = ["component-b", "component-b", "component-c"]
added, removed = _component_diff(suggested, submitted)
# Duplicates should not result in duplicate diffs, and order should be stable
assert added == ["component-a"]
assert removed == ["component-c"]If _component_diff currently returns a different structure (e.g. a dict or a single list), adjust the test unpacking (added, removed = ...) and assertions to match the actual return type.
If _component_diff does not yet normalize/trim whitespace or de-duplicate component names, implement that behavior in aegis_ai_web/src/endpoints/kpi.py so that:
- Empty (
"") and whitespace-only strings are filtered out before diffing. - Each component name appears at most once in the
added/removedcollections, preserving first-seen order.
| def test_filter_by_source_component(self, feedback_log_setup): | ||
| from aegis_ai_web.src.endpoints.kpi import get_cve_kpi | ||
|
|
||
| self._write_manual_rows(feedback_log_setup, self._component_rows()) | ||
|
|
||
| result = get_cve_kpi( | ||
| "source_component", | ||
| source_component="curl", | ||
| detail=True, | ||
| ) |
There was a problem hiding this comment.
suggestion (testing): Add a test for case-insensitive source_component filtering
The new _matches_entry_filters logic lowercases and trims component names, but test_filter_by_source_component only covers an exact-case match (source_component="curl"). Add a test where the CSV contains e.g. "kernel" and "curl", but the query uses a different casing (e.g. source_component="Curl"), and assert the entry is still returned to verify and lock in the case-insensitive behavior.
| def test_filters_recompute_acceptance_percentage(self, feedback_log_setup): | ||
| from aegis_ai_web.src.endpoints.kpi import get_cve_kpi | ||
|
|
||
| self._write_manual_rows(feedback_log_setup, self._component_rows()) | ||
|
|
||
| unfiltered = get_cve_kpi("source_component") | ||
| filtered = get_cve_kpi( | ||
| "source_component", | ||
| source_component="curl", | ||
| ) |
There was a problem hiding this comment.
suggestion (testing): Consider adding a mixed manual+programmatic filter test to verify recomputed acceptance percentages
The current test only exercises manual feedback. Because _collect_normalized_entries now mixes manual and deduplicated programmatic entries, please add a case where both exist for the same feature and filters are applied (e.g., one manual accepted, one programmatic rejected, then filter by source_component or cve_id). Asserting the acceptance percentage and entry count in that mixed scenario will better validate the aggregation logic on realistic data.
Suggested implementation:
def test_filters_recompute_acceptance_percentage_mixed_manual_programmatic(
self, feedback_log_setup, programmatic_feedback_log_setup
):
from aegis_ai_web.src.endpoints.kpi import get_cve_kpi
# One manual accepted feedback for a CVE/component
self._write_manual_rows(
feedback_log_setup,
[
{
"datetime": "2026-03-21 09:00:00.000",
"feature": "source_component",
"value": "curl",
"cve_id": "CVE-2025-2001",
"accepted": True,
},
],
)
# One programmatic rejected feedback for the same CVE/component
self._write_programmatic_rows(
programmatic_feedback_log_setup,
[
{
"datetime": "2026-03-21 10:00:00.123",
"feature": "source_component",
"value": "curl",
"cve_id": "CVE-2025-2001",
"accepted": False,
"score": 0.0,
},
],
)
# Unfiltered KPI should include both manual and programmatic feedback
unfiltered = get_cve_kpi("source_component")
# Filtered KPI should still include both entries, but constrained to the component
filtered = get_cve_kpi(
"source_component",
source_component="curl",
)
assert len(unfiltered["source_component"].entries) == 2
assert len(filtered["source_component"].entries) == 2
# One accepted, one rejected → 50% acceptance in the mixed scenario
assert filtered["source_component"].acceptance_percentage == 50.0
def test_includes_scored_programmatic_component_feedback(
self, feedback_log_setup, programmatic_feedback_log_setup
):
from aegis_ai_web.src.endpoints.kpi import get_cve_kpi
self._write_programmatic_rows(
programmatic_feedback_log_setup,
[
{
"datetime": "2026-03-21 10:00:00.123",
"feature": "source_component",The new mixed manual+programmatic filter test assumes the following, which you should align with the existing helpers and KPI implementation:
- Ensure
_write_manual_rowsand_write_programmatic_rowsaccept the keys used here (value,cve_id,accepted,score). If your existing tests use different keys (e.g.,source_componentinstead ofvalue, oris_accepted), adjust the new test’s dictionaries accordingly. - Confirm how acceptance percentage is computed when manual and programmatic entries are mixed. If programmatic entries are deduplicated or merged with manual ones, you may need to tweak the expected
len(...entries)andacceptance_percentagevalues to reflect the actual aggregation logic. - If filtering by
source_componentuses a different parameter name or filter field (e.g.,componentorsuggested_component), update theget_cve_kpicalls in the new test to match the existing API.
| def test_feature_alias_suggest_affected_components(self, feedback_log_setup): | ||
| from aegis_ai_web.src.endpoints.kpi import get_cve_kpi | ||
|
|
||
| self._write_manual_rows( | ||
| feedback_log_setup, | ||
| [ | ||
| { | ||
| "datetime": "2026-03-19 21:52:24.452", | ||
| "feature": "suggest-affected-components", | ||
| "cve_id": "CVE-2025-2001", |
There was a problem hiding this comment.
suggestion (testing): Add coverage for feature='all' combined with component feature aliases and filters
Since _get_all_features_kpi now uses _collect_normalized_entries with alias handling and filters, please add a complementary test that exercises the feature='all' path. For example, call get_cve_kpi("all", detail=True, source_component="openssl") and assert that both "source_component" and "suggest-affected-components" (or the expected feature set) are present with correctly filtered entries. This will validate alias resolution and filtering when multiple features are requested.
Suggested implementation:
def test_feature_alias_suggest_affected_components(self, feedback_log_setup):
from aegis_ai_web.src.endpoints.kpi import get_cve_kpi
self._write_manual_rows(
feedback_log_setup,
[
{
"datetime": "2026-03-19 21:52:24.452",
"feature": "suggest-affected-components",
"cve_id": "CVE-2025-2001",
"email": "analyst@example.com",
},
],
)
result = get_cve_kpi("suggest-affected-components", detail=True)
suggest_entries = result["suggest-affected-components"].entries
assert len(suggest_entries) > 0
for entry in suggest_entries:
payload = entry.model_dump(exclude_none=True)
assert payload["cve_id"] == "CVE-2025-2001"
def test_feature_all_with_alias_and_filters(self, feedback_log_setup):
from aegis_ai_web.src.endpoints.kpi import get_cve_kpi
# Write mixed feature rows, with component filter and alias
self._write_manual_rows(
feedback_log_setup,
[
{
"datetime": "2026-03-19 21:52:24.452",
"feature": "source_component",
"cve_id": "CVE-2025-2001",
"email": "analyst@example.com",
"source_component": "openssl",
"accepted": True,
"aegis_version": "0.6.1",
},
{
"datetime": "2026-03-19 21:52:24.452",
"feature": "suggest-affected-components",
"cve_id": "CVE-2025-2002",
"email": "analyst@example.com",
"source_component": "openssl",
"accepted": True,
"aegis_version": "0.6.1",
},
{
# This entry should be filtered out by source_component="openssl"
"datetime": "2026-03-19 21:52:24.452",
"feature": "suggest-affected-components",
"cve_id": "CVE-2025-2003",
"email": "analyst@example.com",
"source_component": "nginx",
"accepted": True,
"aegis_version": "0.6.1",
},
],
)
result = get_cve_kpi("all", detail=True, source_component="openssl")
# Ensure both features are present in the aggregated result
assert "source_component" in result
assert "suggest-affected-components" in result
source_entries = result["source_component"].entries
suggest_entries = result["suggest-affected-components"].entries
# All entries must be filtered by the requested source_component
assert len(source_entries) > 0
assert len(suggest_entries) > 0
for entry in source_entries:
payload = entry.model_dump(exclude_none=True)
assert payload["source_component"] == "openssl"
assert payload["cve_id"] == "CVE-2025-2001"
for entry in suggest_entries:
payload = entry.model_dump(exclude_none=True)
assert payload["source_component"] == "openssl"
# Only the openssl entry should remain, nginx should be filtered out
assert payload["cve_id"] == "CVE-2025-2002"I only see part of test_feature_alias_suggest_affected_components, so you may need to:
- Remove any duplicated or conflicting code inside that test if it already has assertions or a different setup further down in the file.
- Ensure that the structure of
feedback_log_setupandself._write_manual_rowsmatches these fields (source_component,accepted,aegis_version). If your test helpers use different field names, adjust the dictionaries accordingly. - If
get_cve_kpi("all", ...)returns a different shape (e.g., attributes instead of dict keys, or different feature names/aliases), update the assertions to match your actual return type and alias resolution. - If the datetime/aegis_version/email fields are mandatory or validated in other tests, keep them consistent here or reuse existing test fixtures/constants instead of hardcoding values.
| assert entry.suggested_components == ["kernel"] | ||
| assert entry.submitted_components == ["linux-kernel"] | ||
| assert entry.rejected_suggestions == ["kernel"] | ||
| assert entry.added_components == ["linux-kernel"] |
There was a problem hiding this comment.
suggestion (testing): Add an explicit test to confirm unscored programmatic entries are excluded
To fully validate _normalize_programmatic_entry, please add a test CSV fixture that includes both a valid scored programmatic entry and entries with empty or non-numeric acceptance_score. The test should assert that only the scored entry appears in get_cve_kpi("source_component", detail=True) results, confirming unscored programmatic feedback is excluded.
| def test_api_detail_and_filters(self, feedback_log_setup): | ||
| TestComponentKpiFilters._write_manual_rows( | ||
| feedback_log_setup, TestComponentKpiFilters._component_rows() | ||
| ) | ||
|
|
||
| response = client.get( | ||
| "/api/v1/analysis/kpi/cve" | ||
| "?feature=source_component" | ||
| "&detail=true" | ||
| "&cve_id=CVE-2025-1002" |
There was a problem hiding this comment.
suggestion (testing): Consider an API-level test to assert response_model_exclude_none behavior with detail=true
This test verifies enriched fields with detail=true and filters, but does not assert that None fields are excluded from the JSON when response_model_exclude_none=True. Please add a focused API test where some optional detail fields are unset (e.g., email, accepted_components), and assert that these keys are absent from the response to cover the null-omission behavior.
Suggested implementation:
def test_api_detail_and_filters(self, feedback_log_setup):
TestComponentKpiFilters._write_manual_rows(
feedback_log_setup, TestComponentKpiFilters._component_rows()
)
response = client.get(
"/api/v1/analysis/kpi/cve"
"?feature=source_component"
"&detail=true"
"&cve_id=CVE-2025-1002"
)
assert response.status_code == 200
entry = response.json()["source_component"]["entries"][0]
assert entry["cve_id"] == "CVE-2025-1002"
assert entry["suggested_components"] == ["kernel"]
assert entry["submitted_components"] == ["linux-kernel"]
assert entry["rejected_suggestions"] == ["kernel"]
assert entry["added_components"] == ["linux-kernel"]
def test_api_detail_excludes_none_fields(self, feedback_log_setup):
"""
Assert response_model_exclude_none behavior with detail=true:
optional fields that are unset/None (e.g., email, accepted_components)
must be omitted from the JSON payload.
"""
TestComponentKpiFilters._write_manual_rows(
feedback_log_setup,
TestComponentKpiFilters._component_rows_with_missing_optional_fields(),
)
response = client.get(
"/api/v1/analysis/kpi/cve"
"?feature=source_component"
"&detail=true"
)
assert response.status_code == 200
data = response.json()
assert "source_component" in data
assert data["source_component"]["entries"], "Expected at least one entry"
entry = data["source_component"]["entries"][0]
# Optional fields that are unset/None in the test data
# must not be present when response_model_exclude_none=True.
assert "email" not in entry
assert "accepted_components" not in entry
def test_api_multiple_source_components_filter(self, feedback_log_setup):
TestComponentKpiFilters._write_manual_rows(
feedback_log_setup, TestComponentKpiFilters._component_rows()
)
response = client.get(
"/api/v1/analysis/kpi/cve"
"?feature=source_component"
"&multiple_source_components=true"
"&detail=true"
)To make test_api_detail_excludes_none_fields pass and correctly exercise response_model_exclude_none, you will need to:
- Implement
TestComponentKpiFilters._component_rows_with_missing_optional_fields()in the same test module (or whereverTestComponentKpiFiltersis defined). It should return rows where the detail-level optional fields such asemailandaccepted_componentsare explicitly unset or set toNonefor at least one entry. - Ensure that these rows, when processed by the API, would normally populate
emailandaccepted_componentswhen present, so that the absence of these keys in the response is specifically due toresponse_model_exclude_none=Trueand not because the fields don't exist in the model.
| def test_api_multiple_source_components_filter(self, feedback_log_setup): | ||
| TestComponentKpiFilters._write_manual_rows( | ||
| feedback_log_setup, TestComponentKpiFilters._component_rows() | ||
| ) | ||
|
|
||
| response = client.get( | ||
| "/api/v1/analysis/kpi/cve" | ||
| "?feature=source_component" | ||
| "&multiple_source_components=true" | ||
| "&detail=true" |
There was a problem hiding this comment.
suggestion (testing): Add a negative API test for multiple_source_components with single-component suggestions
This test currently exercises the positive case where multiple_source_components=true yields an entry with two suggested components. Please also add an API test combining multiple_source_components=true with a source_component or cve_id that only occurs in single-component suggestions, and assert that the response has an empty entries list and acceptance_percentage 0.0. This will mirror test_no_matches_returns_empty_result at the HTTP layer and verify consistent behavior for this filter.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/aegis_ai_web/src/data_models.py`:
- Around line 318-320: The KPI response model currently exposes a raw email
field, which should not be part of the public contract. Remove the email
attribute from the response schema in Data models (the model defining the KPI
feedback/detail payload) and update the KPI endpoint logic in kpi.py to stop
populating it; if correlation is still needed, replace it with a pseudonymous
identifier or keep the value server-side only.
In `@src/aegis_ai_web/src/endpoints/kpi.py`:
- Around line 33-37: The alias resolution in _resolve_feature_aliases is
expanding both feature values into the full COMPONENT_FEATURE_KEYS set, which
unintentionally changes the legacy suggest-affected-components KPI scope. Update
the logic so only source_component maps to the broader component-feature union,
while suggest-affected-components continues to resolve to its original single
key. Keep the change localized to _resolve_feature_aliases and verify any
callers in the KPI endpoint still receive the expected one-way alias behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Enterprise
Run ID: e433b03f-aa11-4b67-9987-eca57548235c
📒 Files selected for processing (5)
docs/CHANGELOG.mdsrc/aegis_ai_web/src/data_models.pysrc/aegis_ai_web/src/endpoints/kpi.pysrc/aegis_ai_web/src/main.pysrc/aegis_ai_web/tests/test_kpi.py
| email: Optional[str] = Field( | ||
| default=None, | ||
| description="User email from feedback (included when detail=true)", |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Remove raw email from the KPI response model.
detail=true now makes the feedback submitter's email part of the API contract, and src/aegis_ai_web/src/endpoints/kpi.py actively populates it. That's unnecessary PII for a KPI endpoint and increases disclosure/retention risk. Prefer a pseudonymous identifier, or keep email server-side unless this route is explicitly admin-only.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/aegis_ai_web/src/data_models.py` around lines 318 - 320, The KPI response
model currently exposes a raw email field, which should not be part of the
public contract. Remove the email attribute from the response schema in Data
models (the model defining the KPI feedback/detail payload) and update the KPI
endpoint logic in kpi.py to stop populating it; if correlation is still needed,
replace it with a pseudonymous identifier or keep the value server-side only.
There was a problem hiding this comment.
@jvang-rh I tend to agree. If I remember correctly, we did not expose all the captured data on purpose because it may contain sensitive information. Moreover, with the email field specifically, we may need to retake PIA, etc.
There was a problem hiding this comment.
@kdudka Thanks for confirming — that context is helpful. Given the PIA implications, it'd be safer to drop email from the KPIEntry response model entirely (or replace it with a non-identifying hash/token if correlation across entries is genuinely needed) rather than exposing it behind detail=true. Happy to help review once the field is removed or reworked.
📝 Learnings were identified and not saved (knowledge base disabled). Enable
| def _resolve_feature_aliases(query_feature: str) -> set[str]: | ||
| """Map component feature aliases to the same CSV feature keys.""" | ||
| if query_feature in COMPONENT_FEATURE_KEYS: | ||
| return set(COMPONENT_FEATURE_KEYS) | ||
| return {query_feature} |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Keep source_component aliasing one-way.
This expands both feature=source_component and feature=suggest-affected-components to the union of both log keys. That changes the legacy suggest-affected-components filter from “that feature only” to “both component features”, while the PR objective/changelog only describe source_component as the alias. Restrict the expansion to source_component so existing callers keep the old KPI scope.
Proposed fix
def _resolve_feature_aliases(query_feature: str) -> set[str]:
"""Map component feature aliases to the same CSV feature keys."""
- if query_feature in COMPONENT_FEATURE_KEYS:
+ if query_feature == "source_component":
return set(COMPONENT_FEATURE_KEYS)
return {query_feature}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def _resolve_feature_aliases(query_feature: str) -> set[str]: | |
| """Map component feature aliases to the same CSV feature keys.""" | |
| if query_feature in COMPONENT_FEATURE_KEYS: | |
| return set(COMPONENT_FEATURE_KEYS) | |
| return {query_feature} | |
| def _resolve_feature_aliases(query_feature: str) -> set[str]: | |
| """Map component feature aliases to the same CSV feature keys.""" | |
| if query_feature == "source_component": | |
| return set(COMPONENT_FEATURE_KEYS) | |
| return {query_feature} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/aegis_ai_web/src/endpoints/kpi.py` around lines 33 - 37, The alias
resolution in _resolve_feature_aliases is expanding both feature values into the
full COMPONENT_FEATURE_KEYS set, which unintentionally changes the legacy
suggest-affected-components KPI scope. Update the logic so only source_component
maps to the broader component-feature union, while suggest-affected-components
continues to resolve to its original single key. Keep the change localized to
_resolve_feature_aliases and verify any callers in the KPI endpoint still
receive the expected one-way alias behavior.
Will provide more in-depth feedback shortly |
kdudka
left a comment
There was a problem hiding this comment.
@jvang-rh Thank you for working on this!
Ideally, the pull request should be opened from an upstream branch so that the CI pipelines have access to our secrets. Unfortunately, I am not able to grant you access in the project settings.
I guess you need to become a member of the RedHatProductSecurity group first?
| ) -> tuple[list[str], list[str], list[str]]: | ||
| """Return accepted, rejected, and added component lists (case-insensitive).""" | ||
| suggested_map = {c.lower().strip(): c for c in suggested if c.strip()} | ||
| submitted_map = {c.lower().strip(): c for c in submitted if c.strip()} |
There was a problem hiding this comment.
I am not sure about the .lower() normalization. So far Aegis treated components as case sensitive (e.g. Glib and glib as two distinct components).
| email: Optional[str] = Field( | ||
| default=None, | ||
| description="User email from feedback (included when detail=true)", |
There was a problem hiding this comment.
@jvang-rh I tend to agree. If I remember correctly, we did not expose all the captured data on purpose because it may contain sensitive information. Moreover, with the email field specifically, we may need to retake PIA, etc.
What
Extend
GET /api/v1/analysis/kpi/cvewith component feedback filters and optional detail enrichment.Why
Analysts need to query KPI data by CVE, suggested component, and multi-component suggestions, and inspect component diffs (accepted/rejected/added) without post-processing raw feedback logs.
How
cve_id,source_component,multiple_source_components,detailsource_componentfeature alias to bothsource_componentandsuggest-affected-componentslog entriesKPIEntryfields whendetail=true(suggested_components,accepted_components,rejected_suggestions,added_components, etc.)response_model_exclude_none=Trueso detail=false responses stay compactHow to Test
After staging deploy:
Related Tickets
Summary by Sourcery
Extend the CVE KPI endpoint to support component feedback filtering and optional detail fields for enriched analysis.
New Features:
Enhancements:
Documentation:
Tests: