hud-python 0.4.22__py3-none-any.whl → 0.4.23__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.

Potentially problematic release.


This version of hud-python might be problematic. Click here for more details.

Files changed (48) hide show
  1. hud/agents/base.py +37 -39
  2. hud/agents/grounded_openai.py +3 -1
  3. hud/agents/misc/response_agent.py +3 -2
  4. hud/agents/openai.py +2 -2
  5. hud/agents/openai_chat_generic.py +3 -1
  6. hud/cli/__init__.py +34 -24
  7. hud/cli/analyze.py +27 -26
  8. hud/cli/build.py +50 -46
  9. hud/cli/debug.py +7 -7
  10. hud/cli/dev.py +107 -99
  11. hud/cli/eval.py +31 -29
  12. hud/cli/hf.py +53 -53
  13. hud/cli/init.py +28 -28
  14. hud/cli/list_func.py +22 -22
  15. hud/cli/pull.py +36 -36
  16. hud/cli/push.py +76 -74
  17. hud/cli/remove.py +42 -40
  18. hud/cli/rl/__init__.py +2 -2
  19. hud/cli/rl/init.py +41 -41
  20. hud/cli/rl/pod.py +97 -91
  21. hud/cli/rl/ssh.py +42 -40
  22. hud/cli/rl/train.py +75 -73
  23. hud/cli/rl/utils.py +10 -10
  24. hud/cli/tests/test_analyze.py +1 -1
  25. hud/cli/tests/test_analyze_metadata.py +2 -2
  26. hud/cli/tests/test_pull.py +45 -45
  27. hud/cli/tests/test_push.py +31 -29
  28. hud/cli/tests/test_registry.py +15 -15
  29. hud/cli/utils/environment.py +11 -11
  30. hud/cli/utils/interactive.py +17 -17
  31. hud/cli/utils/logging.py +12 -12
  32. hud/cli/utils/metadata.py +12 -12
  33. hud/cli/utils/registry.py +5 -5
  34. hud/cli/utils/runner.py +23 -23
  35. hud/cli/utils/server.py +16 -16
  36. hud/shared/hints.py +7 -7
  37. hud/tools/grounding/grounder.py +2 -1
  38. hud/types.py +4 -4
  39. hud/utils/__init__.py +3 -3
  40. hud/utils/{design.py → hud_console.py} +39 -33
  41. hud/utils/pretty_errors.py +6 -6
  42. hud/utils/tests/test_version.py +1 -1
  43. hud/version.py +1 -1
  44. {hud_python-0.4.22.dist-info → hud_python-0.4.23.dist-info}/METADATA +3 -1
  45. {hud_python-0.4.22.dist-info → hud_python-0.4.23.dist-info}/RECORD +48 -48
  46. {hud_python-0.4.22.dist-info → hud_python-0.4.23.dist-info}/WHEEL +0 -0
  47. {hud_python-0.4.22.dist-info → hud_python-0.4.23.dist-info}/entry_points.txt +0 -0
  48. {hud_python-0.4.22.dist-info → hud_python-0.4.23.dist-info}/licenses/LICENSE +0 -0
hud/cli/rl/train.py CHANGED
@@ -11,7 +11,7 @@ from typing import Any
11
11
  import typer
12
12
 
13
13
  from hud.settings import settings
14
- from hud.utils.design import HUDDesign
14
+ from hud.utils.hud_console import HUDConsole
15
15
 
16
16
  from .pod import run_prime_training
