From 8515abec66510109ae86be202cf92fef4b0c6e05 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 12:07:29 +0000 Subject: [PATCH 1/6] Initial plan From 61891370e4dafc6fa0c774958563a3e2a04a071a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 12:09:52 +0000 Subject: [PATCH 2/6] Add installation date filtering to news display logic Co-authored-by: MischaPanch <35432522+MischaPanch@users.noreply.github.com> --- src/serena/dashboard.py | 32 +++++++ test/serena/test_dashboard.py | 156 ++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 test/serena/test_dashboard.py diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index fdc55656..df72dbda 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -2,6 +2,7 @@ import os import socket import threading from collections.abc import Callable +from datetime import datetime from pathlib import Path from typing import TYPE_CHECKING, Any, Self @@ -142,6 +143,31 @@ class SerenaDashboardAPI: def memory_log_handler(self) -> MemoryLogHandler: return self._memory_log_handler + @staticmethod + def _get_installation_date_id() -> int: + """ + Get the installation date ID (YYYYMMDD format) based on the creation time + of the SERENA_HOME directory. + + :return: Installation date as an integer in YYYYMMDD format + """ + serena_home = SerenaPaths().serena_user_home_dir + + # If SERENA_HOME doesn't exist yet, return today's date + if not os.path.exists(serena_home): + today = datetime.now() + return int(today.strftime("%Y%m%d")) + + # Get the creation/modification time of the SERENA_HOME directory + stat_info = os.stat(serena_home) + # Use st_ctime on Unix-like systems (change time, closest to creation time) + # or st_mtime if ctime is not reliable + creation_timestamp = stat_info.st_ctime + creation_date = datetime.fromtimestamp(creation_timestamp) + + # Return as YYYYMMDD integer + return int(creation_date.strftime("%Y%m%d")) + def _setup_routes(self) -> None: # Static files @self._app.route("/dashboard/") @@ -324,6 +350,12 @@ class SerenaDashboardAPI: def _get_unread_news_ids() -> list[int]: all_news_files = (Path(SERENA_DASHBOARD_DIR) / "news").glob("*.html") all_news_ids = [int(f.stem) for f in all_news_files] + + # Filter news items by installation date + installation_date_id = self._get_installation_date_id() + # Only include news items published on or after the installation date + all_news_ids = [news_id for news_id in all_news_ids if news_id >= installation_date_id] + news_snippet_id_file = SerenaPaths().news_snippet_id_file if not os.path.exists(news_snippet_id_file): return all_news_ids diff --git a/test/serena/test_dashboard.py b/test/serena/test_dashboard.py new file mode 100644 index 00000000..5637b3e4 --- /dev/null +++ b/test/serena/test_dashboard.py @@ -0,0 +1,156 @@ +""" +Tests for the dashboard API, specifically the news display logic. +""" + +import os +import tempfile +from datetime import datetime +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from serena.dashboard import SerenaDashboardAPI + + +class TestNewsDisplayLogic: + """Test the news display logic with installation date filtering.""" + + def test_get_installation_date_id_existing_dir(self): + """Test getting installation date from an existing directory.""" + with tempfile.TemporaryDirectory() as temp_dir: + # Get the installation date ID + with patch("serena.dashboard.SerenaPaths") as mock_paths: + mock_paths_instance = MagicMock() + mock_paths_instance.serena_user_home_dir = temp_dir + mock_paths.return_value = mock_paths_instance + + result = SerenaDashboardAPI._get_installation_date_id() + + # Should return a valid YYYYMMDD format integer + assert isinstance(result, int) + assert 20000101 <= result <= 99991231 # Valid date range + + # Should match today's or recent date (since we just created the directory) + result_str = str(result) + assert len(result_str) == 8 + year = int(result_str[0:4]) + month = int(result_str[4:6]) + day = int(result_str[6:8]) + assert 2020 <= year <= 2030 # Reasonable year range + assert 1 <= month <= 12 + assert 1 <= day <= 31 + + def test_get_installation_date_id_nonexistent_dir(self): + """Test getting installation date when directory doesn't exist yet.""" + with patch("serena.dashboard.SerenaPaths") as mock_paths: + mock_paths_instance = MagicMock() + mock_paths_instance.serena_user_home_dir = "/nonexistent/path/that/does/not/exist" + mock_paths.return_value = mock_paths_instance + + result = SerenaDashboardAPI._get_installation_date_id() + + # Should return today's date + today = datetime.now() + expected = int(today.strftime("%Y%m%d")) + assert result == expected + + def test_news_filtering_by_installation_date(self): + """Test that news items are filtered based on installation date.""" + with tempfile.TemporaryDirectory() as temp_dir: + # Create a mock news directory with some news files + news_dir = Path(temp_dir) / "news" + news_dir.mkdir() + + # Create news files with dates before and after a simulated installation date + old_news_file = news_dir / "20250101.html" + recent_news_file = news_dir / "20260201.html" + future_news_file = news_dir / "20260301.html" + + old_news_file.write_text("
Old news
") + recent_news_file.write_text("
Recent news
") + future_news_file.write_text("
Future news
") + + # Mock the installation date to be 2026-02-01 + with patch.object(SerenaDashboardAPI, "_get_installation_date_id", return_value=20260201): + # Create mocks for the API + mock_memory_log_handler = MagicMock() + mock_agent = MagicMock() + mock_tool_usage_stats = MagicMock() + + api = SerenaDashboardAPI( + memory_log_handler=mock_memory_log_handler, + tool_names=[], + agent=mock_agent, + tool_usage_stats=mock_tool_usage_stats, + ) + + # Mock SERENA_DASHBOARD_DIR to point to our temp directory + with patch("serena.dashboard.SERENA_DASHBOARD_DIR", temp_dir): + # Mock SerenaPaths to return a non-existent news snippet file + # so all news items are considered unread + with patch("serena.dashboard.SerenaPaths") as mock_paths: + mock_paths_instance = MagicMock() + mock_paths_instance.news_snippet_id_file = os.path.join(temp_dir, "nonexistent.txt") + mock_paths.return_value = mock_paths_instance + + # Get the client + client = api._app.test_client() + response = client.get("/news_snippet_ids") + data = response.get_json() + + # Should only include news from 20260201 onwards (not 20250101) + assert data["status"] == "success" + news_ids = data["news_snippet_ids"] + assert 20250101 not in news_ids # Old news filtered out + assert 20260201 in news_ids # Recent news included + assert 20260301 in news_ids # Future news included + + def test_news_filtering_with_read_status(self): + """Test that news items respect both installation date and read status.""" + with tempfile.TemporaryDirectory() as temp_dir: + # Create a mock news directory + news_dir = Path(temp_dir) / "news" + news_dir.mkdir() + + # Create news files + (news_dir / "20260201.html").write_text("
News 1
") + (news_dir / "20260202.html").write_text("
News 2
") + (news_dir / "20260203.html").write_text("
News 3
") + + # Create a news snippet file marking 20260201 as read + news_snippet_file = Path(temp_dir) / "last_read_news_snippet_id.txt" + news_snippet_file.write_text("20260201") + + # Mock the installation date to be 2026-02-01 + with patch.object(SerenaDashboardAPI, "_get_installation_date_id", return_value=20260201): + # Create mocks for the API + mock_memory_log_handler = MagicMock() + mock_agent = MagicMock() + mock_tool_usage_stats = MagicMock() + + api = SerenaDashboardAPI( + memory_log_handler=mock_memory_log_handler, + tool_names=[], + agent=mock_agent, + tool_usage_stats=mock_tool_usage_stats, + ) + + # Mock SERENA_DASHBOARD_DIR and SerenaPaths + with patch("serena.dashboard.SERENA_DASHBOARD_DIR", temp_dir): + with patch("serena.dashboard.SerenaPaths") as mock_paths: + mock_paths_instance = MagicMock() + mock_paths_instance.news_snippet_id_file = str(news_snippet_file) + mock_paths.return_value = mock_paths_instance + + # Get the client + client = api._app.test_client() + response = client.get("/news_snippet_ids") + data = response.get_json() + + # Should only include unread news after installation date + assert data["status"] == "success" + news_ids = data["news_snippet_ids"] + assert 20260201 not in news_ids # Read + assert 20260202 in news_ids # Unread and after installation + assert 20260203 in news_ids # Unread and after installation From 8c6fdda64629c2fa2fb2f403f8110b00413bfd67 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 12:12:17 +0000 Subject: [PATCH 3/6] Fix linting and formatting issues Co-authored-by: MischaPanch <35432522+MischaPanch@users.noreply.github.com> --- src/serena/dashboard.py | 18 +++++++++--------- test/serena/test_dashboard.py | 4 +--- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index df72dbda..190f1b1d 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -2,7 +2,7 @@ import os import socket import threading from collections.abc import Callable -from datetime import datetime +from datetime import UTC, datetime from pathlib import Path from typing import TYPE_CHECKING, Any, Self @@ -148,23 +148,23 @@ class SerenaDashboardAPI: """ Get the installation date ID (YYYYMMDD format) based on the creation time of the SERENA_HOME directory. - + :return: Installation date as an integer in YYYYMMDD format """ serena_home = SerenaPaths().serena_user_home_dir - + # If SERENA_HOME doesn't exist yet, return today's date if not os.path.exists(serena_home): - today = datetime.now() + today = datetime.now(tz=UTC) return int(today.strftime("%Y%m%d")) - + # Get the creation/modification time of the SERENA_HOME directory stat_info = os.stat(serena_home) # Use st_ctime on Unix-like systems (change time, closest to creation time) # or st_mtime if ctime is not reliable creation_timestamp = stat_info.st_ctime - creation_date = datetime.fromtimestamp(creation_timestamp) - + creation_date = datetime.fromtimestamp(creation_timestamp, tz=UTC) + # Return as YYYYMMDD integer return int(creation_date.strftime("%Y%m%d")) @@ -350,12 +350,12 @@ class SerenaDashboardAPI: def _get_unread_news_ids() -> list[int]: all_news_files = (Path(SERENA_DASHBOARD_DIR) / "news").glob("*.html") all_news_ids = [int(f.stem) for f in all_news_files] - + # Filter news items by installation date installation_date_id = self._get_installation_date_id() # Only include news items published on or after the installation date all_news_ids = [news_id for news_id in all_news_ids if news_id >= installation_date_id] - + news_snippet_id_file = SerenaPaths().news_snippet_id_file if not os.path.exists(news_snippet_id_file): return all_news_ids diff --git a/test/serena/test_dashboard.py b/test/serena/test_dashboard.py index 5637b3e4..6b073793 100644 --- a/test/serena/test_dashboard.py +++ b/test/serena/test_dashboard.py @@ -8,8 +8,6 @@ from datetime import datetime from pathlib import Path from unittest.mock import MagicMock, patch -import pytest - from serena.dashboard import SerenaDashboardAPI @@ -30,7 +28,7 @@ class TestNewsDisplayLogic: # Should return a valid YYYYMMDD format integer assert isinstance(result, int) assert 20000101 <= result <= 99991231 # Valid date range - + # Should match today's or recent date (since we just created the directory) result_str = str(result) assert len(result_str) == 8 From 373baef0408779105b0403004c6291c454ec5991 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 12:13:37 +0000 Subject: [PATCH 4/6] Address code review feedback for consistency Co-authored-by: MischaPanch <35432522+MischaPanch@users.noreply.github.com> --- src/serena/dashboard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 190f1b1d..0197a8bd 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -155,7 +155,7 @@ class SerenaDashboardAPI: # If SERENA_HOME doesn't exist yet, return today's date if not os.path.exists(serena_home): - today = datetime.now(tz=UTC) + today = datetime.now(UTC) return int(today.strftime("%Y%m%d")) # Get the creation/modification time of the SERENA_HOME directory @@ -163,7 +163,7 @@ class SerenaDashboardAPI: # Use st_ctime on Unix-like systems (change time, closest to creation time) # or st_mtime if ctime is not reliable creation_timestamp = stat_info.st_ctime - creation_date = datetime.fromtimestamp(creation_timestamp, tz=UTC) + creation_date = datetime.fromtimestamp(creation_timestamp, UTC) # Return as YYYYMMDD integer return int(creation_date.strftime("%Y%m%d")) From eff7cc605a758f1b2b19cbb4bd3358b841891d0b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 12:52:30 +0000 Subject: [PATCH 5/6] Remove test file as requested Co-authored-by: MischaPanch <35432522+MischaPanch@users.noreply.github.com> --- test/serena/test_dashboard.py | 154 ---------------------------------- 1 file changed, 154 deletions(-) delete mode 100644 test/serena/test_dashboard.py diff --git a/test/serena/test_dashboard.py b/test/serena/test_dashboard.py deleted file mode 100644 index 6b073793..00000000 --- a/test/serena/test_dashboard.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -Tests for the dashboard API, specifically the news display logic. -""" - -import os -import tempfile -from datetime import datetime -from pathlib import Path -from unittest.mock import MagicMock, patch - -from serena.dashboard import SerenaDashboardAPI - - -class TestNewsDisplayLogic: - """Test the news display logic with installation date filtering.""" - - def test_get_installation_date_id_existing_dir(self): - """Test getting installation date from an existing directory.""" - with tempfile.TemporaryDirectory() as temp_dir: - # Get the installation date ID - with patch("serena.dashboard.SerenaPaths") as mock_paths: - mock_paths_instance = MagicMock() - mock_paths_instance.serena_user_home_dir = temp_dir - mock_paths.return_value = mock_paths_instance - - result = SerenaDashboardAPI._get_installation_date_id() - - # Should return a valid YYYYMMDD format integer - assert isinstance(result, int) - assert 20000101 <= result <= 99991231 # Valid date range - - # Should match today's or recent date (since we just created the directory) - result_str = str(result) - assert len(result_str) == 8 - year = int(result_str[0:4]) - month = int(result_str[4:6]) - day = int(result_str[6:8]) - assert 2020 <= year <= 2030 # Reasonable year range - assert 1 <= month <= 12 - assert 1 <= day <= 31 - - def test_get_installation_date_id_nonexistent_dir(self): - """Test getting installation date when directory doesn't exist yet.""" - with patch("serena.dashboard.SerenaPaths") as mock_paths: - mock_paths_instance = MagicMock() - mock_paths_instance.serena_user_home_dir = "/nonexistent/path/that/does/not/exist" - mock_paths.return_value = mock_paths_instance - - result = SerenaDashboardAPI._get_installation_date_id() - - # Should return today's date - today = datetime.now() - expected = int(today.strftime("%Y%m%d")) - assert result == expected - - def test_news_filtering_by_installation_date(self): - """Test that news items are filtered based on installation date.""" - with tempfile.TemporaryDirectory() as temp_dir: - # Create a mock news directory with some news files - news_dir = Path(temp_dir) / "news" - news_dir.mkdir() - - # Create news files with dates before and after a simulated installation date - old_news_file = news_dir / "20250101.html" - recent_news_file = news_dir / "20260201.html" - future_news_file = news_dir / "20260301.html" - - old_news_file.write_text("
Old news
") - recent_news_file.write_text("
Recent news
") - future_news_file.write_text("
Future news
") - - # Mock the installation date to be 2026-02-01 - with patch.object(SerenaDashboardAPI, "_get_installation_date_id", return_value=20260201): - # Create mocks for the API - mock_memory_log_handler = MagicMock() - mock_agent = MagicMock() - mock_tool_usage_stats = MagicMock() - - api = SerenaDashboardAPI( - memory_log_handler=mock_memory_log_handler, - tool_names=[], - agent=mock_agent, - tool_usage_stats=mock_tool_usage_stats, - ) - - # Mock SERENA_DASHBOARD_DIR to point to our temp directory - with patch("serena.dashboard.SERENA_DASHBOARD_DIR", temp_dir): - # Mock SerenaPaths to return a non-existent news snippet file - # so all news items are considered unread - with patch("serena.dashboard.SerenaPaths") as mock_paths: - mock_paths_instance = MagicMock() - mock_paths_instance.news_snippet_id_file = os.path.join(temp_dir, "nonexistent.txt") - mock_paths.return_value = mock_paths_instance - - # Get the client - client = api._app.test_client() - response = client.get("/news_snippet_ids") - data = response.get_json() - - # Should only include news from 20260201 onwards (not 20250101) - assert data["status"] == "success" - news_ids = data["news_snippet_ids"] - assert 20250101 not in news_ids # Old news filtered out - assert 20260201 in news_ids # Recent news included - assert 20260301 in news_ids # Future news included - - def test_news_filtering_with_read_status(self): - """Test that news items respect both installation date and read status.""" - with tempfile.TemporaryDirectory() as temp_dir: - # Create a mock news directory - news_dir = Path(temp_dir) / "news" - news_dir.mkdir() - - # Create news files - (news_dir / "20260201.html").write_text("
News 1
") - (news_dir / "20260202.html").write_text("
News 2
") - (news_dir / "20260203.html").write_text("
News 3
") - - # Create a news snippet file marking 20260201 as read - news_snippet_file = Path(temp_dir) / "last_read_news_snippet_id.txt" - news_snippet_file.write_text("20260201") - - # Mock the installation date to be 2026-02-01 - with patch.object(SerenaDashboardAPI, "_get_installation_date_id", return_value=20260201): - # Create mocks for the API - mock_memory_log_handler = MagicMock() - mock_agent = MagicMock() - mock_tool_usage_stats = MagicMock() - - api = SerenaDashboardAPI( - memory_log_handler=mock_memory_log_handler, - tool_names=[], - agent=mock_agent, - tool_usage_stats=mock_tool_usage_stats, - ) - - # Mock SERENA_DASHBOARD_DIR and SerenaPaths - with patch("serena.dashboard.SERENA_DASHBOARD_DIR", temp_dir): - with patch("serena.dashboard.SerenaPaths") as mock_paths: - mock_paths_instance = MagicMock() - mock_paths_instance.news_snippet_id_file = str(news_snippet_file) - mock_paths.return_value = mock_paths_instance - - # Get the client - client = api._app.test_client() - response = client.get("/news_snippet_ids") - data = response.get_json() - - # Should only include unread news after installation date - assert data["status"] == "success" - news_ids = data["news_snippet_ids"] - assert 20260201 not in news_ids # Read - assert 20260202 in news_ids # Unread and after installation - assert 20260203 in news_ids # Unread and after installation From 46d3f11427c82be9c318007127abaebe011aaec8 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 7 Feb 2026 20:23:18 +0100 Subject: [PATCH 6/6] Human > AI (for now) --- src/serena/config/serena_config.py | 15 ++++++++++- src/serena/dashboard.py | 43 +++++++++--------------------- 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 622fbf8b..5f6b3fe6 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -8,7 +8,7 @@ import shutil from collections.abc import Iterator, Sequence from copy import deepcopy from dataclasses import dataclass, field -from datetime import datetime +from datetime import UTC, datetime from enum import Enum from functools import cached_property from pathlib import Path @@ -511,6 +511,19 @@ class SerenaConfig(ToolInclusionDefinition, ModeSelectionDefinition, ToStringMix CONFIG_FIELDS_WITH_TYPE_CONVERSION = {"projects", "language_backend"} # *** methods *** + @classmethod + def get_config_file_creation_date(cls) -> datetime | None: + """ + :return: the creation date of the configuration file, or None if the configuration file does not exist + """ + config_file_path = cls._determine_config_file_path() + if not os.path.exists(config_file_path): + return None + + # for unix systems st_ctime is the inode change time (change of metadata), + # which is good enough for our purposes + creation_timestamp = os.stat(config_file_path).st_ctime + return datetime.fromtimestamp(creation_timestamp, UTC) @property def config_file_path(self) -> str | None: diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 0197a8bd..dca3d730 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -2,7 +2,6 @@ import os import socket import threading from collections.abc import Callable -from datetime import UTC, datetime from pathlib import Path from typing import TYPE_CHECKING, Any, Self @@ -11,7 +10,7 @@ from pydantic import BaseModel from sensai.util import logging from serena.analytics import ToolUsageStats -from serena.config.serena_config import LanguageBackend, SerenaPaths +from serena.config.serena_config import LanguageBackend, SerenaConfig, SerenaPaths from serena.constants import SERENA_DASHBOARD_DIR from serena.task_executor import TaskExecutor from serena.util.logging import MemoryLogHandler @@ -143,31 +142,6 @@ class SerenaDashboardAPI: def memory_log_handler(self) -> MemoryLogHandler: return self._memory_log_handler - @staticmethod - def _get_installation_date_id() -> int: - """ - Get the installation date ID (YYYYMMDD format) based on the creation time - of the SERENA_HOME directory. - - :return: Installation date as an integer in YYYYMMDD format - """ - serena_home = SerenaPaths().serena_user_home_dir - - # If SERENA_HOME doesn't exist yet, return today's date - if not os.path.exists(serena_home): - today = datetime.now(UTC) - return int(today.strftime("%Y%m%d")) - - # Get the creation/modification time of the SERENA_HOME directory - stat_info = os.stat(serena_home) - # Use st_ctime on Unix-like systems (change time, closest to creation time) - # or st_mtime if ctime is not reliable - creation_timestamp = stat_info.st_ctime - creation_date = datetime.fromtimestamp(creation_timestamp, UTC) - - # Return as YYYYMMDD integer - return int(creation_date.strftime("%Y%m%d")) - def _setup_routes(self) -> None: # Static files @self._app.route("/dashboard/") @@ -350,18 +324,25 @@ class SerenaDashboardAPI: def _get_unread_news_ids() -> list[int]: all_news_files = (Path(SERENA_DASHBOARD_DIR) / "news").glob("*.html") all_news_ids = [int(f.stem) for f in all_news_files] + """News ids are ints of format YYYYMMDD (publication dates)""" # Filter news items by installation date - installation_date_id = self._get_installation_date_id() + serena_config_creation_date = SerenaConfig.get_config_file_creation_date() + if serena_config_creation_date is None: + # should not normally happen, since config file should exist when the dashboard is started + # We assume a fresh installation in this case + log.error("Serena config file not found when starting the dashboard") + return [] + serena_config_creation_date_int = int(serena_config_creation_date.strftime("%Y%m%d")) # Only include news items published on or after the installation date - all_news_ids = [news_id for news_id in all_news_ids if news_id >= installation_date_id] + post_installation_news_ids = [news_id for news_id in all_news_ids if news_id >= serena_config_creation_date_int] news_snippet_id_file = SerenaPaths().news_snippet_id_file if not os.path.exists(news_snippet_id_file): - return all_news_ids + return post_installation_news_ids with open(news_snippet_id_file, encoding="utf-8") as f: last_read_news_id = int(f.read().strip()) - return [news_id for news_id in all_news_ids if news_id > last_read_news_id] + return [news_id for news_id in post_installation_news_ids if news_id > last_read_news_id] try: unread_news_ids = _get_unread_news_ids()