From 5e59070988fe33ef1609e3c576e02941399547b5 Mon Sep 17 00:00:00 2001 From: Qingchuan Hao Date: Tue, 30 Sep 2025 10:02:39 +0000 Subject: [PATCH 01/10] add feedback callback --- src/aks-agent/azext_aks_agent/_consts.py | 6 ++++ src/aks-agent/azext_aks_agent/agent/agent.py | 28 +++++++++++++------ .../azext_aks_agent/agent/telemetry.py | 17 +++++++++-- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/aks-agent/azext_aks_agent/_consts.py b/src/aks-agent/azext_aks_agent/_consts.py index dac35a115fc..73ead29eeb0 100644 --- a/src/aks-agent/azext_aks_agent/_consts.py +++ b/src/aks-agent/azext_aks_agent/_consts.py @@ -8,6 +8,12 @@ CONST_AGENT_NAME = "AKS AGENT" CONST_AGENT_NAME_ENV_KEY = "AGENT_NAME" CONST_AGENT_CONFIG_FILE_NAME = "aksAgent.yaml" +CONST_PRIVACY_NOTICE_BANNER_ENV_KEY = "PRIVACY_NOTICE_BANNER" +# Privacy Notice Banner displayed in the format of rich.Console +CONST_PRIVACY_NOTICE_BANNER = ( + "When you send us this feedback, you agree we may combine this information, which might include other diagnostic data, to help improve Microsoft products and services.\n" + "Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. [link=https://go.microsoft.com/fwlink/?LinkId=521839]Privacy Statement[/link]" +) # MCP Integration Constants (ported from previous change) CONST_MCP_BINARY_NAME = "aks-mcp" diff --git a/src/aks-agent/azext_aks_agent/agent/agent.py b/src/aks-agent/azext_aks_agent/agent/agent.py index f8ec41002be..9ccf1c67901 100644 --- a/src/aks-agent/azext_aks_agent/agent/agent.py +++ b/src/aks-agent/azext_aks_agent/agent/agent.py @@ -11,14 +11,16 @@ CONST_AGENT_CONFIG_PATH_DIR_ENV_KEY, CONST_AGENT_NAME, CONST_AGENT_NAME_ENV_KEY, + CONST_PRIVACY_NOTICE_BANNER, + CONST_PRIVACY_NOTICE_BANNER_ENV_KEY, ) from azure.cli.core.api import get_config_dir from azure.cli.core.commands.client_factory import get_subscription_id from knack.util import CLIError +from .error_handler import MCPError from .prompt import AKS_CONTEXT_PROMPT_MCP, AKS_CONTEXT_PROMPT_TRADITIONAL from .telemetry import CLITelemetryClient -from .error_handler import MCPError # NOTE(mainred): holmes leverage the log handler RichHandler to provide colorful, readable and well-formatted logs @@ -151,7 +153,7 @@ def aks_agent( :type use_aks_mcp: bool """ - with CLITelemetryClient(): + with CLITelemetryClient() as telemetry: if sys.version_info < (3, 10): raise CLIError( "Please upgrade the python version to 3.10 or above to use aks agent." @@ -165,6 +167,7 @@ def aks_agent( # Set environment variables for Holmes os.environ[CONST_AGENT_CONFIG_PATH_DIR_ENV_KEY] = get_config_dir() os.environ[CONST_AGENT_NAME_ENV_KEY] = CONST_AGENT_NAME + os.environ[CONST_PRIVACY_NOTICE_BANNER_ENV_KEY] = CONST_PRIVACY_NOTICE_BANNER # Detect and read piped input piped_data = None @@ -265,7 +268,7 @@ def aks_agent( is_mcp_mode = current_mode == "mcp" if interactive: _run_interactive_mode_sync(ai, cmd, resource_group_name, name, - prompt, console, show_tool_output, is_mcp_mode) + prompt, console, show_tool_output, is_mcp_mode, telemetry) else: _run_noninteractive_mode_sync(ai, config, cmd, resource_group_name, name, prompt, console, echo, show_tool_output, is_mcp_mode) @@ -312,13 +315,15 @@ async def _setup_mcp_mode(mcp_manager, config_file: str, model: str, api_key: st :return: Enhanced Holmes configuration :raises: Exception if MCP setup fails """ + import tempfile from pathlib import Path + import yaml - import tempfile from holmes.config import Config + from .config_generator import ConfigurationGenerator - from .user_feedback import ProgressReporter from .error_handler import AgentErrorHandler + from .user_feedback import ProgressReporter # Ensure binary is available (download if needed) if not mcp_manager.is_binary_available() or not mcp_manager.validate_binary_version(): @@ -602,7 +607,7 @@ def _build_aks_context(cluster_name, resource_group_name, subscription_id, is_mc def _run_interactive_mode_sync(ai, cmd, resource_group_name, name, - prompt, console, show_tool_output, is_mcp_mode): + prompt, console, show_tool_output, is_mcp_mode, telemetry): """ Run interactive mode synchronously - no event loop conflicts. @@ -617,6 +622,7 @@ def _run_interactive_mode_sync(ai, cmd, resource_group_name, name, :param console: Console object for output :param show_tool_output: Whether to show tool output :param is_mcp_mode: Whether running in MCP mode (affects prompt selection) + :param telemetry: CLITelemetryClient instance for tracking events """ from holmes.interactive import run_interactive_loop @@ -633,7 +639,8 @@ def _run_interactive_mode_sync(ai, cmd, resource_group_name, name, ai, console, prompt, None, None, show_tool_output=show_tool_output, system_prompt_additions=aks_context, - check_version=False + check_version=False, + feedback_callback=telemetry.track_agent_feedback if telemetry else None ) @@ -653,8 +660,9 @@ def _run_noninteractive_mode_sync(ai, config, cmd, resource_group_name, name, :param show_tool_output: Whether to show tool output :param is_mcp_mode: Whether running in MCP mode (affects prompt selection) """ - import uuid import socket + import uuid + from holmes.core.prompt import build_initial_ask_messages from holmes.plugins.destinations import DestinationType from holmes.plugins.interfaces import Issue @@ -702,10 +710,12 @@ def _setup_traditional_mode_sync(config_file: str, model: str, api_key: str, :param verbose: Enable verbose output :return: Traditional Holmes configuration """ + import tempfile from pathlib import Path + import yaml - import tempfile from holmes.config import Config + from .config_generator import ConfigurationGenerator # Load base config diff --git a/src/aks-agent/azext_aks_agent/agent/telemetry.py b/src/aks-agent/azext_aks_agent/agent/telemetry.py index 581eca87ca1..f73d2cbe3bf 100644 --- a/src/aks-agent/azext_aks_agent/agent/telemetry.py +++ b/src/aks-agent/azext_aks_agent/agent/telemetry.py @@ -9,8 +9,11 @@ import platform from applicationinsights import TelemetryClient -from azure.cli.core.telemetry import (_get_azure_subscription_id, - _get_hash_mac_address, _get_user_agent) +from azure.cli.core.telemetry import ( + _get_azure_subscription_id, + _get_hash_mac_address, + _get_user_agent, +) DEFAULT_INSTRUMENTATION_KEY = "c301e561-daea-42d9-b9d1-65fca4166704" APPLICATIONINSIGHTS_INSTRUMENTATION_KEY_ENV = "APPLICATIONINSIGHTS_INSTRUMENTATION_KEY" @@ -75,3 +78,13 @@ def _get_application_insights_instrumentation_key(self) -> str: return os.getenv( APPLICATIONINSIGHTS_INSTRUMENTATION_KEY_ENV, DEFAULT_INSTRUMENTATION_KEY ) + + def track_agent_feedback(self, feedback): + # NOTE: we should try to avoid importing holmesgpt at the top level to prevent dependency issues + from holmesgpt.core.feedback import Feedback + + # Type hint validation for development purposes + if not isinstance(feedback, Feedback): + raise TypeError(f"Expected Feedback object, got {type(feedback)}") + + self.track("AgentCLIFeedback", properties=feedback.to_dict()) From b6625402e830263bf15d7d23f7f8dd09a51952b1 Mon Sep 17 00:00:00 2001 From: Qingchuan Hao Date: Wed, 1 Oct 2025 06:50:34 +0000 Subject: [PATCH 02/10] bump holmesgpt --- src/aks-agent/HISTORY.rst | 10 ++++++++++ src/aks-agent/azext_aks_agent/_consts.py | 8 ++++++-- src/aks-agent/azext_aks_agent/agent/agent.py | 17 ++++++++++++----- src/aks-agent/setup.py | 6 +++--- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/aks-agent/HISTORY.rst b/src/aks-agent/HISTORY.rst index 1cebda08a46..a09a38a32a1 100644 --- a/src/aks-agent/HISTORY.rst +++ b/src/aks-agent/HISTORY.rst @@ -12,6 +12,16 @@ To release a new version, please select a new version number (usually plus 1 to Pending +++++++ +1.0.0b5 ++++++++ +* Bump holmesgpt to 0.14.3 - Enhanced AI debugging experience and bug fixes + * Added TODO list feature to allows holmes to reliably answers questions it wasn’t able to answer before due to early-stopping + * Fixed mcp server http connection fails when using socks proxy by adding the missing socks dependency + * Fixed gpt-5 temperature bug by upgrading litellm and dropping non-1 values for temperature + * Improved the installation time by removing unnecessary dependencies and move test dependencies to dev dependency group +* Added Feedback slash command Feature to allow users to provide feedback on their experience with the agent performance + + 1.0.0b4 +++++++ * Fix the --aks-mcp flag to allow true/false values. diff --git a/src/aks-agent/azext_aks_agent/_consts.py b/src/aks-agent/azext_aks_agent/_consts.py index 73ead29eeb0..6a9d90d0ca8 100644 --- a/src/aks-agent/azext_aks_agent/_consts.py +++ b/src/aks-agent/azext_aks_agent/_consts.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -# aks agent constants +# Constants to customized holmesgpt CONST_AGENT_CONFIG_PATH_DIR_ENV_KEY = "HOLMES_CONFIGPATH_DIR" CONST_AGENT_NAME = "AKS AGENT" CONST_AGENT_NAME_ENV_KEY = "AGENT_NAME" @@ -11,9 +11,13 @@ CONST_PRIVACY_NOTICE_BANNER_ENV_KEY = "PRIVACY_NOTICE_BANNER" # Privacy Notice Banner displayed in the format of rich.Console CONST_PRIVACY_NOTICE_BANNER = ( - "When you send us this feedback, you agree we may combine this information, which might include other diagnostic data, to help improve Microsoft products and services.\n" + "When you send Microsoft this feedback, you agree we may combine this information, which might include other diagnostic data, to help improve Microsoft products and services. " "Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. [link=https://go.microsoft.com/fwlink/?LinkId=521839]Privacy Statement[/link]" ) +# Holmesgpt leverages prometheus_api_client for prometheus toolsets and introduces bz2 library. +# Before libbz2-dev is bundled into azure cli python by https://github.com/Azure/azure-cli/pull/32163, +# we ignore loading prometheus toolset to avoid loading error of bz2 module. +CONST_DISABLE_PROMETHEUS_TOOLSET_ENV_KEY = "DISABLE_PROMETHEUS_TOOLSET" # MCP Integration Constants (ported from previous change) CONST_MCP_BINARY_NAME = "aks-mcp" diff --git a/src/aks-agent/azext_aks_agent/agent/agent.py b/src/aks-agent/azext_aks_agent/agent/agent.py index 9ccf1c67901..fba6a13bba9 100644 --- a/src/aks-agent/azext_aks_agent/agent/agent.py +++ b/src/aks-agent/azext_aks_agent/agent/agent.py @@ -11,6 +11,7 @@ CONST_AGENT_CONFIG_PATH_DIR_ENV_KEY, CONST_AGENT_NAME, CONST_AGENT_NAME_ENV_KEY, + CONST_DISABLE_PROMETHEUS_TOOLSET_ENV_KEY, CONST_PRIVACY_NOTICE_BANNER, CONST_PRIVACY_NOTICE_BANNER_ENV_KEY, ) @@ -23,6 +24,14 @@ from .telemetry import CLITelemetryClient +# NOTE(mainred): environment variables to disable prometheus toolset loading should be set before importing holmes. +def customize_holmesgpt(): + os.environ[CONST_DISABLE_PROMETHEUS_TOOLSET_ENV_KEY] = "true" + os.environ[CONST_AGENT_CONFIG_PATH_DIR_ENV_KEY] = get_config_dir() + os.environ[CONST_AGENT_NAME_ENV_KEY] = CONST_AGENT_NAME + os.environ[CONST_PRIVACY_NOTICE_BANNER_ENV_KEY] = CONST_PRIVACY_NOTICE_BANNER + + # NOTE(mainred): holmes leverage the log handler RichHandler to provide colorful, readable and well-formatted logs # making the interactive mode more user-friendly. # And we removed exising log handlers to avoid duplicate logs. @@ -158,17 +167,14 @@ def aks_agent( raise CLIError( "Please upgrade the python version to 3.10 or above to use aks agent." ) + # customizing holmesgpt should called before importing holmes + customize_holmesgpt() # Initialize variables interactive = not no_interactive echo = not no_echo_request console = init_log() - # Set environment variables for Holmes - os.environ[CONST_AGENT_CONFIG_PATH_DIR_ENV_KEY] = get_config_dir() - os.environ[CONST_AGENT_NAME_ENV_KEY] = CONST_AGENT_NAME - os.environ[CONST_PRIVACY_NOTICE_BANNER_ENV_KEY] = CONST_PRIVACY_NOTICE_BANNER - # Detect and read piped input piped_data = None if not sys.stdin.isatty(): @@ -747,3 +753,4 @@ def _setup_traditional_mode_sync(config_file: str, model: str, api_key: str, os.unlink(temp_config_path) except OSError: pass + pass diff --git a/src/aks-agent/setup.py b/src/aks-agent/setup.py index 1206b6f475f..7f24338b92b 100644 --- a/src/aks-agent/setup.py +++ b/src/aks-agent/setup.py @@ -9,7 +9,7 @@ from setuptools import find_packages, setup -VERSION = "1.0.0b4" +VERSION = "1.0.0b5" CLASSIFIERS = [ "Development Status :: 4 - Beta", @@ -24,8 +24,8 @@ ] DEPENDENCIES = [ - "holmesgpt==0.12.6; python_version >= '3.10'", - "pytest-asyncio>=1.1.0", + "holmesgpt @ git+ssh://git@github.com/robusta-dev/holmesgpt@master", + # "holmesgpt==0.14.3; python_version >= '3.10'", ] with open1("README.rst", "r", encoding="utf-8") as f: From 77bcc6035456ef3fbfbffde190165a2f4ef464bf Mon Sep 17 00:00:00 2001 From: Qingchuan Hao Date: Wed, 1 Oct 2025 07:23:19 +0000 Subject: [PATCH 03/10] fix feedback data in custom properties --- src/aks-agent/azext_aks_agent/agent/telemetry.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/aks-agent/azext_aks_agent/agent/telemetry.py b/src/aks-agent/azext_aks_agent/agent/telemetry.py index f73d2cbe3bf..10fb529d811 100644 --- a/src/aks-agent/azext_aks_agent/agent/telemetry.py +++ b/src/aks-agent/azext_aks_agent/agent/telemetry.py @@ -80,11 +80,13 @@ def _get_application_insights_instrumentation_key(self) -> str: ) def track_agent_feedback(self, feedback): - # NOTE: we should try to avoid importing holmesgpt at the top level to prevent dependency issues - from holmesgpt.core.feedback import Feedback + # NOTE: We should try to avoid importing holmesgpt at the top level to prevent dependency issues + from holmes.core.feedback import Feedback # Type hint validation for development purposes if not isinstance(feedback, Feedback): raise TypeError(f"Expected Feedback object, got {type(feedback)}") - self.track("AgentCLIFeedback", properties=feedback.to_dict()) + self.track("AgentCLIFeedback", properties={"feedback": str(feedback.to_dict())}) + # Flush the telemetry data immediately to avoid too much data being sent at once + self.flush() From 69a764e93fa1a1bc8e8858cec8e83ce3768e2e09 Mon Sep 17 00:00:00 2001 From: Qingchuan Hao Date: Wed, 1 Oct 2025 07:43:25 +0000 Subject: [PATCH 04/10] fix lint --- src/aks-agent/azext_aks_agent/_consts.py | 7 +++++-- src/aks-agent/azext_aks_agent/agent/agent.py | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/aks-agent/azext_aks_agent/_consts.py b/src/aks-agent/azext_aks_agent/_consts.py index 6a9d90d0ca8..afb5d025a2c 100644 --- a/src/aks-agent/azext_aks_agent/_consts.py +++ b/src/aks-agent/azext_aks_agent/_consts.py @@ -11,8 +11,11 @@ CONST_PRIVACY_NOTICE_BANNER_ENV_KEY = "PRIVACY_NOTICE_BANNER" # Privacy Notice Banner displayed in the format of rich.Console CONST_PRIVACY_NOTICE_BANNER = ( - "When you send Microsoft this feedback, you agree we may combine this information, which might include other diagnostic data, to help improve Microsoft products and services. " - "Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. [link=https://go.microsoft.com/fwlink/?LinkId=521839]Privacy Statement[/link]" + "When you send Microsoft this feedback, you agree we may combine this information, which might include other " + "diagnostic data, to help improve Microsoft products and services. Processing of feedback data is governed by " + "the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the " + "feedback you submit is considered Personal Data under that addendum. " + "[link=https://go.microsoft.com/fwlink/?LinkId=521839]Privacy Statement[/link]" ) # Holmesgpt leverages prometheus_api_client for prometheus toolsets and introduces bz2 library. # Before libbz2-dev is bundled into azure cli python by https://github.com/Azure/azure-cli/pull/32163, diff --git a/src/aks-agent/azext_aks_agent/agent/agent.py b/src/aks-agent/azext_aks_agent/agent/agent.py index fba6a13bba9..50e079de4c7 100644 --- a/src/aks-agent/azext_aks_agent/agent/agent.py +++ b/src/aks-agent/azext_aks_agent/agent/agent.py @@ -753,4 +753,3 @@ def _setup_traditional_mode_sync(config_file: str, model: str, api_key: str, os.unlink(temp_config_path) except OSError: pass - pass From 9ff5b2348f5bf9d16afdb45a054f4323683d02ac Mon Sep 17 00:00:00 2001 From: Qingchuan Hao Date: Wed, 1 Oct 2025 08:28:07 +0000 Subject: [PATCH 05/10] increase the default max step --- src/aks-agent/azext_aks_agent/_params.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/aks-agent/azext_aks_agent/_params.py b/src/aks-agent/azext_aks_agent/_params.py index d830ab56d02..5d45610c9fb 100644 --- a/src/aks-agent/azext_aks_agent/_params.py +++ b/src/aks-agent/azext_aks_agent/_params.py @@ -6,12 +6,10 @@ # pylint: disable=too-many-statements,too-many-lines import os.path -from azure.cli.core.api import get_config_dir -from azure.cli.core.commands.parameters import get_three_state_flag - from azext_aks_agent._consts import CONST_AGENT_CONFIG_FILE_NAME - from azext_aks_agent._validators import validate_agent_config_file +from azure.cli.core.api import get_config_dir +from azure.cli.core.commands.parameters import get_three_state_flag def load_arguments(self, _): @@ -37,7 +35,7 @@ def load_arguments(self, _): c.argument( "max_steps", type=int, - default=10, + default=40, required=False, help="Maximum number of steps the LLM can take to investigate the issue.", ) From f6f2fb4f8ef12f69097e514d29c1b286b33d79de Mon Sep 17 00:00:00 2001 From: Qingchuan Hao Date: Wed, 1 Oct 2025 10:39:48 +0000 Subject: [PATCH 06/10] serialize the feedback to string --- src/aks-agent/azext_aks_agent/agent/telemetry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/aks-agent/azext_aks_agent/agent/telemetry.py b/src/aks-agent/azext_aks_agent/agent/telemetry.py index 10fb529d811..928466da52e 100644 --- a/src/aks-agent/azext_aks_agent/agent/telemetry.py +++ b/src/aks-agent/azext_aks_agent/agent/telemetry.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- import datetime +import json import logging import os import platform @@ -87,6 +88,6 @@ def track_agent_feedback(self, feedback): if not isinstance(feedback, Feedback): raise TypeError(f"Expected Feedback object, got {type(feedback)}") - self.track("AgentCLIFeedback", properties={"feedback": str(feedback.to_dict())}) + self.track("AgentCLIFeedback", properties={"feedback": json.dumps(feedback.to_dict())}) # Flush the telemetry data immediately to avoid too much data being sent at once self.flush() From 1097b114fad09d0e38020adb19b80db0d36ab6fa Mon Sep 17 00:00:00 2001 From: Qingchuan Hao Date: Wed, 1 Oct 2025 11:07:26 +0000 Subject: [PATCH 07/10] update change history related to promethes toolset disabling --- src/aks-agent/HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aks-agent/HISTORY.rst b/src/aks-agent/HISTORY.rst index a09a38a32a1..c6eaa733db7 100644 --- a/src/aks-agent/HISTORY.rst +++ b/src/aks-agent/HISTORY.rst @@ -20,7 +20,7 @@ Pending * Fixed gpt-5 temperature bug by upgrading litellm and dropping non-1 values for temperature * Improved the installation time by removing unnecessary dependencies and move test dependencies to dev dependency group * Added Feedback slash command Feature to allow users to provide feedback on their experience with the agent performance - +* Disable prometheus toolset loading by default to workaround the libbz2-dev missing issue in Azure CLI python environment. 1.0.0b4 +++++++ From 2f59c5b84dc4a9d8336781919bfd2042351b5a53 Mon Sep 17 00:00:00 2001 From: Qingchuan Hao Date: Fri, 10 Oct 2025 02:59:38 +0000 Subject: [PATCH 08/10] keep user feedback and model in telemetry --- src/aks-agent/azext_aks_agent/_consts.py | 2 +- src/aks-agent/azext_aks_agent/agent/telemetry.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/aks-agent/azext_aks_agent/_consts.py b/src/aks-agent/azext_aks_agent/_consts.py index afb5d025a2c..57e1a5734b9 100644 --- a/src/aks-agent/azext_aks_agent/_consts.py +++ b/src/aks-agent/azext_aks_agent/_consts.py @@ -15,7 +15,7 @@ "diagnostic data, to help improve Microsoft products and services. Processing of feedback data is governed by " "the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the " "feedback you submit is considered Personal Data under that addendum. " - "[link=https://go.microsoft.com/fwlink/?LinkId=521839]Privacy Statement[/link]" + "Privacy Statement: https://go.microsoft.com/fwlink/?LinkId=521839" ) # Holmesgpt leverages prometheus_api_client for prometheus toolsets and introduces bz2 library. # Before libbz2-dev is bundled into azure cli python by https://github.com/Azure/azure-cli/pull/32163, diff --git a/src/aks-agent/azext_aks_agent/agent/telemetry.py b/src/aks-agent/azext_aks_agent/agent/telemetry.py index 928466da52e..08da8790fed 100644 --- a/src/aks-agent/azext_aks_agent/agent/telemetry.py +++ b/src/aks-agent/azext_aks_agent/agent/telemetry.py @@ -82,12 +82,18 @@ def _get_application_insights_instrumentation_key(self) -> str: def track_agent_feedback(self, feedback): # NOTE: We should try to avoid importing holmesgpt at the top level to prevent dependency issues - from holmes.core.feedback import Feedback + from holmes.core.feedback import Feedback, FeedbackMetadata # Type hint validation for development purposes if not isinstance(feedback, Feedback): raise TypeError(f"Expected Feedback object, got {type(feedback)}") - self.track("AgentCLIFeedback", properties={"feedback": json.dumps(feedback.to_dict())}) + # Before privacy team's approval for other user data, we keep only direct user feedback, and model info. + feedback_filtered = Feedback() + feedback_filtered.user_feedback = feedback.user_feedback + feedback_metadata = FeedbackMetadata() + feedback_metadata.model = feedback.metadata.model + feedback_filtered.metadata = feedback_metadata + self.track("AgentCLIFeedback", properties={"feedback": json.dumps(feedback_filtered.to_dict())}) # Flush the telemetry data immediately to avoid too much data being sent at once self.flush() From a90fb5d274bdf8eb1075345ca60b49f06517c339 Mon Sep 17 00:00:00 2001 From: Qingchuan Hao Date: Tue, 14 Oct 2025 01:03:46 +0000 Subject: [PATCH 09/10] bump holmesgpt to 0.15.0 --- src/aks-agent/HISTORY.rst | 4 ++-- src/aks-agent/setup.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/aks-agent/HISTORY.rst b/src/aks-agent/HISTORY.rst index c6eaa733db7..57f3ff8a2da 100644 --- a/src/aks-agent/HISTORY.rst +++ b/src/aks-agent/HISTORY.rst @@ -14,8 +14,8 @@ Pending 1.0.0b5 +++++++ -* Bump holmesgpt to 0.14.3 - Enhanced AI debugging experience and bug fixes - * Added TODO list feature to allows holmes to reliably answers questions it wasn’t able to answer before due to early-stopping +* Bump holmesgpt to 0.15.0 - Enhanced AI debugging experience and bug fixes + * Added TODO list feature to allows holmes to reliably answers questions it wasn't able to answer before due to early-stopping * Fixed mcp server http connection fails when using socks proxy by adding the missing socks dependency * Fixed gpt-5 temperature bug by upgrading litellm and dropping non-1 values for temperature * Improved the installation time by removing unnecessary dependencies and move test dependencies to dev dependency group diff --git a/src/aks-agent/setup.py b/src/aks-agent/setup.py index 7f24338b92b..ee2dbf93b6c 100644 --- a/src/aks-agent/setup.py +++ b/src/aks-agent/setup.py @@ -24,8 +24,7 @@ ] DEPENDENCIES = [ - "holmesgpt @ git+ssh://git@github.com/robusta-dev/holmesgpt@master", - # "holmesgpt==0.14.3; python_version >= '3.10'", + "holmesgpt==0.15.0; python_version >= '3.10'", ] with open1("README.rst", "r", encoding="utf-8") as f: From 5cf2e93f8e774b73af489de5ab61dad8aee14194 Mon Sep 17 00:00:00 2001 From: Qingchuan Hao Date: Thu, 16 Oct 2025 01:47:48 +0000 Subject: [PATCH 10/10] skip async tests --- .../latest/test_aks_agent_mcp_integration.py | 110 +++++---- .../tests/latest/test_aks_agent_status.py | 226 +++++++++--------- 2 files changed, 177 insertions(+), 159 deletions(-) diff --git a/src/aks-agent/azext_aks_agent/tests/latest/test_aks_agent_mcp_integration.py b/src/aks-agent/azext_aks_agent/tests/latest/test_aks_agent_mcp_integration.py index 72a9b68c53e..d62ab2911b1 100644 --- a/src/aks-agent/azext_aks_agent/tests/latest/test_aks_agent_mcp_integration.py +++ b/src/aks-agent/azext_aks_agent/tests/latest/test_aks_agent_mcp_integration.py @@ -9,6 +9,7 @@ import asyncio from unittest.mock import AsyncMock, Mock, patch + import pytest @@ -27,13 +28,13 @@ def setup_method(self): def test_initialize_mcp_manager_success(self): """Test successful MCP manager initialization.""" from azext_aks_agent.agent.agent import _initialize_mcp_manager - + with patch('azext_aks_agent.agent.mcp_manager.MCPManager') as mock_mcp_class: mock_manager = Mock() mock_mcp_class.return_value = mock_manager - + result = _initialize_mcp_manager(verbose=True) - + assert result == mock_manager mock_mcp_class.assert_called_once_with(verbose=True) @@ -41,41 +42,42 @@ def test_initialize_mcp_manager_import_error(self): """Test MCP manager initialization with import error.""" from azext_aks_agent.agent.agent import _initialize_mcp_manager from azext_aks_agent.agent.error_handler import MCPError - + with patch('azext_aks_agent.agent.mcp_manager.MCPManager', side_effect=ImportError("Module not found")): with pytest.raises(MCPError) as exc_info: _initialize_mcp_manager() - + assert "MCP manager initialization failed" in str(exc_info.value) assert exc_info.value.error_code == "MCP_IMPORT" assert "Ensure all required dependencies are installed" in exc_info.value.suggestions[0] + @pytest.mark.skip(reason="The async test is currently not supported in test pipeline.") @pytest.mark.asyncio async def test_setup_mcp_mode_basic_workflow(self): """Test basic MCP mode setup workflow without complex mocking.""" from azext_aks_agent.agent.agent import _setup_mcp_mode from azext_aks_agent.agent.binary_manager import BinaryStatus - + # Create a simple mock manager mock_manager = Mock() mock_manager.is_binary_available.return_value = True mock_manager.validate_binary_version.return_value = True mock_manager.start_server = AsyncMock(return_value=True) mock_manager.get_server_url.return_value = "http://localhost:8003/sse" - + # Mock binary status mock_binary_status = BinaryStatus(available=True, version_valid=True) mock_manager.binary_manager.ensure_binary = AsyncMock(return_value=mock_binary_status) - + # Test with a non-existent config file (will use empty config) with patch('pathlib.Path.exists', return_value=False), \ - patch('tempfile.NamedTemporaryFile') as mock_temp_file, \ - patch('yaml.dump') as mock_yaml_dump, \ - patch('os.unlink'): - + patch('tempfile.NamedTemporaryFile') as mock_temp_file, \ + patch('yaml.dump') as mock_yaml_dump, \ + patch('os.unlink'): + # Mock the temporary file context manager mock_temp_file.return_value.__enter__.return_value.name = "/tmp/test_config.yaml" - + # This should fail because we haven't mocked Holmes Config.load_from_file, # but that's expected - we're just testing the workflow doesn't crash try: @@ -86,113 +88,118 @@ async def test_setup_mcp_mode_basic_workflow(self): except Exception as e: # Expected to fail at Config.load_from_file assert "Config" in str(e) or "load_from_file" in str(e) or "ImportError" in str(e) - + # Verify the manager methods were called correctly mock_manager.start_server.assert_called_once() assert mock_manager.get_server_url.called - + # Check the content of the configuration that was passed to yaml.dump if mock_yaml_dump.call_count > 0: config_data = mock_yaml_dump.call_args[0][0] - + # Verify MCP server configuration is present assert "mcp_servers" in config_data assert "aks-mcp" in config_data["mcp_servers"] assert config_data["mcp_servers"]["aks-mcp"]["url"] == "http://localhost:8003/sse" - + # Verify conflicting toolsets are disabled assert "toolsets" in config_data toolsets = config_data["toolsets"] assert toolsets["aks/core"]["enabled"] is False assert toolsets["kubernetes/core"]["enabled"] is False - + + @pytest.mark.skip(reason="The async test is currently not supported in test pipeline.") @pytest.mark.asyncio async def test_setup_mcp_mode_binary_not_available(self): """Test MCP mode setup when binary is not available and download fails.""" from azext_aks_agent.agent.agent import _setup_mcp_mode from azext_aks_agent.agent.binary_manager import BinaryStatus from azext_aks_agent.agent.error_handler import BinaryError - + # Setup mocks mock_manager = Mock() mock_manager.is_binary_available.return_value = False mock_manager.validate_binary_version.return_value = False - + # Mock failed binary download mock_binary_status = BinaryStatus(available=False, error_message="Download failed") mock_manager.binary_manager.ensure_binary = AsyncMock(return_value=mock_binary_status) - + # Test the function with pytest.raises(BinaryError) as exc_info: await _setup_mcp_mode( mock_manager, self.test_config_file, self.test_model, self.test_api_key, self.test_max_steps, verbose=True ) - + assert "Binary setup failed" in str(exc_info.value) assert exc_info.value.error_code == "BINARY_SETUP" + @pytest.mark.skip(reason="The async test is currently not supported in test pipeline.") @pytest.mark.asyncio async def test_setup_mcp_mode_server_start_failure(self): """Test MCP mode setup when server fails to start.""" from azext_aks_agent.agent.agent import _setup_mcp_mode from azext_aks_agent.agent.binary_manager import BinaryStatus from azext_aks_agent.agent.error_handler import ServerError - + # Setup mocks mock_manager = Mock() mock_manager.is_binary_available.return_value = True mock_manager.validate_binary_version.return_value = True mock_manager.start_server = AsyncMock(return_value=False) - + mock_binary_status = BinaryStatus(available=True, version_valid=True) mock_manager.binary_manager.ensure_binary = AsyncMock(return_value=mock_binary_status) - + # Test the function with pytest.raises(ServerError) as exc_info: await _setup_mcp_mode( mock_manager, self.test_config_file, self.test_model, self.test_api_key, self.test_max_steps, verbose=True ) - + assert "Server startup failed" in str(exc_info.value) assert exc_info.value.error_code == "SERVER_STARTUP" def test_error_handler_functionality(self): """Test the enhanced error handling system.""" from azext_aks_agent.agent.error_handler import ( - AgentErrorHandler, MCPError, BinaryError, ServerError + AgentErrorHandler, + BinaryError, + MCPError, + ServerError, ) - + # Test MCP setup error handling original_error = ConnectionError("Network connection failed") mcp_error = AgentErrorHandler.handle_mcp_setup_error(original_error, "initialization") - + assert isinstance(mcp_error, MCPError) assert "MCP setup failed during initialization" in str(mcp_error) assert mcp_error.error_code == "MCP_SETUP" assert "Check your internet connection" in mcp_error.suggestions - + # Test binary error handling binary_error = AgentErrorHandler.handle_binary_error( Exception("Download timeout"), "download" ) - + assert isinstance(binary_error, BinaryError) assert "Binary download failed" in str(binary_error) assert binary_error.error_code == "BINARY_DOWNLOAD" assert "Verify you have internet connectivity" in binary_error.suggestions - + # Test server error handling server_error = AgentErrorHandler.handle_server_error( Exception("Port in use"), "startup" ) - + assert isinstance(server_error, ServerError) assert "MCP server startup failed" in str(server_error) assert server_error.error_code == "SERVER_STARTUP" assert "Check if the MCP binary is available and executable" in server_error.suggestions - + # Test error message formatting formatted_message = AgentErrorHandler.format_error_message(mcp_error) assert "AKS Agent Error (MCP_SETUP)" in formatted_message @@ -202,9 +209,10 @@ def test_error_handler_functionality(self): def test_setup_traditional_mode_config_loading(self): """Test traditional mode setup with actual config loading.""" import tempfile + import yaml from azext_aks_agent.agent.config_generator import ConfigurationGenerator - + # Create a temporary config file test_config = { "existing": "config", @@ -212,40 +220,40 @@ def test_setup_traditional_mode_config_loading(self): "custom/toolset": {"enabled": True} } } - + with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: yaml.dump(test_config, f) config_file_path = f.name - + try: # Test loading and processing config as dictionary from pathlib import Path - + expanded_config_file = Path(config_file_path) base_config_dict = {} - + if expanded_config_file.exists(): with open(expanded_config_file, 'r') as f: base_config_dict = yaml.safe_load(f) or {} - + # Use ConfigurationGenerator to create traditional config traditional_config_dict = ConfigurationGenerator.generate_traditional_config(base_config_dict) - + # Verify the configuration was processed correctly assert "toolsets" in traditional_config_dict assert "existing" in traditional_config_dict assert traditional_config_dict["existing"] == "config" - + # Verify traditional toolsets are enabled toolsets = traditional_config_dict["toolsets"] assert toolsets["aks/core"]["enabled"] is True assert toolsets["kubernetes/core"]["enabled"] is True assert toolsets["kubernetes/live-metrics"]["enabled"] is True assert toolsets["custom/toolset"]["enabled"] is True - + # Verify no MCP servers are configured assert "mcp_servers" not in traditional_config_dict - + finally: Path(config_file_path).unlink() # Clean up temp file @@ -253,10 +261,10 @@ def test_setup_traditional_mode_config_loading(self): def test_aks_agent_calls_sync_implementation(self, mock_stdin): """Test that aks_agent works with new synchronous implementation.""" from azext_aks_agent.agent.agent import aks_agent - + # Mock stdin to avoid pytest capture issues mock_stdin.isatty.return_value = True # No piped input - + # Call the function with use_aks_mcp=False to avoid MCP setup try: aks_agent( @@ -284,13 +292,13 @@ def test_python_version_check(self, mock_stdin): """Test that agent checks Python version requirement.""" from azext_aks_agent.agent.agent import aks_agent from knack.util import CLIError - - # Mock stdin to avoid pytest capture issues + + # Mock stdin to avoid pytest capture issues mock_stdin.isatty.return_value = True # No piped input - + with patch('azext_aks_agent.agent.agent.sys') as mock_sys: mock_sys.version_info = (3, 9, 0) # Below required version - + with pytest.raises(CLIError) as exc_info: aks_agent( self.mock_cmd, @@ -307,5 +315,5 @@ def test_python_version_check(self, mock_stdin): False, use_aks_mcp=False, ) - + assert "upgrade the python version to 3.10" in str(exc_info.value) diff --git a/src/aks-agent/azext_aks_agent/tests/latest/test_aks_agent_status.py b/src/aks-agent/azext_aks_agent/tests/latest/test_aks_agent_status.py index 995e0efdd4d..5c3c76ff6f6 100644 --- a/src/aks-agent/azext_aks_agent/tests/latest/test_aks_agent_status.py +++ b/src/aks-agent/azext_aks_agent/tests/latest/test_aks_agent_status.py @@ -7,125 +7,133 @@ Unit tests for agent status collection functionality. """ -import os import json +import os import tempfile -import pytest -from unittest.mock import Mock, patch from datetime import datetime, timedelta +from unittest.mock import Mock, patch +import pytest from azext_aks_agent.agent.status import AgentStatusManager -from azext_aks_agent.agent.status_models import AgentStatus, BinaryStatus, ServerStatus, ConfigStatus +from azext_aks_agent.agent.status_models import ( + AgentStatus, + BinaryStatus, + ConfigStatus, + ServerStatus, +) class TestAgentStatusManager: """Test cases for AgentStatusManager.""" - + def setup_method(self): """Set up test fixtures.""" self.temp_dir = tempfile.mkdtemp() self.status_manager = AgentStatusManager(config_dir=self.temp_dir) - + def teardown_method(self): """Clean up test fixtures.""" import shutil shutil.rmtree(self.temp_dir, ignore_errors=True) - + def test_status_manager_init_with_default_config_dir(self): """Test initialization with default config directory.""" with patch('azext_aks_agent.agent.status.get_config_dir') as mock_get_config_dir: mock_get_config_dir.return_value = '/mock/config/dir' - + manager = AgentStatusManager() - + assert manager.config_dir == '/mock/config/dir' mock_get_config_dir.assert_called_once() - + def test_status_manager_init_with_custom_config_dir(self): """Test initialization with custom config directory.""" custom_dir = '/custom/config/dir' manager = AgentStatusManager(config_dir=custom_dir) - + assert manager.config_dir == custom_dir - + + @pytest.mark.skip(reason="The async test is currently not supported in test pipeline.") @pytest.mark.asyncio @patch('azext_aks_agent.agent.status.psutil') async def test_get_status_success(self, mock_psutil): """Test successful status collection.""" # Mock binary manager with patch.object(self.status_manager.binary_manager, 'get_binary_path') as mock_path, \ - patch.object(self.status_manager.binary_manager, 'get_binary_version') as mock_version, \ - patch.object(self.status_manager.binary_manager, 'validate_version') as mock_validate, \ - patch('os.path.exists') as mock_exists: - + patch.object(self.status_manager.binary_manager, 'get_binary_version') as mock_version, \ + patch.object(self.status_manager.binary_manager, 'validate_version') as mock_validate, \ + patch('os.path.exists') as mock_exists: + mock_path.return_value = '/mock/binary/path' mock_version.return_value = '1.0.0' mock_validate.return_value = True mock_exists.return_value = True - + # Mock process info mock_process = Mock() mock_process.create_time.return_value = datetime.now().timestamp() - 3600 # 1 hour ago mock_psutil.Process.return_value = mock_process - + # Mock file stats with patch('os.stat') as mock_stat, \ - patch('os.path.getmtime') as mock_getmtime: - + patch('os.path.getmtime') as mock_getmtime: + mock_stat.return_value.st_size = 1024 mock_stat.return_value.st_mtime = datetime.now().timestamp() mock_getmtime.return_value = datetime.now().timestamp() - + status = await self.status_manager.get_status() - + assert isinstance(status, AgentStatus) assert isinstance(status.mcp_binary, BinaryStatus) assert isinstance(status.server, ServerStatus) assert isinstance(status.config, ConfigStatus) - + + @pytest.mark.skip(reason="The async test is currently not supported in test pipeline.") @pytest.mark.asyncio async def test_get_status_with_error(self): """Test status collection with error.""" # Mock determine_current_mode to raise exception with patch.object(self.status_manager, '_determine_current_mode', side_effect=Exception("Test error")): - + status = await self.status_manager.get_status() - + assert status.mode == "error" assert "Status collection failed" in status.error_message - + def test_get_mcp_binary_status_available(self): """Test MCP binary status when binary is available.""" with patch.object(self.status_manager.binary_manager, 'get_binary_path') as mock_path, \ - patch.object(self.status_manager.binary_manager, 'get_binary_version') as mock_version, \ - patch.object(self.status_manager.binary_manager, 'validate_version') as mock_validate, \ - patch('os.path.exists') as mock_exists, \ - patch('azext_aks_agent.agent.status_models.BinaryStatus.from_file_path') as mock_from_path: - + patch.object(self.status_manager.binary_manager, 'get_binary_version') as mock_version, \ + patch.object(self.status_manager.binary_manager, 'validate_version') as mock_validate, \ + patch('os.path.exists') as mock_exists, \ + patch('azext_aks_agent.agent.status_models.BinaryStatus.from_file_path') as mock_from_path: + mock_path.return_value = '/mock/binary/path' mock_version.return_value = '1.0.0' mock_validate.return_value = True mock_exists.return_value = True - + expected_status = BinaryStatus(available=True, version='1.0.0', version_valid=True) mock_from_path.return_value = expected_status - + result = self.status_manager._get_mcp_binary_status() - + assert result == expected_status mock_from_path.assert_called_once_with('/mock/binary/path', version='1.0.0', version_valid=True) - + def test_get_mcp_binary_status_not_available(self): """Test MCP binary status when binary is not available.""" with patch.object(self.status_manager.binary_manager, 'get_binary_path', return_value='/mock/bin'), \ - patch('os.path.exists', return_value=False): - + patch('os.path.exists', return_value=False): + result = self.status_manager._get_mcp_binary_status() - + assert not result.available assert result.path == '/mock/bin' assert result.error_message == 'Binary not found' - + + @pytest.mark.skip(reason="The async test is currently not supported in test pipeline.") @pytest.mark.asyncio @patch('azext_aks_agent.agent.status.MCPManager') @patch('azext_aks_agent.agent.status.psutil') @@ -139,24 +147,25 @@ async def test_get_server_status_running_healthy(self, mock_psutil, mock_mcp_man mock_manager.get_server_port.return_value = 8003 mock_manager.server_process = Mock() mock_manager.server_process.pid = 12345 - + mock_mcp_manager_class.return_value = mock_manager - + # Mock process info mock_process = Mock() start_time = datetime.now() - timedelta(hours=1) mock_process.create_time.return_value = start_time.timestamp() mock_psutil.Process.return_value = mock_process - + result = await self.status_manager._get_server_status() - + assert result.running assert result.healthy assert result.url == 'http://localhost:8003/sse' assert result.port == 8003 assert result.pid == 12345 assert result.uptime is not None - + + @pytest.mark.skip(reason="The async test is currently not supported in test pipeline.") @pytest.mark.asyncio @patch('azext_aks_agent.agent.status.MCPManager') async def test_get_server_status_not_running(self, mock_mcp_manager_class): @@ -165,34 +174,35 @@ async def test_get_server_status_not_running(self, mock_mcp_manager_class): mock_manager = Mock() mock_manager.is_server_running.return_value = False mock_mcp_manager_class.return_value = mock_manager - + result = await self.status_manager._get_server_status() - + assert not result.running assert not result.healthy assert result.url is None assert result.port is None assert result.pid is None - + + @pytest.mark.skip(reason="The async test is currently not supported in test pipeline.") @pytest.mark.asyncio @patch('azext_aks_agent.agent.status.MCPManager') async def test_get_server_status_with_exception(self, mock_mcp_manager_class): """Test server status collection with exception.""" mock_mcp_manager_class.side_effect = Exception("Test error") - + result = await self.status_manager._get_server_status() - + assert not result.running assert not result.healthy assert "Server status check failed" in result.error_message - + def test_get_configuration_status_mcp_mode(self): """Test configuration status in MCP mode.""" # Create mock state file state_file_path = os.path.join(self.temp_dir, "aks_agent_mode_state") with open(state_file_path, 'w') as f: json.dump({"last_mode": "mcp"}, f) - + # Create mock config file config_file_path = os.path.join(self.temp_dir, "aksAgent.yaml") config_data = { @@ -206,189 +216,189 @@ def test_get_configuration_status_mcp_mode(self): } with open(config_file_path, 'w') as f: json.dump(config_data, f) - + with patch('azext_aks_agent.agent.status.ConfigurationGenerator.validate_mcp_config') as mock_validate: mock_validate.return_value = True - + result = self.status_manager._get_configuration_status() - + assert result.mode == "mcp" assert result.config_valid assert len(result.mcp_servers) == 1 assert "aks-mcp" in result.mcp_servers - + def test_get_configuration_status_traditional_mode(self): """Test configuration status in traditional mode.""" # Create mock state file state_file_path = os.path.join(self.temp_dir, "aks_agent_mode_state") with open(state_file_path, 'w') as f: json.dump({"last_mode": "traditional"}, f) - + with patch('azext_aks_agent.agent.status.ConfigurationGenerator.validate_traditional_config') as mock_validate: mock_validate.return_value = True - + result = self.status_manager._get_configuration_status() - + assert result.mode == "traditional" assert result.config_valid - + def test_get_configuration_status_with_exception(self): """Test configuration status collection with exception.""" with patch('os.path.exists', side_effect=Exception("Test error")): - + result = self.status_manager._get_configuration_status() - + assert result.mode == "unknown" assert not result.config_valid assert "Configuration status check failed" in result.error_message - + def test_determine_current_mode_mcp(self): """Test mode determination for MCP mode.""" config_status = ConfigStatus(mode="mcp") binary_status = BinaryStatus(available=True, version_valid=True) server_status = ServerStatus(running=True) - + result = self.status_manager._determine_current_mode(config_status, binary_status, server_status) - + assert result == "mcp" - + def test_determine_current_mode_traditional(self): """Test mode determination for traditional mode.""" config_status = ConfigStatus(mode="traditional") binary_status = BinaryStatus(available=False) server_status = ServerStatus(running=False) - + result = self.status_manager._determine_current_mode(config_status, binary_status, server_status) - + assert result == "traditional" - + def test_determine_current_mode_inferred_mcp(self): """Test mode determination inferred as MCP from component status.""" config_status = ConfigStatus(mode="unknown") binary_status = BinaryStatus(available=True, version_valid=True) server_status = ServerStatus(running=True) - + result = self.status_manager._determine_current_mode(config_status, binary_status, server_status) - + assert result == "mcp" - + def test_determine_current_mode_mcp_available(self): """Test mode determination for MCP available but server not running.""" config_status = ConfigStatus(mode="unknown") binary_status = BinaryStatus(available=True) server_status = ServerStatus(running=False) - + result = self.status_manager._determine_current_mode(config_status, binary_status, server_status) - + assert result == "mcp_available" - + def test_get_last_mode_from_file(self): """Test getting last mode from state file.""" state_file_path = os.path.join(self.temp_dir, "aks_agent_mode_state") with open(state_file_path, 'w') as f: json.dump({"last_mode": "mcp"}, f) - + result = self.status_manager._get_last_mode() - + assert result == "mcp" - + def test_get_last_mode_no_file(self): """Test getting last mode when file doesn't exist.""" result = self.status_manager._get_last_mode() - + assert result == "unknown" - + def test_get_last_mode_invalid_json(self): """Test getting last mode with invalid JSON in file.""" state_file_path = os.path.join(self.temp_dir, "aks_agent_mode_state") with open(state_file_path, 'w') as f: f.write("invalid json") - + result = self.status_manager._get_last_mode() - + assert result == "unknown" - + def test_get_last_mode_change_time(self): """Test getting last mode change time.""" state_file_path = os.path.join(self.temp_dir, "aks_agent_mode_state") with open(state_file_path, 'w') as f: json.dump({"last_mode": "mcp"}, f) - + result = self.status_manager._get_last_mode_change_time() - + assert result is not None assert isinstance(result, datetime) - + def test_get_last_mode_change_time_no_file(self): """Test getting last mode change time when file doesn't exist.""" result = self.status_manager._get_last_mode_change_time() - + assert result is None - + def test_get_last_used_timestamp(self): """Test getting last used timestamp.""" # Create some files with different timestamps config_file_path = os.path.join(self.temp_dir, "aksAgent.yaml") state_file_path = os.path.join(self.temp_dir, "aks_agent_mode_state") - + with open(config_file_path, 'w') as f: f.write("{}") - + with open(state_file_path, 'w') as f: f.write("{}") - + result = self.status_manager._get_last_used_timestamp() - + assert result is not None assert isinstance(result, datetime) - + def test_get_last_used_timestamp_no_files(self): """Test getting last used timestamp when no files exist.""" result = self.status_manager._get_last_used_timestamp() - + assert result is None - + def test_load_config_file_json(self): """Test loading JSON configuration file.""" config_file_path = os.path.join(self.temp_dir, "test_config.json") config_data = {"test": "data"} - + with open(config_file_path, 'w') as f: json.dump(config_data, f) - + result = self.status_manager._load_config_file(config_file_path) - + assert result == config_data - + def test_load_config_file_yaml(self): """Test loading YAML configuration file.""" config_file_path = os.path.join(self.temp_dir, "test_config.yaml") - + with open(config_file_path, 'w') as f: f.write("test: data\n") - + with patch('yaml.safe_load') as mock_yaml: mock_yaml.return_value = {"test": "data"} - + result = self.status_manager._load_config_file(config_file_path) - + assert result == {"test": "data"} - + def test_load_config_file_nonexistent(self): """Test loading nonexistent configuration file.""" result = self.status_manager._load_config_file("/nonexistent/file.json") - + assert result is None - + def test_load_config_file_invalid_json_falls_back_to_yaml(self): """Test loading invalid JSON configuration file falls back to YAML then fails gracefully.""" config_file_path = os.path.join(self.temp_dir, "invalid.json") - + with open(config_file_path, 'w') as f: f.write("invalid json content") - + # Mock yaml to also fail, simulating no yaml library or invalid yaml with patch('yaml.safe_load', side_effect=Exception("YAML parse error")): result = self.status_manager._load_config_file(config_file_path) - + assert result is None