from __future__ import annotations import pytest def test_require_missing_raises(): from argus.ray.models import _require with pytest.raises(ValueError, match="missing required field: x"): _require({}, "x") with pytest.raises(ValueError, match="missing required field: x"): _require({"x": ""}, "x") def test_ray_config_from_dict_new_format_and_defaults(): from argus.ray.models import RayConfig cfg = RayConfig.from_dict( { "ray": { "address": "http://127.0.0.1:8265", "shared_root": "/private", "entrypoint_resources": {"worker_node": 1}, "runtime_env": {"env_vars": {"HF_ENDPOINT": "x"}}, } } ) assert cfg.address.endswith("8265") assert cfg.shared_root == "/private" assert cfg.entrypoint_num_cpus == 1.0 assert cfg.entrypoint_resources["worker_node"] == 1.0 assert cfg.runtime_env_env_vars["HF_ENDPOINT"] == "x" assert cfg.user_code_path == "/private/user/code" assert cfg.verl_code_path == "/private/common/code/verl/verl_repo" public = cfg.to_public_dict() assert public["runtime_env"]["env_vars"]["HF_ENDPOINT"] == "x" def test_ray_config_from_dict_requires_mappings(): from argus.ray.models import RayConfig with pytest.raises(ValueError, match="runtime_env\\.env_vars must be a mapping"): RayConfig.from_dict( { "address": "x", "shared_root": "/p", "entrypoint_resources": {}, "runtime_env": {"env_vars": ["nope"]}, } ) with pytest.raises(ValueError, match="entrypoint_resources must be a mapping"): RayConfig.from_dict( { "address": "x", "shared_root": "/p", "entrypoint_resources": ["nope"], } ) def test_jobspec_validation_and_null_coercion(): from argus.ray.models import JobSpec spec = JobSpec.from_dict( { "workload": "ppo", "code_path": "/code", "model_id": "m", "train_file": "train.jsonl", "val_file": "val.jsonl", "test_freq": "", } ) assert spec.workload == "ppo" assert spec.val_file == "val.jsonl" assert spec.test_freq is None assert spec.total_training_steps is None assert spec.nnodes == 2 assert spec.n_gpus_per_node == 4 pub = spec.to_public_dict() assert pub["submission_id"] == "" assert "trainer_device" not in pub def test_jobspec_sft_adds_trainer_device_default(): from argus.ray.models import JobSpec spec = JobSpec.from_dict( { "workload": "sft", "code_path": "/code", "model_id": "m", "train_file": "train.jsonl", "val_file": "val.jsonl", } ) pub = spec.to_public_dict() assert pub["trainer_device"] == "cpu" def test_jobspec_unsupported_workload(): from argus.ray.models import JobSpec with pytest.raises(ValueError, match="unsupported workload"): JobSpec.from_dict( {"workload": "nope", "code_path": "x", "model_id": "m", "train_file": "t", "val_file": "v"} ) def test_parse_taskspec_basic_no_kind_compat(): from argus.ray.models import JobSpec, parse_taskspec got = parse_taskspec( { "workload": "ppo", "code_path": "/code", "model_id": "m", "train_file": "train.jsonl", "val_file": "val.jsonl", } ) assert isinstance(got, JobSpec) assert got.workload == "ppo" def test_parse_taskspec_advanced_smoke(): from argus.ray.models import AdvancedTaskSpec, parse_taskspec got = parse_taskspec( { "kind": "advanced", "nnodes": 2, "n_gpus_per_node": 4, "command": "python3 -m verl.trainer.main_ppo +ray_kwargs.ray_init.address=auto", } ) assert isinstance(got, AdvancedTaskSpec) assert got.kind == "advanced" assert got.workload == "advanced" assert got.nnodes == 2 assert got.n_gpus_per_node == 4 def test_parse_taskspec_advanced_requires_command_nnodes_gpus(): from argus.ray.models import parse_taskspec with pytest.raises(ValueError, match="missing required field: command"): parse_taskspec({"kind": "advanced", "nnodes": 1, "n_gpus_per_node": 1}) with pytest.raises(ValueError, match="missing required field: nnodes"): parse_taskspec({"kind": "advanced", "n_gpus_per_node": 1, "command": "python3 -m verl.trainer.main_ppo"}) with pytest.raises(ValueError, match="missing required field: n_gpus_per_node"): parse_taskspec({"kind": "advanced", "nnodes": 1, "command": "python3 -m verl.trainer.main_ppo"})