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
13 changes: 11 additions & 2 deletions ymir/agents/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,7 @@ async def set_jira_labels(
_MAX_GENERATED_TITLE_LENGTH = 80
_MAX_CANONICAL_TITLE_REPLACEMENT_ATTEMPTS = 3
_JIRA_ISSUE_KEY_RE = re.compile(r"\b(?:RHEL|PACKIT)-\d+\b", re.IGNORECASE)
_CVE_STREAM_SUFFIX_RE = re.compile(r"\s+\[rhel-[^\]]+\]\s*$", re.IGNORECASE)
_CONDITIONAL_DELETE_LUA = """
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
Expand Down Expand Up @@ -914,6 +915,11 @@ def _validate_canonical_title(title: str, jira_issue: str) -> str:
return title


def _normalize_cve_title(title: str) -> str:
"""Remove the stream-specific suffix Jira adds to CVE summaries."""
return _CVE_STREAM_SUFFIX_RE.sub("", title)


def _validate_generated_title(title: str, jira_issue: str) -> str:
"""Enforce the stricter title-agent output contract before publication."""
title = _validate_canonical_title(title, jira_issue)
Expand All @@ -938,6 +944,8 @@ async def _get_cached_canonical_metadata(
validator = _validate_canonical_title if is_cve else _validate_generated_title
validator(metadata.title, jira_issue)
_normalize_jira_updated(metadata.summary_updated)
if is_cve:
metadata = metadata.model_copy(update={"title": _normalize_cve_title(metadata.title)})
return metadata, cached
except ValueError as error:
logger.warning("Discarding invalid canonical title cache record %s: %s", cache_key, error)
Expand Down Expand Up @@ -1037,7 +1045,8 @@ async def resolve_canonical_mr_title(
The Redis key is stable for the package/family. A matching summary digest
reuses the record; only a newer ``summary_updated`` value from its source
Jira issue compare-and-swaps a replacement. CVE families use the Jira
summary verbatim; non-CVE families require a validated generated title.
summary with its stream-specific suffix removed; non-CVE families require
a validated generated title.
"""
jira_summary = _validate_canonical_title(jira_summary, jira_issue)
summary_updated = _normalize_jira_updated(summary_updated)
Expand All @@ -1061,7 +1070,7 @@ async def resolve_canonical_mr_title(
logger.info("Kept newer canonical MR title for %s from %s", jira_issue, cache_key)
return cached_metadata.title

title = jira_summary if cve_ids else generated_title
title = _normalize_cve_title(jira_summary) if cve_ids else generated_title
if title is None:
raise ValueError(f"Non-CVE issue {jira_issue} requires a generated title on cache miss")
title = (
Expand Down
39 changes: 38 additions & 1 deletion ymir/agents/tests/unit/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
_canonical_mr_title_key,
_check_zstream_branch_consistency,
_is_newer_summary,
_normalize_cve_title,
_normalize_jira_updated,
_validate_generated_title,
canonical_title_mentions_components,
Expand Down Expand Up @@ -241,7 +242,7 @@ async def generator(_summary):
raise AssertionError("CVE title generation must not run")

async def jira_details(*_args, **_kwargs):
return {"fields": {"summary": "CVE-2026-1234 curl: Fix an overflow"}}
return {"fields": {"summary": "CVE-2026-1234 curl: Fix an overflow [rhel-9.9]"}}

flexmock(agent_tasks).should_receive("run_tool").replace_with(jira_details).once()
title = await resolve_current_canonical_mr_title(
Expand All @@ -256,6 +257,42 @@ async def jira_details(*_args, **_kwargs):
assert title == "CVE-2026-1234 curl: Fix an overflow"


def test_cve_title_removes_stream_suffix():
assert _normalize_cve_title("CVE-2026-1234 curl: Fix an overflow [rhel-9.9]") == (
"CVE-2026-1234 curl: Fix an overflow"
)
assert _normalize_cve_title("CVE-2026-1234 [rhel-9.9] curl: Fix an overflow") == (
"CVE-2026-1234 [rhel-9.9] curl: Fix an overflow"
)


@pytest.mark.asyncio
async def test_cached_cve_title_removes_existing_stream_suffix():
redis = CanonicalTitleRedis()

await resolve_canonical_mr_title(
redis,
package="curl",
jira_issue="RHEL-100",
jira_summary="CVE-2026-1234 curl: Fix an overflow [rhel-9.9]",
cve_id="CVE-2026-1234",
)
key = next(iter(redis.store))
metadata = CachedMRMetadata.model_validate_json(redis.store[key])
metadata.title = "CVE-2026-1234 curl: Fix an overflow [rhel-9.9]"
redis.store[key] = metadata.model_dump_json()

title = await resolve_canonical_mr_title(
redis,
package="curl",
jira_issue="RHEL-101",
jira_summary="CVE-2026-1234 curl: Fix an overflow [rhel-9.9]",
cve_id="CVE-2026-1234",
)

assert title == "CVE-2026-1234 curl: Fix an overflow"


@pytest.mark.asyncio
async def test_current_title_skips_cache_for_multi_family_consolidation():
redis = CanonicalTitleRedis()
Expand Down
Loading