Merge pull request 'chore: fix CI pipeline flakiness by stabilizing test fixtures and assertions' (#11119) from bugfix/m3.6.0-ci-pipeline-flakiness-stabilization into master
CI / lint (push) Successful in 52s
CI / push-validation (push) Successful in 30s
CI / helm (push) Successful in 58s
CI / build (push) Successful in 1m7s
CI / quality (push) Successful in 1m9s
CI / typecheck (push) Successful in 1m15s
CI / security (push) Successful in 1m39s
CI / unit_tests (push) Successful in 6m5s
CI / docker (push) Successful in 1m39s
CI / integration_tests (push) Successful in 10m29s
CI / coverage (push) Successful in 11m46s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Has been cancelled
CI / benchmark-regression (push) Has been cancelled

This commit was merged in pull request #11119.
This commit is contained in:
2026-06-14 21:33:39 +00:00
committed by Forgejo
3 changed files with 124 additions and 6 deletions
@@ -0,0 +1,25 @@
Feature: Memory service clock-advance guard determinism
As a CleverAgents developer
I want the _wait_for_clock_advance() helper to behave deterministically
So that entity-tracking tests never produce spurious timestamp failures on fast CI runners
# This feature verifies the monotonic busy-wait helper introduced in
# features/steps/memory_service_coverage_steps.py to replace the four
# bare time.sleep(0.01) guards that caused intermittent timestamp failures
# on heavily-loaded CI runners (issue #9963).
#
# The helper spins on the monotonic clock until the UTC clock has advanced
# past the supplied `before` timestamp, raising AssertionError if the
# deadline is exceeded.
@mock_only
Scenario: Clock-advance guard raises AssertionError when deadline is exceeded
Given a zero-second clock-advance deadline
When I invoke the clock-advance guard with a future timestamp
Then an AssertionError should be raised mentioning the deadline
@mock_only
Scenario: Clock-advance guard returns successfully once the clock advances
Given the UTC clock will advance on the next check
When I invoke the clock-advance guard with the current timestamp
Then the guard should return without raising
@@ -0,0 +1,77 @@
"""Step definitions for memory_service_clock_wait.feature.
Verifies the _wait_for_clock_advance() helper introduced in
memory_service_coverage_steps.py raises AssertionError when the deadline
is exceeded, and returns normally once the clock actually advances.
"""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from typing import Any
from behave import given, then, when
from features.steps.memory_service_coverage_steps import _wait_for_clock_advance
@given("a zero-second clock-advance deadline")
def step_zero_second_deadline(context: Any) -> None:
"""Store a zero-second deadline for use in subsequent steps."""
context.clock_advance_deadline = 0.0
@when("I invoke the clock-advance guard with a future timestamp")
def step_invoke_guard_with_future_timestamp(context: Any) -> None:
"""Call _wait_for_clock_advance with a timestamp far in the future.
Because the deadline is 0 seconds, the guard should immediately raise
AssertionError — the clock cannot advance past a future timestamp
before the deadline expires.
"""
# Use a timestamp one hour in the future so the guard can never succeed.
future = datetime.now(UTC) + timedelta(hours=1)
context.clock_advance_error = None
try:
_wait_for_clock_advance(future, deadline_secs=context.clock_advance_deadline)
except AssertionError as exc:
context.clock_advance_error = exc
@then("an AssertionError should be raised mentioning the deadline")
def step_assert_error_raised(context: Any) -> None:
"""Verify the guard raised AssertionError with the expected message."""
assert context.clock_advance_error is not None, (
"_wait_for_clock_advance() should have raised AssertionError but did not"
)
assert "Clock did not advance" in str(context.clock_advance_error), (
f"AssertionError message missing expected text: {context.clock_advance_error}"
)
@given("the UTC clock will advance on the next check")
def step_clock_will_advance(context: Any) -> None:
"""Capture the current UTC timestamp; the clock will advance naturally."""
context.before_timestamp = datetime.now(UTC)
@when("I invoke the clock-advance guard with the current timestamp")
def step_invoke_guard_current_timestamp(context: Any) -> None:
"""Call _wait_for_clock_advance with the captured timestamp.
The guard should return normally once the UTC clock advances past
context.before_timestamp, which will happen within milliseconds.
"""
context.clock_advance_error = None
try:
_wait_for_clock_advance(context.before_timestamp, deadline_secs=2.0)
except AssertionError as exc:
context.clock_advance_error = exc
@then("the guard should return without raising")
def step_guard_returns_normally(context: Any) -> None:
"""Verify the guard returned without raising an error."""
assert context.clock_advance_error is None, (
f"_wait_for_clock_advance() raised unexpectedly: {context.clock_advance_error}"
)
@@ -6,7 +6,7 @@ import asyncio
import shutil
import tempfile
import time
from datetime import UTC
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from unittest.mock import patch
@@ -24,6 +24,22 @@ from cleveragents.application.services.memory_service import (
)
def _wait_for_clock_advance(before: datetime, deadline_secs: float = 2.0) -> None:
"""Spin until the UTC clock advances past *before*, bounded by deadline.
Uses the monotonic clock for the deadline check so the guard itself is
immune to wall-clock adjustments. Raises ``AssertionError`` if the UTC
clock has not advanced within ``deadline_secs`` seconds.
"""
deadline = time.monotonic() + deadline_secs
while datetime.now(UTC) <= before:
if time.monotonic() > deadline:
raise AssertionError(
f"Clock did not advance past {before} within {deadline_secs}s"
)
time.sleep(0.001)
@given("a conversation adapter configured to return text values")
def step_adapter_returning_text(context: Any) -> None:
"""Create an adapter that returns buffer strings."""
@@ -437,7 +453,7 @@ def step_have_tracked_file_entity(context: Any, name: str) -> None:
@when("I track the same entity again")
def step_track_same_entity(context: Any) -> None:
"""Track the same entity again to update it."""
time.sleep(0.01) # Small delay to ensure time difference
_wait_for_clock_advance(datetime.now(UTC))
context.tracked_entity = context.memory_service.track_entity(
context.tracked_entity.name, context.tracked_entity.entity_type
)
@@ -504,11 +520,11 @@ def step_memory_service_entities_over_time(context: Any) -> None:
"""Track entities at different times."""
context.memory_service = MemoryService(session_id="time-entity-session")
# Track entities with small delays to ensure different timestamps
# Track entities with clock-advance guards to ensure different timestamps
context.memory_service.track_entity("oldest", EntityType.PROJECT)
time.sleep(0.01)
_wait_for_clock_advance(datetime.now(UTC))
context.memory_service.track_entity("middle", EntityType.PLAN)
time.sleep(0.01)
_wait_for_clock_advance(datetime.now(UTC))
context.memory_service.track_entity("newest", EntityType.FILE)
@@ -763,7 +779,7 @@ def step_track_project_with_initial_metadata(context: Any, name: str) -> None:
@when("I track the same project entity again with additional metadata")
def step_track_same_entity_with_additional_metadata(context: Any) -> None:
"""Track the same entity with additional metadata to trigger update."""
time.sleep(0.01) # Small delay to ensure time difference
_wait_for_clock_advance(datetime.now(UTC))
additional_metadata = {"status": "active", "priority": "high"}
context.tracked_entity = context.memory_service.track_entity(
context.entity_name, EntityType.PROJECT, additional_metadata