hud-python 0.4.45__py3-none-any.whl → 0.5.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hud/__init__.py +27 -7
- hud/agents/__init__.py +70 -5
- hud/agents/base.py +238 -500
- hud/agents/claude.py +236 -247
- hud/agents/gateway.py +42 -0
- hud/agents/gemini.py +264 -0
- hud/agents/gemini_cua.py +324 -0
- hud/agents/grounded_openai.py +98 -100
- hud/agents/misc/integration_test_agent.py +51 -20
- hud/agents/misc/response_agent.py +48 -36
- hud/agents/openai.py +282 -296
- hud/agents/{openai_chat_generic.py → openai_chat.py} +63 -33
- hud/agents/operator.py +199 -0
- hud/agents/resolver.py +70 -0
- hud/agents/tests/conftest.py +133 -0
- hud/agents/tests/test_base.py +300 -622
- hud/agents/tests/test_base_runtime.py +233 -0
- hud/agents/tests/test_claude.py +381 -214
- hud/agents/tests/test_client.py +9 -10
- hud/agents/tests/test_gemini.py +369 -0
- hud/agents/tests/test_grounded_openai_agent.py +65 -50
- hud/agents/tests/test_openai.py +377 -140
- hud/agents/tests/test_operator.py +362 -0
- hud/agents/tests/test_resolver.py +192 -0
- hud/agents/tests/test_run_eval.py +179 -0
- hud/agents/types.py +148 -0
- hud/cli/__init__.py +493 -546
- hud/cli/analyze.py +43 -5
- hud/cli/build.py +699 -113
- hud/cli/debug.py +8 -5
- hud/cli/dev.py +889 -732
- hud/cli/eval.py +793 -667
- hud/cli/flows/dev.py +167 -0
- hud/cli/flows/init.py +191 -0
- hud/cli/flows/tasks.py +153 -56
- hud/cli/flows/templates.py +151 -0
- hud/cli/flows/tests/__init__.py +1 -0
- hud/cli/flows/tests/test_dev.py +126 -0
- hud/cli/init.py +60 -58
- hud/cli/pull.py +1 -1
- hud/cli/push.py +38 -13
- hud/cli/rft.py +311 -0
- hud/cli/rft_status.py +145 -0
- hud/cli/tests/test_analyze.py +5 -5
- hud/cli/tests/test_analyze_metadata.py +3 -2
- hud/cli/tests/test_analyze_module.py +120 -0
- hud/cli/tests/test_build.py +110 -8
- hud/cli/tests/test_build_failure.py +41 -0
- hud/cli/tests/test_build_module.py +50 -0
- hud/cli/tests/test_cli_init.py +6 -1
- hud/cli/tests/test_cli_more_wrappers.py +30 -0
- hud/cli/tests/test_cli_root.py +140 -0
- hud/cli/tests/test_convert.py +361 -0
- hud/cli/tests/test_debug.py +12 -10
- hud/cli/tests/test_dev.py +197 -0
- hud/cli/tests/test_eval.py +251 -0
- hud/cli/tests/test_eval_bedrock.py +51 -0
- hud/cli/tests/test_init.py +124 -0
- hud/cli/tests/test_main_module.py +11 -5
- hud/cli/tests/test_mcp_server.py +12 -100
- hud/cli/tests/test_push.py +1 -1
- hud/cli/tests/test_push_happy.py +74 -0
- hud/cli/tests/test_push_wrapper.py +23 -0
- hud/cli/tests/test_registry.py +1 -1
- hud/cli/tests/test_utils.py +1 -1
- hud/cli/{rl → utils}/celebrate.py +14 -12
- hud/cli/utils/config.py +18 -1
- hud/cli/utils/docker.py +130 -4
- hud/cli/utils/env_check.py +9 -9
- hud/cli/utils/git.py +136 -0
- hud/cli/utils/interactive.py +39 -5
- hud/cli/utils/metadata.py +70 -1
- hud/cli/utils/runner.py +1 -1
- hud/cli/utils/server.py +2 -2
- hud/cli/utils/source_hash.py +3 -3
- hud/cli/utils/tasks.py +4 -1
- hud/cli/utils/tests/__init__.py +0 -0
- hud/cli/utils/tests/test_config.py +58 -0
- hud/cli/utils/tests/test_docker.py +93 -0
- hud/cli/utils/tests/test_docker_hints.py +71 -0
- hud/cli/utils/tests/test_env_check.py +74 -0
- hud/cli/utils/tests/test_environment.py +42 -0
- hud/cli/utils/tests/test_git.py +142 -0
- hud/cli/utils/tests/test_interactive_module.py +60 -0
- hud/cli/utils/tests/test_local_runner.py +50 -0
- hud/cli/utils/tests/test_logging_utils.py +23 -0
- hud/cli/utils/tests/test_metadata.py +49 -0
- hud/cli/utils/tests/test_package_runner.py +35 -0
- hud/cli/utils/tests/test_registry_utils.py +49 -0
- hud/cli/utils/tests/test_remote_runner.py +25 -0
- hud/cli/utils/tests/test_runner_modules.py +52 -0
- hud/cli/utils/tests/test_source_hash.py +36 -0
- hud/cli/utils/tests/test_tasks.py +80 -0
- hud/cli/utils/version_check.py +258 -0
- hud/cli/{rl → utils}/viewer.py +2 -2
- hud/clients/README.md +12 -11
- hud/clients/__init__.py +4 -3
- hud/clients/base.py +166 -26
- hud/clients/environment.py +51 -0
- hud/clients/fastmcp.py +13 -6
- hud/clients/mcp_use.py +45 -15
- hud/clients/tests/test_analyze_scenarios.py +206 -0
- hud/clients/tests/test_protocol.py +9 -3
- hud/datasets/__init__.py +23 -20
- hud/datasets/loader.py +326 -0
- hud/datasets/runner.py +198 -105
- hud/datasets/tests/__init__.py +0 -0
- hud/datasets/tests/test_loader.py +221 -0
- hud/datasets/tests/test_utils.py +315 -0
- hud/datasets/utils.py +270 -90
- hud/environment/__init__.py +52 -0
- hud/environment/connection.py +258 -0
- hud/environment/connectors/__init__.py +33 -0
- hud/environment/connectors/base.py +68 -0
- hud/environment/connectors/local.py +177 -0
- hud/environment/connectors/mcp_config.py +137 -0
- hud/environment/connectors/openai.py +101 -0
- hud/environment/connectors/remote.py +172 -0
- hud/environment/environment.py +835 -0
- hud/environment/integrations/__init__.py +45 -0
- hud/environment/integrations/adk.py +67 -0
- hud/environment/integrations/anthropic.py +196 -0
- hud/environment/integrations/gemini.py +92 -0
- hud/environment/integrations/langchain.py +82 -0
- hud/environment/integrations/llamaindex.py +68 -0
- hud/environment/integrations/openai.py +238 -0
- hud/environment/mock.py +306 -0
- hud/environment/router.py +263 -0
- hud/environment/scenarios.py +620 -0
- hud/environment/tests/__init__.py +1 -0
- hud/environment/tests/test_connection.py +317 -0
- hud/environment/tests/test_connectors.py +205 -0
- hud/environment/tests/test_environment.py +593 -0
- hud/environment/tests/test_integrations.py +257 -0
- hud/environment/tests/test_local_connectors.py +242 -0
- hud/environment/tests/test_scenarios.py +1086 -0
- hud/environment/tests/test_tools.py +208 -0
- hud/environment/types.py +23 -0
- hud/environment/utils/__init__.py +35 -0
- hud/environment/utils/formats.py +215 -0
- hud/environment/utils/schema.py +171 -0
- hud/environment/utils/tool_wrappers.py +113 -0
- hud/eval/__init__.py +67 -0
- hud/eval/context.py +727 -0
- hud/eval/display.py +299 -0
- hud/eval/instrument.py +187 -0
- hud/eval/manager.py +533 -0
- hud/eval/parallel.py +268 -0
- hud/eval/task.py +372 -0
- hud/eval/tests/__init__.py +1 -0
- hud/eval/tests/test_context.py +178 -0
- hud/eval/tests/test_eval.py +210 -0
- hud/eval/tests/test_manager.py +152 -0
- hud/eval/tests/test_parallel.py +168 -0
- hud/eval/tests/test_task.py +291 -0
- hud/eval/types.py +65 -0
- hud/eval/utils.py +194 -0
- hud/patches/__init__.py +19 -0
- hud/patches/mcp_patches.py +308 -0
- hud/patches/warnings.py +54 -0
- hud/samples/browser.py +4 -4
- hud/server/__init__.py +2 -1
- hud/server/low_level.py +2 -1
- hud/server/router.py +164 -0
- hud/server/server.py +567 -80
- hud/server/tests/test_mcp_server_integration.py +11 -11
- hud/server/tests/test_mcp_server_more.py +1 -1
- hud/server/tests/test_server_extra.py +2 -0
- hud/settings.py +45 -3
- hud/shared/exceptions.py +36 -10
- hud/shared/hints.py +26 -1
- hud/shared/requests.py +15 -3
- hud/shared/tests/test_exceptions.py +40 -31
- hud/shared/tests/test_hints.py +167 -0
- hud/telemetry/__init__.py +20 -19
- hud/telemetry/exporter.py +201 -0
- hud/telemetry/instrument.py +165 -253
- hud/telemetry/tests/test_eval_telemetry.py +356 -0
- hud/telemetry/tests/test_exporter.py +258 -0
- hud/telemetry/tests/test_instrument.py +401 -0
- hud/tools/__init__.py +18 -2
- hud/tools/agent.py +223 -0
- hud/tools/apply_patch.py +639 -0
- hud/tools/base.py +54 -4
- hud/tools/bash.py +2 -2
- hud/tools/computer/__init__.py +36 -3
- hud/tools/computer/anthropic.py +2 -2
- hud/tools/computer/gemini.py +385 -0
- hud/tools/computer/hud.py +23 -6
- hud/tools/computer/openai.py +20 -21
- hud/tools/computer/qwen.py +434 -0
- hud/tools/computer/settings.py +37 -0
- hud/tools/edit.py +3 -7
- hud/tools/executors/base.py +4 -2
- hud/tools/executors/pyautogui.py +1 -1
- hud/tools/grounding/grounded_tool.py +13 -18
- hud/tools/grounding/grounder.py +10 -31
- hud/tools/grounding/tests/test_grounded_tool.py +26 -44
- hud/tools/jupyter.py +330 -0
- hud/tools/playwright.py +18 -3
- hud/tools/shell.py +308 -0
- hud/tools/tests/test_agent_tool.py +355 -0
- hud/tools/tests/test_apply_patch.py +718 -0
- hud/tools/tests/test_computer.py +4 -9
- hud/tools/tests/test_computer_actions.py +24 -2
- hud/tools/tests/test_jupyter_tool.py +181 -0
- hud/tools/tests/test_shell.py +596 -0
- hud/tools/tests/test_submit.py +85 -0
- hud/tools/tests/test_types.py +193 -0
- hud/tools/types.py +21 -1
- hud/types.py +194 -56
- hud/utils/__init__.py +2 -0
- hud/utils/env.py +67 -0
- hud/utils/hud_console.py +89 -18
- hud/utils/mcp.py +15 -58
- hud/utils/strict_schema.py +162 -0
- hud/utils/tests/test_init.py +1 -2
- hud/utils/tests/test_mcp.py +1 -28
- hud/utils/tests/test_pretty_errors.py +186 -0
- hud/utils/tests/test_tool_shorthand.py +154 -0
- hud/utils/tests/test_version.py +1 -1
- hud/utils/types.py +20 -0
- hud/version.py +1 -1
- hud_python-0.5.13.dist-info/METADATA +264 -0
- hud_python-0.5.13.dist-info/RECORD +305 -0
- {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/WHEEL +1 -1
- hud/agents/langchain.py +0 -261
- hud/agents/lite_llm.py +0 -72
- hud/cli/rl/__init__.py +0 -180
- hud/cli/rl/config.py +0 -101
- hud/cli/rl/display.py +0 -133
- hud/cli/rl/gpu.py +0 -63
- hud/cli/rl/gpu_utils.py +0 -321
- hud/cli/rl/local_runner.py +0 -595
- hud/cli/rl/presets.py +0 -96
- hud/cli/rl/remote_runner.py +0 -463
- hud/cli/rl/rl_api.py +0 -150
- hud/cli/rl/vllm.py +0 -177
- hud/cli/rl/wait_utils.py +0 -89
- hud/datasets/parallel.py +0 -687
- hud/misc/__init__.py +0 -1
- hud/misc/claude_plays_pokemon.py +0 -292
- hud/otel/__init__.py +0 -35
- hud/otel/collector.py +0 -142
- hud/otel/config.py +0 -181
- hud/otel/context.py +0 -570
- hud/otel/exporters.py +0 -369
- hud/otel/instrumentation.py +0 -135
- hud/otel/processors.py +0 -121
- hud/otel/tests/__init__.py +0 -1
- hud/otel/tests/test_processors.py +0 -197
- hud/rl/README.md +0 -30
- hud/rl/__init__.py +0 -1
- hud/rl/actor.py +0 -176
- hud/rl/buffer.py +0 -405
- hud/rl/chat_template.jinja +0 -101
- hud/rl/config.py +0 -192
- hud/rl/distributed.py +0 -132
- hud/rl/learner.py +0 -637
- hud/rl/tests/__init__.py +0 -1
- hud/rl/tests/test_learner.py +0 -186
- hud/rl/train.py +0 -382
- hud/rl/types.py +0 -101
- hud/rl/utils/start_vllm_server.sh +0 -30
- hud/rl/utils.py +0 -524
- hud/rl/vllm_adapter.py +0 -143
- hud/telemetry/job.py +0 -352
- hud/telemetry/replay.py +0 -74
- hud/telemetry/tests/test_replay.py +0 -40
- hud/telemetry/tests/test_trace.py +0 -63
- hud/telemetry/trace.py +0 -158
- hud/utils/agent_factories.py +0 -86
- hud/utils/async_utils.py +0 -65
- hud/utils/group_eval.py +0 -223
- hud/utils/progress.py +0 -149
- hud/utils/tasks.py +0 -127
- hud/utils/tests/test_async_utils.py +0 -173
- hud/utils/tests/test_progress.py +0 -261
- hud_python-0.4.45.dist-info/METADATA +0 -552
- hud_python-0.4.45.dist-info/RECORD +0 -228
- {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/licenses/LICENSE +0 -0
hud/cli/rft_status.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from hud.cli.utils.viewer import show_json_interactive
|
|
10
|
+
from hud.settings import settings
|
|
11
|
+
from hud.utils.hud_console import HUDConsole
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
console = Console()
|
|
15
|
+
hud_console = HUDConsole()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def rft_status_command(
|
|
19
|
+
model_id: str,
|
|
20
|
+
verbose: bool = False,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Check the status of an RFT training job.
|
|
24
|
+
"""
|
|
25
|
+
# hud_console.header("RFT Job Status")
|
|
26
|
+
|
|
27
|
+
# Preflight check: API key
|
|
28
|
+
if not settings.api_key:
|
|
29
|
+
hud_console.error("HUD_API_KEY not found in environment.")
|
|
30
|
+
hud_console.info("Run 'hud set HUD_API_KEY=...' or export it.")
|
|
31
|
+
raise typer.Exit(1)
|
|
32
|
+
|
|
33
|
+
# Prepare request
|
|
34
|
+
base_url = settings.hud_rl_url
|
|
35
|
+
url = f"{base_url}/training/jobs/{model_id}/raw-status"
|
|
36
|
+
|
|
37
|
+
headers = {"Authorization": f"Bearer {settings.api_key}"}
|
|
38
|
+
|
|
39
|
+
hud_console.info(f"Fetching status for model: {model_id}")
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
with httpx.Client(timeout=30.0) as client:
|
|
43
|
+
resp = client.get(url, headers=headers)
|
|
44
|
+
|
|
45
|
+
if resp.status_code >= 400:
|
|
46
|
+
try:
|
|
47
|
+
detail = resp.json()
|
|
48
|
+
except Exception as e:
|
|
49
|
+
detail = f"{resp.text} - {e}"
|
|
50
|
+
hud_console.error(f"Request failed ({resp.status_code}): {detail}")
|
|
51
|
+
raise typer.Exit(1)
|
|
52
|
+
|
|
53
|
+
data = resp.json()
|
|
54
|
+
|
|
55
|
+
# Display status information
|
|
56
|
+
status = data.get("status", "Unknown")
|
|
57
|
+
|
|
58
|
+
# Show status with appropriate styling
|
|
59
|
+
if status.lower() in ["succeeded", "completed"]:
|
|
60
|
+
hud_console.success(f"Job Status: {status}")
|
|
61
|
+
elif status.lower() in ["failed", "error", "cancelled"]:
|
|
62
|
+
hud_console.error(f"Job Status: {status}")
|
|
63
|
+
elif status.lower() in [
|
|
64
|
+
"running",
|
|
65
|
+
"in_progress",
|
|
66
|
+
"processing",
|
|
67
|
+
"validating_files",
|
|
68
|
+
"queued",
|
|
69
|
+
]:
|
|
70
|
+
hud_console.info(f"Job Status: {status} 🔄")
|
|
71
|
+
else:
|
|
72
|
+
hud_console.info(f"Job Status: {status}")
|
|
73
|
+
|
|
74
|
+
# Most important: Show fine-tuned model if available
|
|
75
|
+
if data.get("fine_tuned_model"):
|
|
76
|
+
hud_console.success(f"Fine-tuned Model: {data['fine_tuned_model']}")
|
|
77
|
+
console.print("\n[dim]You can now use this model in your applications![/dim]")
|
|
78
|
+
|
|
79
|
+
# Display full response in verbose mode or interactive viewer
|
|
80
|
+
if verbose:
|
|
81
|
+
hud_console.section_title("Full Status Details")
|
|
82
|
+
show_json_interactive(data, title="RFT Job Status", initial_expanded=True)
|
|
83
|
+
else:
|
|
84
|
+
# Show key information
|
|
85
|
+
if "model" in data:
|
|
86
|
+
hud_console.info(f"Base Model: {data['model']}")
|
|
87
|
+
|
|
88
|
+
if "created_at" in data:
|
|
89
|
+
# Convert timestamp to readable format if it's a unix timestamp
|
|
90
|
+
created = data["created_at"]
|
|
91
|
+
if isinstance(created, int) and created > 1000000000:
|
|
92
|
+
from datetime import datetime
|
|
93
|
+
|
|
94
|
+
created_str = datetime.fromtimestamp(created).strftime("%Y-%m-%d %H:%M:%S")
|
|
95
|
+
hud_console.info(f"Created: {created_str}")
|
|
96
|
+
else:
|
|
97
|
+
hud_console.info(f"Created: {created}")
|
|
98
|
+
|
|
99
|
+
if data.get("finished_at"):
|
|
100
|
+
finished = data["finished_at"]
|
|
101
|
+
if isinstance(finished, int) and finished > 1000000000:
|
|
102
|
+
from datetime import datetime
|
|
103
|
+
|
|
104
|
+
finished_str = datetime.fromtimestamp(finished).strftime(
|
|
105
|
+
"%Y-%m-%d %H:%M:%S"
|
|
106
|
+
)
|
|
107
|
+
hud_console.info(f"Finished: {finished_str}")
|
|
108
|
+
else:
|
|
109
|
+
hud_console.info(f"Finished: {finished}")
|
|
110
|
+
|
|
111
|
+
if data.get("trained_tokens"):
|
|
112
|
+
hud_console.info(f"Trained Tokens: {data['trained_tokens']:,}")
|
|
113
|
+
|
|
114
|
+
if (
|
|
115
|
+
"estimated_finish" in data
|
|
116
|
+
and data["estimated_finish"]
|
|
117
|
+
and data["estimated_finish"] > 0
|
|
118
|
+
):
|
|
119
|
+
from datetime import datetime
|
|
120
|
+
|
|
121
|
+
est_str = datetime.fromtimestamp(data["estimated_finish"]).strftime(
|
|
122
|
+
"%Y-%m-%d %H:%M:%S"
|
|
123
|
+
)
|
|
124
|
+
hud_console.info(f"Estimated Finish: {est_str}")
|
|
125
|
+
|
|
126
|
+
# Only show error if it's actually an error (not empty/null)
|
|
127
|
+
if data.get("error"):
|
|
128
|
+
error = data["error"]
|
|
129
|
+
# Check if it's a real error
|
|
130
|
+
if isinstance(error, dict):
|
|
131
|
+
# Check if any field has actual content
|
|
132
|
+
has_content = any(error.get(k) for k in ["code", "message", "param"])
|
|
133
|
+
if has_content:
|
|
134
|
+
error_msg = error.get("message") or str(error)
|
|
135
|
+
hud_console.error(f"Error: {error_msg}")
|
|
136
|
+
elif isinstance(error, str) and error.strip():
|
|
137
|
+
hud_console.error(f"Error: {error}")
|
|
138
|
+
|
|
139
|
+
# Suggest verbose mode for more details
|
|
140
|
+
console.print("\n[dim]Use --verbose to see full status details[/dim]")
|
|
141
|
+
|
|
142
|
+
except httpx.RequestError as e:
|
|
143
|
+
hud_console.error(f"Connection error: {e}")
|
|
144
|
+
hud_console.info("Is the RL service running?")
|
|
145
|
+
raise typer.Exit(1) from e
|
hud/cli/tests/test_analyze.py
CHANGED
|
@@ -50,7 +50,7 @@ class TestAnalyzeEnvironment:
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
with (
|
|
53
|
-
patch("hud.
|
|
53
|
+
patch("hud.clients.fastmcp.FastMCPHUDClient") as MockClient,
|
|
54
54
|
patch("hud.cli.analyze.console"),
|
|
55
55
|
patch("hud.cli.analyze.display_interactive") as mock_interactive,
|
|
56
56
|
):
|
|
@@ -80,7 +80,7 @@ class TestAnalyzeEnvironment:
|
|
|
80
80
|
async def test_analyze_environment_failure(self) -> None:
|
|
81
81
|
"""Test handling analysis failure."""
|
|
82
82
|
with (
|
|
83
|
-
patch("hud.
|
|
83
|
+
patch("hud.clients.fastmcp.FastMCPHUDClient") as MockClient,
|
|
84
84
|
patch("hud.cli.analyze.console") as mock_console,
|
|
85
85
|
patch("platform.system", return_value="Windows"),
|
|
86
86
|
):
|
|
@@ -119,7 +119,7 @@ class TestAnalyzeEnvironment:
|
|
|
119
119
|
|
|
120
120
|
for output_format in ["json", "markdown", "interactive"]:
|
|
121
121
|
with (
|
|
122
|
-
patch("hud.
|
|
122
|
+
patch("hud.clients.fastmcp.FastMCPHUDClient") as MockClient,
|
|
123
123
|
patch("hud.cli.analyze.console") as mock_console,
|
|
124
124
|
patch("hud.cli.analyze.display_interactive") as mock_interactive,
|
|
125
125
|
patch("hud.cli.analyze.display_markdown") as mock_markdown,
|
|
@@ -163,7 +163,7 @@ class TestAnalyzeWithConfig:
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
with (
|
|
166
|
-
patch("hud.
|
|
166
|
+
patch("hud.clients.fastmcp.FastMCPHUDClient") as MockClient,
|
|
167
167
|
patch("hud.cli.analyze.console"),
|
|
168
168
|
patch("hud.cli.analyze.display_interactive") as mock_interactive,
|
|
169
169
|
):
|
|
@@ -190,7 +190,7 @@ class TestAnalyzeWithConfig:
|
|
|
190
190
|
mock_config = {"server": {"command": "test"}}
|
|
191
191
|
|
|
192
192
|
with (
|
|
193
|
-
patch("hud.
|
|
193
|
+
patch("hud.clients.fastmcp.FastMCPHUDClient") as MockClient,
|
|
194
194
|
patch("hud.cli.analyze.console"),
|
|
195
195
|
):
|
|
196
196
|
# Setup mock client that fails
|
|
@@ -214,6 +214,7 @@ class TestAnalyzeFromMetadata:
|
|
|
214
214
|
|
|
215
215
|
@mock.patch("hud.cli.utils.metadata.check_local_cache")
|
|
216
216
|
@mock.patch("hud.cli.utils.metadata.fetch_lock_from_registry")
|
|
217
|
+
@mock.patch("hud.cli.utils.metadata.hud_console")
|
|
217
218
|
@mock.patch("hud.cli.utils.metadata.console")
|
|
218
219
|
async def test_analyze_not_found(self, mock_console, mock_hud_console, mock_fetch, mock_check):
|
|
219
220
|
"""Test when environment not found anywhere."""
|
|
@@ -222,9 +223,9 @@ class TestAnalyzeFromMetadata:
|
|
|
222
223
|
|
|
223
224
|
await analyze_from_metadata("test/notfound:latest", "json", verbose=False)
|
|
224
225
|
|
|
225
|
-
# Should show error
|
|
226
|
+
# Should show error via hud_console
|
|
226
227
|
mock_hud_console.error.assert_called_with("Environment metadata not found")
|
|
227
|
-
# Should print suggestions
|
|
228
|
+
# Should print suggestions via console
|
|
228
229
|
mock_console.print.assert_called()
|
|
229
230
|
|
|
230
231
|
@mock.patch("hud.cli.utils.metadata.check_local_cache")
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from hud.cli.analyze import (
|
|
9
|
+
analyze_environment,
|
|
10
|
+
analyze_environment_from_config,
|
|
11
|
+
analyze_environment_from_mcp_config,
|
|
12
|
+
display_interactive,
|
|
13
|
+
display_markdown,
|
|
14
|
+
parse_docker_command,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Mark entire module as asyncio to ensure async tests run with pytest-asyncio
|
|
22
|
+
pytestmark = pytest.mark.asyncio
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_parse_docker_command():
|
|
26
|
+
cmd = ["docker", "run", "--rm", "-i", "img"]
|
|
27
|
+
cfg = parse_docker_command(cmd)
|
|
28
|
+
assert cfg == {"local": {"command": "docker", "args": ["run", "--rm", "-i", "img"]}}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.asyncio
|
|
32
|
+
@patch("hud.clients.fastmcp.FastMCPHUDClient")
|
|
33
|
+
@patch("hud.cli.analyze.console")
|
|
34
|
+
async def test_analyze_environment_success_json(mock_console, MockClient):
|
|
35
|
+
client = AsyncMock()
|
|
36
|
+
client.initialize.return_value = None
|
|
37
|
+
client.analyze_environment.return_value = {"tools": [], "resources": []}
|
|
38
|
+
client.shutdown.return_value = None
|
|
39
|
+
MockClient.return_value = client
|
|
40
|
+
|
|
41
|
+
await analyze_environment(["docker", "run", "img"], output_format="json", verbose=False)
|
|
42
|
+
assert client.initialize.awaited
|
|
43
|
+
assert client.analyze_environment.awaited
|
|
44
|
+
assert client.shutdown.awaited
|
|
45
|
+
assert mock_console.print_json.called
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.mark.asyncio
|
|
49
|
+
@patch("hud.clients.fastmcp.FastMCPHUDClient")
|
|
50
|
+
@patch("hud.cli.analyze.console")
|
|
51
|
+
async def test_analyze_environment_failure(mock_console, MockClient):
|
|
52
|
+
client = AsyncMock()
|
|
53
|
+
client.initialize.side_effect = RuntimeError("boom")
|
|
54
|
+
client.shutdown.return_value = None
|
|
55
|
+
MockClient.return_value = client
|
|
56
|
+
|
|
57
|
+
# Should swallow exception and return without raising
|
|
58
|
+
await analyze_environment(["docker", "run", "img"], output_format="json", verbose=True)
|
|
59
|
+
assert client.shutdown.awaited
|
|
60
|
+
assert mock_console.print_json.called is False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_display_interactive_metadata_only(monkeypatch):
|
|
64
|
+
import hud.cli.analyze as mod
|
|
65
|
+
|
|
66
|
+
monkeypatch.setattr(mod, "console", MagicMock(), raising=False)
|
|
67
|
+
monkeypatch.setattr(mod, "hud_console", MagicMock(), raising=False)
|
|
68
|
+
|
|
69
|
+
analysis = {
|
|
70
|
+
"image": "img:latest",
|
|
71
|
+
"status": "cached",
|
|
72
|
+
"tool_count": 2,
|
|
73
|
+
"tools": [
|
|
74
|
+
{"name": "t1", "description": "d1", "inputSchema": {"type": "object"}},
|
|
75
|
+
{"name": "t2", "description": "d2"},
|
|
76
|
+
],
|
|
77
|
+
"resources": [],
|
|
78
|
+
}
|
|
79
|
+
display_interactive(analysis)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_display_markdown_both_paths(capsys):
|
|
83
|
+
# metadata-only
|
|
84
|
+
md_only = {"image": "img:latest", "tool_count": 0, "tools": [], "resources": []}
|
|
85
|
+
display_markdown(md_only)
|
|
86
|
+
|
|
87
|
+
# live metadata
|
|
88
|
+
live = {"metadata": {"servers": ["s1"], "initialized": True}, "tools": [], "resources": []}
|
|
89
|
+
display_markdown(live)
|
|
90
|
+
|
|
91
|
+
# Check that output was generated
|
|
92
|
+
captured = capsys.readouterr()
|
|
93
|
+
assert "MCP Environment Analysis" in captured.out
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@patch("hud.clients.fastmcp.FastMCPHUDClient")
|
|
97
|
+
async def test_analyze_environment_from_config(MockClient, tmp_path: Path):
|
|
98
|
+
client = AsyncMock()
|
|
99
|
+
client.initialize.return_value = None
|
|
100
|
+
client.analyze_environment.return_value = {"tools": [], "resources": []}
|
|
101
|
+
client.shutdown.return_value = None
|
|
102
|
+
MockClient.return_value = client
|
|
103
|
+
|
|
104
|
+
cfg = tmp_path / "mcp.json"
|
|
105
|
+
cfg.write_text('{"local": {"command": "docker", "args": ["run", "img"]}}')
|
|
106
|
+
await analyze_environment_from_config(cfg, output_format="json", verbose=False)
|
|
107
|
+
assert client.initialize.awaited and client.shutdown.awaited
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@patch("hud.clients.fastmcp.FastMCPHUDClient")
|
|
111
|
+
async def test_analyze_environment_from_mcp_config(MockClient):
|
|
112
|
+
client = AsyncMock()
|
|
113
|
+
client.initialize.return_value = None
|
|
114
|
+
client.analyze_environment.return_value = {"tools": [], "resources": []}
|
|
115
|
+
client.shutdown.return_value = None
|
|
116
|
+
MockClient.return_value = client
|
|
117
|
+
|
|
118
|
+
mcp_config = {"local": {"command": "docker", "args": ["run", "img"]}}
|
|
119
|
+
await analyze_environment_from_mcp_config(mcp_config, output_format="json", verbose=False)
|
|
120
|
+
assert client.initialize.awaited and client.shutdown.awaited
|
hud/cli/tests/test_build.py
CHANGED
|
@@ -60,12 +60,12 @@ class TestIncrementVersion:
|
|
|
60
60
|
def test_increment_minor(self):
|
|
61
61
|
"""Test incrementing minor version."""
|
|
62
62
|
assert increment_version("1.2.3", "minor") == "1.3.0"
|
|
63
|
-
assert increment_version("0.5.
|
|
63
|
+
assert increment_version("0.5.13", "minor") == "0.6.0"
|
|
64
64
|
|
|
65
65
|
def test_increment_major(self):
|
|
66
66
|
"""Test incrementing major version."""
|
|
67
67
|
assert increment_version("1.2.3", "major") == "2.0.0"
|
|
68
|
-
assert increment_version("0.5.
|
|
68
|
+
assert increment_version("0.5.13", "major") == "1.0.0"
|
|
69
69
|
|
|
70
70
|
def test_increment_with_v_prefix(self):
|
|
71
71
|
"""Test incrementing version with v prefix."""
|
|
@@ -206,7 +206,7 @@ RUN pip install fastmcp
|
|
|
206
206
|
class TestAnalyzeMcpEnvironment:
|
|
207
207
|
"""Test analyzing MCP environment."""
|
|
208
208
|
|
|
209
|
-
@mock.patch("hud.
|
|
209
|
+
@mock.patch("hud.clients.fastmcp.FastMCPHUDClient")
|
|
210
210
|
async def test_analyze_success(self, mock_client_class):
|
|
211
211
|
"""Test successful environment analysis."""
|
|
212
212
|
# Setup mock client
|
|
@@ -219,6 +219,17 @@ class TestAnalyzeMcpEnvironment:
|
|
|
219
219
|
mock_tool.description = "Test tool"
|
|
220
220
|
mock_tool.inputSchema = {"type": "object"}
|
|
221
221
|
|
|
222
|
+
# Prefer analyze_environment path (aligns with analyze CLI tests)
|
|
223
|
+
mock_client.analyze_environment = mock.AsyncMock(
|
|
224
|
+
return_value={
|
|
225
|
+
"metadata": {"servers": ["local"], "initialized": True},
|
|
226
|
+
"tools": [{"name": "test_tool", "description": "Test tool"}],
|
|
227
|
+
"hub_tools": {},
|
|
228
|
+
"resources": [],
|
|
229
|
+
"telemetry": {},
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
# Fallback still defined for completeness
|
|
222
233
|
mock_client.list_tools.return_value = [mock_tool]
|
|
223
234
|
|
|
224
235
|
result = await analyze_mcp_environment("test:latest")
|
|
@@ -229,7 +240,7 @@ class TestAnalyzeMcpEnvironment:
|
|
|
229
240
|
assert result["tools"][0]["name"] == "test_tool"
|
|
230
241
|
assert "initializeMs" in result
|
|
231
242
|
|
|
232
|
-
@mock.patch("hud.
|
|
243
|
+
@mock.patch("hud.clients.fastmcp.FastMCPHUDClient")
|
|
233
244
|
async def test_analyze_failure(self, mock_client_class):
|
|
234
245
|
"""Test failed environment analysis."""
|
|
235
246
|
# Setup mock client to fail
|
|
@@ -237,14 +248,25 @@ class TestAnalyzeMcpEnvironment:
|
|
|
237
248
|
mock_client_class.return_value = mock_client
|
|
238
249
|
mock_client.initialize.side_effect = ConnectionError("Connection failed")
|
|
239
250
|
|
|
240
|
-
|
|
251
|
+
from hud.shared.exceptions import HudException
|
|
252
|
+
|
|
253
|
+
with pytest.raises(HudException, match="Connection failed"):
|
|
241
254
|
await analyze_mcp_environment("test:latest")
|
|
242
255
|
|
|
243
|
-
@mock.patch("hud.
|
|
256
|
+
@mock.patch("hud.clients.fastmcp.FastMCPHUDClient")
|
|
244
257
|
async def test_analyze_verbose_mode(self, mock_client_class):
|
|
245
258
|
"""Test analysis in verbose mode."""
|
|
246
259
|
mock_client = mock.AsyncMock()
|
|
247
260
|
mock_client_class.return_value = mock_client
|
|
261
|
+
mock_client.analyze_environment = mock.AsyncMock(
|
|
262
|
+
return_value={
|
|
263
|
+
"metadata": {"servers": ["local"], "initialized": True},
|
|
264
|
+
"tools": [],
|
|
265
|
+
"hub_tools": {},
|
|
266
|
+
"resources": [],
|
|
267
|
+
"telemetry": {},
|
|
268
|
+
}
|
|
269
|
+
)
|
|
248
270
|
mock_client.list_tools.return_value = []
|
|
249
271
|
|
|
250
272
|
# Just test that it runs without error in verbose mode
|
|
@@ -312,6 +334,7 @@ class TestBuildEnvironment:
|
|
|
312
334
|
"""Test the main build_environment function."""
|
|
313
335
|
|
|
314
336
|
@mock.patch("hud.cli.build.build_docker_image")
|
|
337
|
+
@mock.patch("hud.cli.build.collect_runtime_metadata")
|
|
315
338
|
@mock.patch("hud.cli.build.analyze_mcp_environment")
|
|
316
339
|
@mock.patch("hud.cli.build.save_to_registry")
|
|
317
340
|
@mock.patch("hud.cli.build.get_docker_image_id")
|
|
@@ -322,6 +345,7 @@ class TestBuildEnvironment:
|
|
|
322
345
|
mock_get_id,
|
|
323
346
|
mock_save_registry,
|
|
324
347
|
mock_analyze,
|
|
348
|
+
mock_collect_runtime,
|
|
325
349
|
mock_build_docker,
|
|
326
350
|
tmp_path,
|
|
327
351
|
):
|
|
@@ -356,6 +380,12 @@ ENV API_KEY
|
|
|
356
380
|
],
|
|
357
381
|
}
|
|
358
382
|
mock_get_id.return_value = "sha256:abc123"
|
|
383
|
+
mock_collect_runtime.return_value = {
|
|
384
|
+
"python": "3.11.6",
|
|
385
|
+
"cuda": None,
|
|
386
|
+
"cudnn": None,
|
|
387
|
+
"pytorch": None,
|
|
388
|
+
}
|
|
359
389
|
|
|
360
390
|
# Mock final rebuild
|
|
361
391
|
mock_result = mock.Mock()
|
|
@@ -363,7 +393,7 @@ ENV API_KEY
|
|
|
363
393
|
mock_run.return_value = mock_result
|
|
364
394
|
|
|
365
395
|
# Run build
|
|
366
|
-
build_environment(str(env_dir), "test
|
|
396
|
+
build_environment(str(env_dir), "test-env:latest")
|
|
367
397
|
|
|
368
398
|
# Check lock file was created
|
|
369
399
|
lock_file = env_dir / "hud.lock.yaml"
|
|
@@ -373,11 +403,83 @@ ENV API_KEY
|
|
|
373
403
|
with open(lock_file) as f:
|
|
374
404
|
lock_data = yaml.safe_load(f)
|
|
375
405
|
|
|
376
|
-
|
|
406
|
+
# Lock file format version
|
|
407
|
+
assert lock_data["version"] == "1.3"
|
|
408
|
+
|
|
409
|
+
assert lock_data["images"]["full"] == "test-env:0.1.0@sha256:abc123"
|
|
410
|
+
assert lock_data["images"]["local"] == "test-env:0.1.0"
|
|
377
411
|
assert lock_data["build"]["version"] == "0.1.0"
|
|
412
|
+
assert lock_data["build"]["baseImage"] == "python:3.11"
|
|
413
|
+
assert lock_data["build"]["platform"] == "linux/amd64"
|
|
378
414
|
assert lock_data["environment"]["toolCount"] == 2
|
|
415
|
+
assert lock_data["environment"]["runtime"]["python"] == "3.11.6"
|
|
379
416
|
assert len(lock_data["tools"]) == 2
|
|
380
417
|
|
|
418
|
+
@mock.patch("hud.cli.build.build_docker_image")
|
|
419
|
+
@mock.patch("hud.cli.build.collect_runtime_metadata")
|
|
420
|
+
@mock.patch("hud.cli.build.analyze_mcp_environment")
|
|
421
|
+
@mock.patch("hud.cli.build.save_to_registry")
|
|
422
|
+
@mock.patch("hud.cli.build.get_docker_image_id")
|
|
423
|
+
@mock.patch("subprocess.run")
|
|
424
|
+
def test_build_environment_internal_tools(
|
|
425
|
+
self,
|
|
426
|
+
mock_run,
|
|
427
|
+
mock_get_id,
|
|
428
|
+
mock_save_registry,
|
|
429
|
+
mock_analyze,
|
|
430
|
+
mock_collect_runtime,
|
|
431
|
+
mock_build_docker,
|
|
432
|
+
tmp_path,
|
|
433
|
+
):
|
|
434
|
+
"""Dispatcher tools should include internalTools in lock, with count."""
|
|
435
|
+
env_dir = tmp_path / "env-int"
|
|
436
|
+
env_dir.mkdir()
|
|
437
|
+
(env_dir / "pyproject.toml").write_text("""
|
|
438
|
+
[tool.hud]
|
|
439
|
+
image = "test/env:dev"
|
|
440
|
+
""")
|
|
441
|
+
dockerfile = env_dir / "Dockerfile"
|
|
442
|
+
dockerfile.write_text("""
|
|
443
|
+
FROM python:3.11
|
|
444
|
+
""")
|
|
445
|
+
|
|
446
|
+
mock_build_docker.return_value = True
|
|
447
|
+
mock_analyze.return_value = {
|
|
448
|
+
"success": True,
|
|
449
|
+
"toolCount": 1,
|
|
450
|
+
"internalToolCount": 2,
|
|
451
|
+
"initializeMs": 500,
|
|
452
|
+
"tools": [
|
|
453
|
+
{
|
|
454
|
+
"name": "setup",
|
|
455
|
+
"description": "setup dispatcher",
|
|
456
|
+
"inputSchema": {"type": "object"},
|
|
457
|
+
"internalTools": ["board", "seed"],
|
|
458
|
+
}
|
|
459
|
+
],
|
|
460
|
+
}
|
|
461
|
+
mock_get_id.return_value = "sha256:fff111"
|
|
462
|
+
mock_collect_runtime.return_value = {
|
|
463
|
+
"python": "3.11.6",
|
|
464
|
+
"cuda": None,
|
|
465
|
+
"cudnn": None,
|
|
466
|
+
"pytorch": None,
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
mock_result = mock.Mock()
|
|
470
|
+
mock_result.returncode = 0
|
|
471
|
+
mock_run.return_value = mock_result
|
|
472
|
+
|
|
473
|
+
build_environment(str(env_dir), "env-int:latest")
|
|
474
|
+
|
|
475
|
+
lock_file = env_dir / "hud.lock.yaml"
|
|
476
|
+
with open(lock_file) as f:
|
|
477
|
+
data = yaml.safe_load(f)
|
|
478
|
+
assert data["version"] == "1.3"
|
|
479
|
+
assert data["environment"]["internalToolCount"] == 2
|
|
480
|
+
assert data["tools"][0]["name"] == "setup"
|
|
481
|
+
assert data["tools"][0]["internalTools"] == ["board", "seed"]
|
|
482
|
+
|
|
381
483
|
def test_build_environment_no_directory(self):
|
|
382
484
|
"""Test build when directory doesn't exist."""
|
|
383
485
|
with pytest.raises(typer.Exit):
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from hud.cli.build import build_environment
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@patch("hud.cli.build.compute_source_hash", return_value="deadbeef")
|
|
16
|
+
@patch(
|
|
17
|
+
"hud.cli.build.analyze_mcp_environment",
|
|
18
|
+
return_value={"initializeMs": 10, "toolCount": 0, "tools": []},
|
|
19
|
+
)
|
|
20
|
+
@patch("hud.cli.build.build_docker_image", return_value=True)
|
|
21
|
+
def test_build_label_rebuild_failure(_bd, _an, _hash, tmp_path: Path, monkeypatch):
|
|
22
|
+
# Minimal environment dir
|
|
23
|
+
env = tmp_path / "env"
|
|
24
|
+
env.mkdir()
|
|
25
|
+
(env / "Dockerfile").write_text("FROM python:3.11")
|
|
26
|
+
|
|
27
|
+
# Ensure subprocess.run returns non-zero for the second build (label build)
|
|
28
|
+
import types
|
|
29
|
+
|
|
30
|
+
def run_side_effect(cmd, *a, **k):
|
|
31
|
+
# Return 0 for first docker build, 1 for label build
|
|
32
|
+
if isinstance(cmd, list) and cmd[:2] == ["docker", "build"] and "--label" in cmd:
|
|
33
|
+
return types.SimpleNamespace(returncode=1, stderr="boom")
|
|
34
|
+
return types.SimpleNamespace(returncode=0, stdout="")
|
|
35
|
+
|
|
36
|
+
monkeypatch.setenv("FASTMCP_DISABLE_BANNER", "1")
|
|
37
|
+
with (
|
|
38
|
+
patch("hud.cli.build.subprocess.run", side_effect=run_side_effect),
|
|
39
|
+
pytest.raises(typer.Exit),
|
|
40
|
+
):
|
|
41
|
+
build_environment(str(env), verbose=False)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from unittest import mock
|
|
5
|
+
|
|
6
|
+
from hud.cli.build import (
|
|
7
|
+
extract_env_vars_from_dockerfile,
|
|
8
|
+
get_docker_image_digest,
|
|
9
|
+
get_docker_image_id,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_extract_env_vars_from_dockerfile_complex(tmp_path: Path):
|
|
17
|
+
dockerfile = tmp_path / "Dockerfile"
|
|
18
|
+
dockerfile.write_text(
|
|
19
|
+
"""
|
|
20
|
+
FROM python:3.11
|
|
21
|
+
ARG BUILD_TOKEN
|
|
22
|
+
ARG DEFAULTED=1
|
|
23
|
+
ENV RUNTIME_KEY
|
|
24
|
+
ENV FROM_ARG=$BUILD_TOKEN
|
|
25
|
+
ENV WITH_DEFAULT=val
|
|
26
|
+
"""
|
|
27
|
+
)
|
|
28
|
+
required, optional = extract_env_vars_from_dockerfile(dockerfile)
|
|
29
|
+
# BUILD_TOKEN required (ARG without default)
|
|
30
|
+
assert "BUILD_TOKEN" in required
|
|
31
|
+
# RUNTIME_KEY required (ENV without value)
|
|
32
|
+
assert "RUNTIME_KEY" in required
|
|
33
|
+
# FROM_ARG references BUILD_TOKEN -> required
|
|
34
|
+
assert "FROM_ARG" in required
|
|
35
|
+
# DEFAULTED and WITH_DEFAULT should not be marked required by default
|
|
36
|
+
assert "DEFAULTED" not in required
|
|
37
|
+
assert "WITH_DEFAULT" not in required
|
|
38
|
+
assert optional == []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@mock.patch("subprocess.run")
|
|
42
|
+
def test_get_docker_image_digest_none(mock_run):
|
|
43
|
+
mock_run.return_value = mock.Mock(stdout="[]", returncode=0)
|
|
44
|
+
assert get_docker_image_digest("img") is None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@mock.patch("subprocess.run")
|
|
48
|
+
def test_get_docker_image_id_ok(mock_run):
|
|
49
|
+
mock_run.return_value = mock.Mock(stdout="sha256:abc", returncode=0)
|
|
50
|
+
assert get_docker_image_id("img") == "sha256:abc"
|
hud/cli/tests/test_cli_init.py
CHANGED
|
@@ -193,10 +193,15 @@ class TestCLICommands:
|
|
|
193
193
|
|
|
194
194
|
def test_version_command(self) -> None:
|
|
195
195
|
"""Test version command."""
|
|
196
|
+
import re
|
|
197
|
+
|
|
198
|
+
# Strip ANSI escape codes
|
|
199
|
+
ansi_escape = re.compile(r"\x1b\[[0-9;]*m")
|
|
196
200
|
with patch("hud.__version__", "1.2.3"):
|
|
197
201
|
result = runner.invoke(app, ["version"])
|
|
198
202
|
assert result.exit_code == 0
|
|
199
|
-
|
|
203
|
+
clean_output = ansi_escape.sub("", result.output)
|
|
204
|
+
assert "1.2.3" in clean_output
|
|
200
205
|
|
|
201
206
|
def test_version_import_error(self) -> None:
|
|
202
207
|
"""Test version command when version unavailable."""
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
import hud.cli as cli
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_version_does_not_crash():
|
|
9
|
+
# Just ensure it runs without raising
|
|
10
|
+
cli.version()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@patch("hud.cli.list_module.list_command")
|
|
14
|
+
def test_list_environments_wrapper(mock_list):
|
|
15
|
+
cli.list_environments(filter_name=None, json_output=False, show_all=False, verbose=False)
|
|
16
|
+
assert mock_list.called
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@patch("hud.cli.clone_repository", return_value=(True, "/tmp/repo"))
|
|
20
|
+
@patch("hud.cli.get_clone_message", return_value={})
|
|
21
|
+
@patch("hud.cli.print_tutorial")
|
|
22
|
+
def test_clone_wrapper(mock_tutorial, _msg, _clone):
|
|
23
|
+
cli.clone("https://example.com/repo.git")
|
|
24
|
+
assert mock_tutorial.called
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@patch("hud.cli.remove_command")
|
|
28
|
+
def test_remove_wrapper(mock_remove):
|
|
29
|
+
cli.remove(target="all", yes=True, verbose=False)
|
|
30
|
+
assert mock_remove.called
|