17
17
  from .utils import (
@@ -20,7 +20,7 @@ from .utils import (
20
20
  validate_dataset_name,
21
21
  )
22
22
 
23
- design = HUDDesign()
23
+ hud_console = HUDConsole()
24
24
 
25
25
 
26
26
  def find_task_json_files() -> list[Path]:
@@ -67,7 +67,7 @@ def train_command_wrapper(
67
67
  ) -> None:
68
68
  """Wrapper to handle interactive prompts before entering async context."""
69
69
  # Pre-flight checks for required environment variables
70
- design.section_title("🔍 Pre-flight Checks")
70
+ hud_console.section_title("🔍 Pre-flight Checks")
71
71
 
72
72
  missing_vars = []
73
73
 
@@ -75,30 +75,32 @@ def train_command_wrapper(
75
75
  if not settings.api_key:
76
76
  missing_vars.append("HUD_API_KEY")
77
77
  else:
78
- design.success("✓ HUD_API_KEY configured")
78
+ hud_console.success("✓ HUD_API_KEY configured")
79
79
 
80
80
  # Check WANDB API key (optional but recommended)
81
81
  if not getattr(settings, "wandb_api_key", None):
82
- design.warning("⚠ WANDB_API_KEY not set (optional but recommended for training metrics)")
82
+ hud_console.warning(
83
+ "⚠ WANDB_API_KEY not set (optional but recommended for training metrics)"
84
+ )
83
85
  else:
84
- design.success("✓ WANDB_API_KEY configured")
86
+ hud_console.success("✓ WANDB_API_KEY configured")
85
87
 
86
88
  # Check PRIME API key (required for remote training)
87
89
  if provider == "prime" and not getattr(settings, "prime_api_key", None):
88
90
  missing_vars.append("PRIME_API_KEY")
89
91
  elif provider == "prime":
90
- design.success("✓ PRIME_API_KEY configured")
92
+ hud_console.success("✓ PRIME_API_KEY configured")
91
93
 
92
94
  if missing_vars:
93
- design.error(f"Missing required environment variables: {', '.join(missing_vars)}")
94
- design.info("")
95
- design.info("Set them using one of these methods:")
96
- design.info("1. Environment variables:")
95
+ hud_console.error(f"Missing required environment variables: {', '.join(missing_vars)}")
96
+ hud_console.info("")
97
+ hud_console.info("Set them using one of these methods:")
98
+ hud_console.info("1. Environment variables:")
97
99
  for var in missing_vars:
98
- design.command_example(f"export {var}=your-{var.lower().replace('_', '-')}")
99
- design.info("")
100
- design.info("2. Create a .env file in your project root:")
101
- design.command_example(
100
+ hud_console.command_example(f"export {var}=your-{var.lower().replace('_', '-')}")
101
+ hud_console.info("")
102
+ hud_console.info("2. Create a .env file in your project root:")
103
+ hud_console.command_example(
102
104
  "\n".join([f"{var}=your-{var.lower().replace('_', '-')}" for var in missing_vars]),
103
105
  "env",
104
106
  )
@@ -114,7 +116,7 @@ def train_command_wrapper(
114
116
  yaml_files = list(config_dir.glob("*.yaml"))
115
117
  if len(yaml_files) == 1:
116
118
  config = yaml_files[0]
117
- design.info(f"Using config: {config}")
119
+ hud_console.info(f"Using config: {config}")
118
120
 
119
121
  # Store user choice for pod creation
120
122
  auto_create_pod = None
@@ -128,20 +130,20 @@ def train_command_wrapper(
128
130
  config_dir = Path("configs")
129
131
  yaml_files = list(config_dir.glob("*.yaml"))
130
132
  config_names = [f.name for f in yaml_files]
131
- selected_config = design.select(
133
+ selected_config = hud_console.select(
132
134
  "Multiple config files found. Select one:", config_names
133
135
  )
134
136
  config = config_dir / selected_config
135
137
  else:
136
138
  # No config found, offer to generate
137
- generate_config = design.select(
139
+ generate_config = hud_console.select(
138
140
  "No config file found. Would you like to generate one?",
139
141
  ["Yes, generate config", "No, I'll create it manually"],
140
142
  )
141
143
 
142
144
  if generate_config == "Yes, generate config":
143
- design.info("Running 'hud rl init' to generate config...")
144
- design.info("")
145
+ hud_console.info("Running 'hud rl init' to generate config...")
146
+ hud_console.info("")
145
147
  # Import here to avoid circular imports
146
148
  from .init import init_command_wrapper
147
149
 
@@ -153,29 +155,29 @@ def train_command_wrapper(
153
155
  yaml_files = list(config_dir.glob("*.yaml"))
154
156
  if yaml_files:
155
157
  config = yaml_files[0]
156
- design.success(f"Using generated config: {config}")
158
+ hud_console.success(f"Using generated config: {config}")
157
159
  else:
158
- design.error("Config generation failed")
160
+ hud_console.error("Config generation failed")
159
161
  raise typer.Exit(1)
160
162
  else:
161
- design.info("Please create a config file and try again")
163
+ hud_console.info("Please create a config file and try again")
162
164
  raise typer.Exit(1)
163
165
 
164
166
  if "dataset" in missing:
165
167
  if missing["dataset"] == "multiple_json":
166
168
  # Multiple JSON files found, let user choose
167
169
  json_files = find_task_json_files()
168
- design.info("Multiple task files found:")
169
- file_choice = design.select(
170
+ hud_console.info("Multiple task files found:")
171
+ file_choice = hud_console.select(
170
172
  "Select a task file to use:",
171
173
  choices=[str(f) for f in json_files],
172
174
  )
173
175
  dataset = file_choice
174
- design.success(f"Selected: {dataset}")
176
+ hud_console.success(f"Selected: {dataset}")
175
177
  elif missing["dataset"] == "none":
176
- design.error("No dataset specified and no task JSON files found")
177
- design.info("Please use --dataset or create a tasks.json file")
178
- design.hint(
178
+ hud_console.error("No dataset specified and no task JSON files found")
179
+ hud_console.info("Please use --dataset or create a tasks.json file")
180
+ hud_console.hint(
179
181
  "Example: hud hf --name my-org/my-tasks # Generate tasks from HUD evaluation"
180
182
  )
181
183
  raise typer.Exit(1)
@@ -201,12 +203,12 @@ def train_command_wrapper(
201
203
  value = parts[1].strip()
202
204
  if value and value != "None":
203
205
  has_global_team = True
204
- design.info("Using globally configured team ID")
206
+ hud_console.info("Using globally configured team ID")
205
207
  break
206
208
 
207
209
  if not has_global_team:
208
210
  # Only ask if no global team is configured
209
- auto_create_pod = design.select(
211
+ auto_create_pod = hud_console.select(
210
212
  "How would you like to create the Prime Intellect pod?",
211
213
  ["Personal account (automated)", "Team account (enter team ID)"],
212
214
  )
@@ -217,7 +219,7 @@ def train_command_wrapper(
217
219
 
218
220
  # Save it globally automatically
219
221
  subprocess.run(["prime", "config", "set-team-id", team_id]) # noqa: S603, S607
220
- design.success("Team ID saved globally")
222
+ hud_console.success("Team ID saved globally")
221
223
 
222
224
  auto_create_pod = (
223
225
  "Personal account (automated)" # Treat as automated after getting team ID
@@ -249,13 +251,13 @@ async def train_command(
249
251
  team_id: str | None = None,
250
252
  ) -> None:
251
253
  """Run RL training on HUD environments."""
252
- design.header("🤖 HUD RL Training")
254
+ hud_console.header("🤖 HUD RL Training")
253
255
 
254
256
  # Get environment image
255
257
  image = detect_image_name()
256
258
  if not image:
257
- design.error("No environment image found")
258
- design.hint("Run 'hud build' first or specify with 'hud rl init <image>'")
259
+ hud_console.error("No environment image found")
260
+ hud_console.hint("Run 'hud build' first or specify with 'hud rl init <image>'")
259
261
  raise typer.Exit(1)
260
262
 
261
263
  # Handle dataset (JSON file or HuggingFace dataset)
@@ -269,17 +271,17 @@ async def train_command(
269
271
  if json_files:
270
272
  if len(json_files) == 1:
271
273
  dataset = str(json_files[0])
272
- design.info(f"Found task file: {dataset}")
274
+ hud_console.info(f"Found task file: {dataset}")
273
275
  is_json_file = True
274
276
  else:
275
277
  # This case should have been handled in train_command_wrapper
276
- design.error("Multiple task files found but none selected")
278
+ hud_console.error("Multiple task files found but none selected")
277
279
  raise typer.Exit(1)
278
280
  else:
279
281
  # Use dataset from lock file
280
282
  dataset = get_primary_dataset()
281
283
  if dataset:
282
- design.info(f"Using dataset from lock file: {dataset}")
284
+ hud_console.info(f"Using dataset from lock file: {dataset}")
283
285
 
284
286
  # Check if dataset is a file path
285
287
  if dataset and Path(dataset).exists() and dataset.endswith(".json"):
@@ -288,7 +290,7 @@ async def train_command(
288
290
  # Validate dataset
289
291
  if dataset and is_json_file:
290
292
  # Load and validate JSON file
291
- design.info(f"Validating task file: {dataset}")
293
+ hud_console.info(f"Validating task file: {dataset}")
292
294
  try:
293
295
  with open(dataset) as f: # noqa: ASYNC230
294
296
  tasks_data = json.load(f)
@@ -299,17 +301,17 @@ async def train_command(
299
301
  elif isinstance(tasks_data, list):
300
302
  tasks = tasks_data
301
303
  else:
302
- design.error("Invalid tasks file format")
304
+ hud_console.error("Invalid tasks file format")
303
305
  raise typer.Exit(1)
304
306
 
305
307
  dataset_size = len(tasks)
306
308
  if dataset_size < 4:
307
- design.error(f"Task file has only {dataset_size} tasks")
308
- design.info("RL training requires at least 4 tasks for proper batching")
309
- design.hint("Consider adding more tasks to your JSON file")
309
+ hud_console.error(f"Task file has only {dataset_size} tasks")
310
+ hud_console.info("RL training requires at least 4 tasks for proper batching")
311
+ hud_console.hint("Consider adding more tasks to your JSON file")
310
312
  raise typer.Exit(1)
311
313
 
312
- design.success(f"✓ Task file has {dataset_size} tasks")
314
+ hud_console.success(f"✓ Task file has {dataset_size} tasks")
313
315
 
314
316
  # Check and convert MCP configs to remote if needed
315
317
  if tasks:
@@ -328,7 +330,7 @@ async def train_command(
328
330
  config_type = "local"
329
331
 
330
332
  if config_type == "local":
331
- design.info("Converting local MCP configs to remote for training...")
333
+ hud_console.info("Converting local MCP configs to remote for training...")
332
334
 
333
335
  # Get the image name from lock file or environment
334
336
  from .utils import get_image_from_lock
@@ -336,18 +338,18 @@ async def train_command(
336
338
  env_image = image or get_image_from_lock()
337
339
 
338
340
  if not env_image:
339
- design.error("No image found for remote MCP conversion")
340
- design.hint("Run 'hud build' first")
341
+ hud_console.error("No image found for remote MCP conversion")
342
+ hud_console.hint("Run 'hud build' first")
341
343
  raise typer.Exit(1)
342
344
 
343
345
  # Check if image needs to be pushed
344
346
  if "/" not in env_image or env_image.startswith("local/"):
345
- design.warning(f"Image '{env_image}' appears to be local only")
346
- design.info("Running 'hud push' to make it publicly available...")
347
+ hud_console.warning(f"Image '{env_image}' appears to be local only")
348
+ hud_console.info("Running 'hud push' to make it publicly available...")
347
349
  from hud.cli.push import push_command
348
350
 
349
351
  push_command(directory=".", yes=True)
350
- design.success("Image pushed successfully")
352
+ hud_console.success("Image pushed successfully")
351
353
  # Re-read image name after push
352
354
  env_image = get_image_from_lock()
353
355
 
@@ -364,18 +366,18 @@ async def train_command(
364
366
  }
365
367
  task["mcp_config"] = remote_config
366
368
 
367
- design.success("✓ Converted all tasks to use remote MCP configs")
369
+ hud_console.success("✓ Converted all tasks to use remote MCP configs")
368
370
 
369
371
  # Save the modified tasks back to the file
370
372
  with open(dataset, "w") as f: # noqa: ASYNC230
371
373
  json.dump(tasks, f, indent=2)
372
- design.info("Updated task file with remote configs")
374
+ hud_console.info("Updated task file with remote configs")
373
375
  except json.JSONDecodeError as e:
374
- design.error(f"Invalid JSON in task file: {e}")
376
+ hud_console.error(f"Invalid JSON in task file: {e}")
375
377
  raise typer.Exit(1) from e
376
378
  elif dataset:
377
379
  # Validate HuggingFace dataset
378
- design.info(f"Validating dataset: {dataset}")
380
+ hud_console.info(f"Validating dataset: {dataset}")
379
381
  try:
380
382
  # Try to load dataset info from HuggingFace
381
383
  from datasets import load_dataset_builder
@@ -386,21 +388,21 @@ async def train_command(
386
388
  # Check split sizes
387
389
  train_size = ds_info.splits.get("train", None) if ds_info.splits else None
388
390
  if train_size and train_size.num_examples < 4:
389
- design.error(f"Dataset '{dataset}' has only {train_size.num_examples} tasks")
390
- design.info("RL training requires at least 4 tasks for proper batching")
391
- design.hint("Consider adding more tasks or duplicating existing ones")
391
+ hud_console.error(f"Dataset '{dataset}' has only {train_size.num_examples} tasks")
392
+ hud_console.info("RL training requires at least 4 tasks for proper batching")
393
+ hud_console.hint("Consider adding more tasks or duplicating existing ones")
392
394
  raise typer.Exit(1)
393
395
  elif train_size:
394
396
  dataset_size = train_size.num_examples
395
- design.success(f"✓ Dataset has {dataset_size} tasks")
397
+ hud_console.success(f"✓ Dataset has {dataset_size} tasks")
396
398
  except Exception as e:
397
399
  # If we can't validate, warn but continue
398
- design.warning(f"Could not validate dataset size: {e}")
399
- design.info("Proceeding with training - ensure dataset has at least 4 tasks")
400
+ hud_console.warning(f"Could not validate dataset size: {e}")
401
+ hud_console.info("Proceeding with training - ensure dataset has at least 4 tasks")
400
402
 
401
403
  # Display configuration
402
- design.section_title("📋 Training Configuration")
403
- design.json_config(
404
+ hud_console.section_title("📋 Training Configuration")
405
+ hud_console.json_config(
404
406
  json.dumps(
405
407
  {
406
408
  "Model": model,
@@ -416,13 +418,13 @@ async def train_command(
416
418
  )
417
419
 
418
420
  if not config:
419
- design.error("No config file found")
420
- design.hint("Run 'hud rl init' to generate a config file")
421
+ hud_console.error("No config file found")
422
+ hud_console.hint("Run 'hud rl init' to generate a config file")
421
423
  raise typer.Exit(1)
422
424
 
423
425
  if not dataset:
424
- design.error("No dataset found")
425
- design.hint("Run 'hud hf tasks.json' to create a dataset")
426
+ hud_console.error("No dataset found")
427
+ hud_console.hint("Run 'hud hf tasks.json' to create a dataset")
426
428
  raise typer.Exit(1)
427
429
 
428
430
  # Always run remote training
@@ -499,14 +501,14 @@ def create_dataset_interactive() -> str | None:
499
501
  # Check if tasks.json exists
500
502
  tasks_file = Path("tasks.json")
501
503
  if not tasks_file.exists():
502
- design.error("No tasks.json file found")
504
+ hud_console.error("No tasks.json file found")
503
505
  return None
504
506
 
505
507
  # Prompt for dataset name
506
508
  dataset_name = typer.prompt("Enter HuggingFace dataset name (e.g., username/dataset-name)")
507
509
 
508
510
  if not validate_dataset_name(dataset_name):
509
- design.error("Invalid dataset name format")
511
+ hud_console.error("Invalid dataset name format")
510
512
  return None
511
513
 
512
514
  # Run hf command
@@ -519,9 +521,9 @@ def create_dataset_interactive() -> str | None:
519
521
  if result.returncode == 0:
520
522
  return dataset_name
521
523
  else:
522
- design.error("Failed to create dataset")
524
+ hud_console.error("Failed to create dataset")
523
525
  if result.stderr:
524
- design.error(result.stderr)
526
+ hud_console.error(result.stderr)
525
527
  return None
526
528
 
527
529
 
@@ -539,7 +541,7 @@ async def run_remote_training(
539
541
  is_json_file: bool = False,
540
542
  ) -> None:
541
543
  """Run training on remote infrastructure."""
542
- design.section_title("🚀 Remote Training")
544
+ hud_console.section_title("🚀 Remote Training")
543
545
 
544
546
  if provider == "prime":
545
547
  await run_prime_training(
@@ -555,6 +557,6 @@ async def run_remote_training(
555
557
  is_json_file,
556
558
  )
557
559
  else:
558
- design.error(f"Provider '{provider}' not yet supported")
559
- design.info("Currently supported: prime")
560
+ hud_console.error(f"Provider '{provider}' not yet supported")
561
+ hud_console.info("Currently supported: prime")
560
562
  raise typer.Exit(1)
hud/cli/rl/utils.py CHANGED
@@ -8,9 +8,9 @@ from typing import Any
8
8
 
9
9
  import yaml
10
10
 
11
- from hud.utils.design import HUDDesign
11
+ from hud.utils.hud_console import HUDConsole
12
12
 
13
- design = HUDDesign()
13
+ hud_console = HUDConsole()
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16
 
@@ -25,7 +25,7 @@ def read_lock_file() -> dict[str, Any]:
25
25
  with open(lock_file) as f:
26
26
  return yaml.safe_load(f) or {}
27
27
  except Exception as e:
28
- design.warning(f"Could not read hud.lock.yaml: {e}")
28
+ hud_console.warning(f"Could not read hud.lock.yaml: {e}")
29
29
  return {}
30
30
 
31
31
 
@@ -38,7 +38,7 @@ def write_lock_file(data: dict[str, Any]) -> bool:
38
38
  yaml.dump(data, f, default_flow_style=False, sort_keys=False)
39
39
  return True
40
40
  except Exception as e:
41
- design.warning(f"Could not write hud.lock.yaml: {e}")
41
+ hud_console.warning(f"Could not write hud.lock.yaml: {e}")
42
42
  return False
43
43
 
44
44
 
@@ -109,26 +109,26 @@ def validate_dataset_name(name: str) -> bool:
109
109
  return False
110
110
 
111
111
  if "/" not in name:
112
- design.error(f"Invalid dataset name: {name}")
113
- design.info("Dataset name should be in format: org/dataset")
112
+ hud_console.error(f"Invalid dataset name: {name}")
113
+ hud_console.info("Dataset name should be in format: org/dataset")
114
114
  return False
115
115
 
116
116
  parts = name.split("/")
117
117
  if len(parts) != 2:
118
- design.error(f"Invalid dataset name: {name}")
118
+ hud_console.error(f"Invalid dataset name: {name}")
119
119
  return False
120
120
 
121
121
  org, dataset = parts
122
122
  if not org or not dataset:
123
- design.error(f"Invalid dataset name: {name}")
123
+ hud_console.error(f"Invalid dataset name: {name}")
124
124
  return False
125
125
 
126
126
  # Check for valid characters (alphanumeric, dash, underscore)
127
127
  import re
128
128
 
129
129
  if not re.match(r"^[a-zA-Z0-9_-]+$", org) or not re.match(r"^[a-zA-Z0-9_-]+$", dataset):
130
- design.error(f"Invalid characters in dataset name: {name}")
131
- design.info("Use only letters, numbers, dashes, and underscores")
130
+ hud_console.error(f"Invalid characters in dataset name: {name}")
131
+ hud_console.info("Use only letters, numbers, dashes, and underscores")
132
132
  return False
133
133
 
134
134
  return True
@@ -251,7 +251,7 @@ class TestDisplayFunctions:
251
251
 
252
252
  # Check console was called multiple times
253
253
  assert mock_console.print.call_count > 0
254
- # The design.section_title uses its own console, not the patched one
254
+ # The hud_console.section_title uses its own console, not the patched one
255
255
  # Just verify the function ran without errors
256
256
 
257
257
  def test_display_markdown_basic(self) -> None:
@@ -216,7 +216,7 @@ class TestAnalyzeFromMetadata:
216
216
  @mock.patch("hud.cli.utils.metadata.fetch_lock_from_registry")
217
217
  @mock.patch("hud.cli.utils.metadata.design")
218
218
  @mock.patch("hud.cli.utils.metadata.console")
219
- async def test_analyze_not_found(self, mock_console, mock_design, mock_fetch, mock_check):
219
+ async def test_analyze_not_found(self, mock_console, mock_hud_console, mock_fetch, mock_check):
220
220
  """Test when environment not found anywhere."""
221
221
  mock_check.return_value = None
222
222
  mock_fetch.return_value = None
@@ -224,7 +224,7 @@ class TestAnalyzeFromMetadata:
224
224
  await analyze_from_metadata("test/notfound:latest", "json", verbose=False)
225
225
 
226
226
  # Should show error
227
- mock_design.error.assert_called_with("Environment metadata not found")
227
+ mock_hud_console.error.assert_called_with("Environment metadata not found")
228
228
  # Should print suggestions
229
229
  mock_console.print.assert_called()
230
230
 
@@ -180,15 +180,15 @@ class TestFormatSize:
180
180
  class TestPullEnvironment:
181
181
  """Test the main pull_environment function."""
182
182
 
183
- @mock.patch("hud.cli.pull.HUDDesign")
183
+ @mock.patch("hud.cli.pull.HUDConsole")
184
184
  @mock.patch("hud.cli.pull.save_to_registry")
185
185
  @mock.patch("subprocess.Popen")
186
- def test_pull_with_lock_file(self, mock_popen, mock_save, mock_design_class, tmp_path):
186
+ def test_pull_with_lock_file(self, mock_popen, mock_save, mock_hud_console_class, tmp_path):
187
187
  """Test pulling with a lock file."""
188
- # Create mock design instance
189
- mock_design = mock.Mock()
190
- mock_design.console = mock.Mock()
191
- mock_design_class.return_value = mock_design
188
+ # Create mock hud_console instance
189
+ mock_hud_console = mock.Mock()
190
+ mock_hud_console.console = mock.Mock()
191
+ mock_hud_console_class.return_value = mock_hud_console
192
192
 
193
193
  # Create lock file
194
194
  lock_data = {
@@ -218,15 +218,15 @@ class TestPullEnvironment:
218
218
  # Verify lock was saved to registry
219
219
  mock_save.assert_called_once()
220
220
 
221
- @mock.patch("hud.cli.pull.HUDDesign")
221
+ @mock.patch("hud.cli.pull.HUDConsole")
222
222
  @mock.patch("hud.cli.pull.fetch_lock_from_registry")
223
223
  @mock.patch("subprocess.Popen")
224
- def test_pull_from_registry(self, mock_popen, mock_fetch, mock_design_class):
224
+ def test_pull_from_registry(self, mock_popen, mock_fetch, mock_hud_console_class):
225
225
  """Test pulling from HUD registry."""
226
- # Create mock design instance
227
- mock_design = mock.Mock()
228
- mock_design.console = mock.Mock()
229
- mock_design_class.return_value = mock_design
226
+ # Create mock hud_console instance
227
+ mock_hud_console = mock.Mock()
228
+ mock_hud_console.console = mock.Mock()
229
+ mock_hud_console_class.return_value = mock_hud_console
230
230
 
231
231
  # Mock registry response
232
232
  lock_data = {"image": "docker.io/org/env:latest@sha256:def456", "tools": []}
@@ -250,18 +250,18 @@ class TestPullEnvironment:
250
250
  call_args = mock_popen.call_args[0][0]
251
251
  assert "docker.io/org/env:latest@sha256:def456" in call_args
252
252
 
253
- @mock.patch("hud.cli.pull.HUDDesign")
253
+ @mock.patch("hud.cli.pull.HUDConsole")
254
254
  @mock.patch("hud.cli.pull.get_docker_manifest")
255
255
  @mock.patch("hud.cli.pull.fetch_lock_from_registry")
256
256
  @mock.patch("subprocess.Popen")
257
257
  def test_pull_docker_image_direct(
258
- self, mock_popen, mock_fetch, mock_manifest, mock_design_class
258
+ self, mock_popen, mock_fetch, mock_manifest, mock_hud_console_class
259
259
  ):
260
260
  """Test pulling Docker image directly."""
261
- # Create mock design instance
262
- mock_design = mock.Mock()
263
- mock_design.console = mock.Mock()
264
- mock_design_class.return_value = mock_design
261
+ # Create mock hud_console instance
262
+ mock_hud_console = mock.Mock()
263
+ mock_hud_console.console = mock.Mock()
264
+ mock_hud_console_class.return_value = mock_hud_console
265
265
 
266
266
  # Mock no registry data
267
267
  mock_fetch.return_value = None
@@ -284,28 +284,28 @@ class TestPullEnvironment:
284
284
  call_args = mock_popen.call_args[0][0]
285
285
  assert call_args == ["docker", "pull", "ubuntu:latest"]
286
286
 
287
- @mock.patch("hud.cli.pull.HUDDesign")
288
- def test_pull_verify_only(self, mock_design_class):
287
+ @mock.patch("hud.cli.pull.HUDConsole")
288
+ def test_pull_verify_only(self, mock_hud_console_class):
289
289
  """Test verify-only mode."""
290
- # Create mock design instance
291
- mock_design = mock.Mock()
292
- mock_design.console = mock.Mock()
293
- mock_design_class.return_value = mock_design
290
+ # Create mock hud_console instance
291
+ mock_hud_console = mock.Mock()
292
+ mock_hud_console.console = mock.Mock()
293
+ mock_hud_console_class.return_value = mock_hud_console
294
294
 
295
295
  # Should not actually pull
296
296
  pull_environment("test:latest", verify_only=True)
297
297
 
298
298
  # Check success message
299
- mock_design.success.assert_called_with("Verification complete")
299
+ mock_hud_console.success.assert_called_with("Verification complete")
300
300
 
301
- @mock.patch("hud.cli.pull.HUDDesign")
301
+ @mock.patch("hud.cli.pull.HUDConsole")
302
302
  @mock.patch("subprocess.Popen")
303
- def test_pull_docker_failure(self, mock_popen, mock_design_class):
303
+ def test_pull_docker_failure(self, mock_popen, mock_hud_console_class):
304
304
  """Test handling Docker pull failure."""
305
- # Create mock design instance
306
- mock_design = mock.Mock()
307
- mock_design.console = mock.Mock()
308
- mock_design_class.return_value = mock_design
305
+ # Create mock hud_console instance
306
+ mock_hud_console = mock.Mock()
307
+ mock_hud_console.console = mock.Mock()
308
+ mock_hud_console_class.return_value = mock_hud_console
309
309
 
310
310
  # Mock docker pull failure
311
311
  mock_process = mock.Mock()
@@ -318,16 +318,16 @@ class TestPullEnvironment:
318
318
  with pytest.raises(typer.Exit):
319
319
  pull_environment("invalid:image", yes=True)
320
320
 
321
- mock_design.error.assert_called_with("Pull failed")
321
+ mock_hud_console.error.assert_called_with("Pull failed")
322
322
 
323
- @mock.patch("hud.cli.pull.HUDDesign")
323
+ @mock.patch("hud.cli.pull.HUDConsole")
324
324
  @mock.patch("typer.confirm")
325
- def test_pull_user_cancels(self, mock_confirm, mock_design_class):
325
+ def test_pull_user_cancels(self, mock_confirm, mock_hud_console_class):
326
326
  """Test when user cancels pull."""
327
- # Create mock design instance
328
- mock_design = mock.Mock()
329
- mock_design.console = mock.Mock()
330
- mock_design_class.return_value = mock_design
327
+ # Create mock hud_console instance
328
+ mock_hud_console = mock.Mock()
329
+ mock_hud_console.console = mock.Mock()
330
+ mock_hud_console_class.return_value = mock_hud_console
331
331
 
332
332
  mock_confirm.return_value = False
333
333
 
@@ -335,15 +335,15 @@ class TestPullEnvironment:
335
335
  pull_environment("test:latest", yes=False)
336
336
 
337
337
  assert exc_info.value.exit_code == 0
338
- mock_design.info.assert_called_with("Aborted")
338
+ mock_hud_console.info.assert_called_with("Aborted")
339
339
 
340
- @mock.patch("hud.cli.pull.HUDDesign")
341
- def test_pull_nonexistent_lock_file(self, mock_design_class):
340
+ @mock.patch("hud.cli.pull.HUDConsole")
341
+ def test_pull_nonexistent_lock_file(self, mock_hud_console_class):
342
342
  """Test pulling with non-existent lock file."""
343
- # Create mock design instance
344
- mock_design = mock.Mock()
345
- mock_design.console = mock.Mock()
346
- mock_design_class.return_value = mock_design
343
+ # Create mock hud_console instance
344
+ mock_hud_console = mock.Mock()
345
+ mock_hud_console.console = mock.Mock()
346
+ mock_hud_console_class.return_value = mock_hud_console
347
347
 
348
348
  with pytest.raises(typer.Exit):
349
349
  pull_environment("nonexistent.yaml")