hackathon-advisor / tests /test_model_runtime.py
JacobLinCool's picture
feat: add live project atlas
4791c0a verified
import pytest
from hackathon_advisor.model_runtime import (
DEFAULT_ADAPTER_ID,
DEFAULT_ADAPTER_REVISION,
MiniCPMTransformersPlanner,
RuleBasedPlanner,
create_tool_planner,
render_context,
runtime_status,
system_prompt,
_best_local_device,
_minicpm_generation_kwargs,
_load_minicpm_causal_lm,
_minicpm_chat_inputs,
_normalize_xml_tool_output,
_resolve_torch_device,
_strip_unused_generation_inputs,
)
from hackathon_advisor.zerogpu import gpu_task, zero_gpu_duration_seconds, zero_gpu_enabled
class FakeBackends:
def __init__(self, mps: bool) -> None:
self.mps = type("MPS", (), {"is_available": staticmethod(lambda: mps)})()
class FakeTorch:
def __init__(self, cuda: bool = False, mps: bool = False) -> None:
self.bfloat16 = "bfloat16"
self.float32 = "float32"
self.cuda = type("CUDA", (), {"is_available": staticmethod(lambda: cuda)})()
self.backends = FakeBackends(mps)
class FakeInputs(dict):
def to(self, device):
self["device"] = device
return self
class FakeTokenizer:
def __init__(self) -> None:
self.template_call = None
self.tokenizer_call = None
def apply_chat_template(self, messages, *, tokenize, add_generation_prompt, enable_thinking):
self.template_call = {
"messages": messages,
"tokenize": tokenize,
"add_generation_prompt": add_generation_prompt,
"enable_thinking": enable_thinking,
}
return "rendered prompt"
def __call__(self, prompts, *, return_tensors):
self.tokenizer_call = {"prompts": prompts, "return_tensors": return_tensors}
return FakeInputs({"input_ids": [1], "attention_mask": [1], "token_type_ids": [0]})
class FakeMiniCPMModel:
last_instance = None
@classmethod
def from_pretrained(cls, model_id, **kwargs):
instance = cls()
instance.model_id = model_id
instance.kwargs = kwargs
instance.device = None
cls.last_instance = instance
return instance
def to(self, device):
self.device = device
return self
def test_rule_planner_emits_valid_search_call() -> None:
planner = RuleBasedPlanner()
resolution = planner.plan("search similar lullaby audio projects", {})
assert resolution.status == "valid"
assert resolution.call.name == "search_projects"
assert resolution.call.arguments["query"] == "search similar lullaby audio projects"
def test_rule_planner_uses_plan_when_idea_exists() -> None:
planner = RuleBasedPlanner()
resolution = planner.plan("make a build plan", {"ideas": [{"title": "A", "pitch": "B"}]})
assert resolution.status == "valid"
assert resolution.call.name == "make_plan"
def test_rule_planner_keeps_empty_board_commands_as_commands() -> None:
planner = RuleBasedPlanner()
plan = planner.plan("make a build plan", {})
rank = planner.plan("compare ideas", {})
assert plan.status == "valid"
assert plan.call.name == "make_plan"
assert rank.status == "valid"
assert rank.call.name == "compare_ideas"
def test_rule_planner_defaults_blank_to_list_projects() -> None:
planner = RuleBasedPlanner()
resolution = planner.plan("", {})
assert resolution.status == "valid"
assert resolution.call.name == "list_projects"
def test_rule_planner_routes_project_reference_commands() -> None:
planner = RuleBasedPlanner()
listed = planner.plan("show current map", {})
project = planner.plan("read project lolaby", {})
project_url = planner.plan("open space https://huggingface.co/spaces/build-small-hackathon/lolaby", {})
assert listed.status == "valid"
assert listed.call.name == "list_projects"
assert project.status == "valid"
assert project.call.name == "get_project"
assert project.call.arguments["id"] == "lolaby"
assert project_url.status == "valid"
assert project_url.call.name == "get_project"
assert project_url.call.arguments["id"] == "build-small-hackathon/lolaby"
def test_rule_planner_keeps_project_words_inside_ideas() -> None:
planner = RuleBasedPlanner()
resolution = planner.plan("A dashboard that helps teams show projects to mentors", {})
assert resolution.status == "valid"
assert resolution.call.name == "save_idea"
def test_rule_planner_does_not_match_commands_inside_idea_words() -> None:
planner = RuleBasedPlanner()
planting = planner.plan(
"A neighborhood seed swap archive that reminds gardeners when to plant shared seeds",
{},
)
cooking_plan = planner.plan(
"A countertop helper that turns pantry leftovers into a weekly cooking plan",
{},
)
assert planting.status == "valid"
assert planting.call.name == "save_idea"
assert cooking_plan.status == "valid"
assert cooking_plan.call.name == "save_idea"
def test_rule_planner_splits_explicit_idea_pitch() -> None:
planner = RuleBasedPlanner()
resolution = planner.plan(
"idea: Hands-on science coach -- A lab-notebook companion for household experiments.",
{},
)
assert resolution.status == "valid"
assert resolution.call.name == "save_idea"
assert resolution.call.arguments["title"] == "Hands-on science coach"
assert resolution.call.arguments["pitch"] == "A lab-notebook companion for household experiments."
def test_render_context_includes_state() -> None:
context = render_context(
"make a plan",
{
"ideas": [{"title": "Archive Cartographer", "pitch": "Map family memories."}],
"trace": [{"input": "first", "verdict": "ECHO x2", "overall": 5.1}],
},
)
assert "Archive Cartographer" in context
assert "ECHO x2" in context
assert '<function name="tool_name">' in context
assert "Available tools:" in context
assert "search_projects" in context
def test_system_prompt_keeps_runtime_role_user_facing() -> None:
prompt = system_prompt()
assert "The Unwritten Almanac" in prompt
assert "Mothback" not in prompt
assert "Build Small" not in prompt
def test_create_tool_planner_defaults_to_minicpm(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("ADVISOR_MODEL_BACKEND", raising=False)
monkeypatch.delenv("ADVISOR_ADAPTER_ID", raising=False)
monkeypatch.delenv("ADVISOR_ADAPTER_REVISION", raising=False)
planner = create_tool_planner()
status = runtime_status(planner).to_dict()
assert isinstance(planner, MiniCPMTransformersPlanner)
assert status["backend"] == "minicpm-transformers"
assert status["loaded"] is False
assert status["adapter_id"] == DEFAULT_ADAPTER_ID
assert status["adapter_revision"] == DEFAULT_ADAPTER_REVISION
def test_create_tool_planner_accepts_explicit_rules_backend(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ADVISOR_MODEL_BACKEND", "rules")
planner = create_tool_planner()
assert isinstance(planner, RuleBasedPlanner)
assert runtime_status(planner).to_dict()["loaded"] is True
def test_create_tool_planner_accepts_adapter_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ADVISOR_MODEL_BACKEND", "minicpm-transformers")
monkeypatch.setenv("ADVISOR_MODEL_ID", "openbmb/MiniCPM5-1B")
monkeypatch.setenv("ADVISOR_ADAPTER_ID", DEFAULT_ADAPTER_ID)
monkeypatch.setenv("ADVISOR_ADAPTER_REVISION", "abc123")
planner = create_tool_planner()
status = runtime_status(planner).to_dict()
assert isinstance(planner, MiniCPMTransformersPlanner)
assert status["backend"] == "minicpm-transformers"
assert status["model_id"] == "openbmb/MiniCPM5-1B"
assert status["adapter_id"] == DEFAULT_ADAPTER_ID
assert status["adapter_revision"] == "abc123"
assert status["loaded"] is False
def test_create_tool_planner_rejects_unknown_backend(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ADVISOR_MODEL_BACKEND", "bogus")
with pytest.raises(RuntimeError, match="Unsupported"):
create_tool_planner()
def test_minicpm_status_is_lazy() -> None:
planner = MiniCPMTransformersPlanner("openbmb/MiniCPM5-1B", DEFAULT_ADAPTER_ID)
status = runtime_status(planner).to_dict()
assert status["backend"] == "minicpm-transformers"
assert status["adapter_id"] == DEFAULT_ADAPTER_ID
assert status["adapter_revision"] == ""
assert status["loaded"] is False
def test_zerogpu_disabled_leaves_function_unwrapped(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("ADVISOR_ZERO_GPU", raising=False)
def marker() -> str:
return "ok"
assert zero_gpu_enabled() is False
assert gpu_task(marker) is marker
def test_zerogpu_duration_validates_positive_values(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ADVISOR_ZERO_GPU_DURATION", "7")
assert zero_gpu_duration_seconds() == 7
monkeypatch.setenv("ADVISOR_ZERO_GPU_DURATION", "0")
with pytest.raises(RuntimeError, match="positive"):
zero_gpu_duration_seconds()
monkeypatch.setenv("ADVISOR_ZERO_GPU_DURATION", "121")
with pytest.raises(RuntimeError, match="at most 120"):
zero_gpu_duration_seconds()
def test_generation_inputs_drop_token_type_ids() -> None:
inputs = {"input_ids": [1], "attention_mask": [1], "token_type_ids": [0]}
_strip_unused_generation_inputs(inputs)
assert inputs == {"input_ids": [1], "attention_mask": [1]}
def test_minicpm_loader_matches_official_cuda_dtype() -> None:
model = _load_minicpm_causal_lm(FakeMiniCPMModel, "openbmb/MiniCPM5-1B", "cuda", FakeTorch())
assert model.model_id == "openbmb/MiniCPM5-1B"
assert model.kwargs == {"torch_dtype": "bfloat16", "trust_remote_code": True}
assert model.device == "cuda"
def test_minicpm_loader_uses_device_map_for_auto() -> None:
model = _load_minicpm_causal_lm(FakeMiniCPMModel, "openbmb/MiniCPM5-1B", "auto", FakeTorch())
assert model.kwargs == {
"torch_dtype": "bfloat16",
"device_map": "auto",
"trust_remote_code": True,
}
assert model.device is None
def test_minicpm_chat_inputs_follow_official_template_flow() -> None:
tokenizer = FakeTokenizer()
inputs = _minicpm_chat_inputs(
tokenizer,
[{"role": "user", "content": "hello"}],
enable_thinking=False,
device="cuda",
)
assert tokenizer.template_call == {
"messages": [{"role": "user", "content": "hello"}],
"tokenize": False,
"add_generation_prompt": True,
"enable_thinking": False,
}
assert tokenizer.tokenizer_call == {"prompts": ["rendered prompt"], "return_tensors": "pt"}
assert inputs == {"input_ids": [1], "attention_mask": [1], "device": "cuda"}
def test_minicpm_generation_kwargs_match_demo_sampling_policy() -> None:
inputs = {"input_ids": [1], "attention_mask": [1]}
sampled = _minicpm_generation_kwargs(inputs, max_new_tokens=32, temperature=0.9, top_p=0.95)
deterministic = _minicpm_generation_kwargs(inputs, max_new_tokens=32, temperature=0.0)
assert sampled == {
"input_ids": [1],
"attention_mask": [1],
"max_new_tokens": 32,
"temperature": 0.9,
"top_p": 0.95,
"do_sample": True,
}
assert deterministic == {
"input_ids": [1],
"attention_mask": [1],
"max_new_tokens": 32,
"do_sample": False,
}
def test_model_xml_fragment_is_normalized() -> None:
output = 'name="save_idea">{"title":"A","pitch":"B"}'
assert _normalize_xml_tool_output(output) == '<function name="save_idea">{"title":"A","pitch":"B"}</function>'
def test_resolve_device_keeps_auto_and_explicit_cpu() -> None:
assert _resolve_torch_device("auto", FakeTorch()) == "auto"
assert _resolve_torch_device("cpu", FakeTorch(cuda=True, mps=True)) == "cpu"
def test_resolve_device_prefers_cuda_then_mps_then_cpu(monkeypatch) -> None:
monkeypatch.delenv("ADVISOR_ZERO_GPU", raising=False)
assert _best_local_device(FakeTorch(cuda=True, mps=True)) == "cuda"
assert _best_local_device(FakeTorch(cuda=False, mps=True)) == "mps"
assert _best_local_device(FakeTorch(cuda=False, mps=False)) == "cpu"
# "local" resolves through the same ladder
assert _resolve_torch_device("local", FakeTorch(cuda=False, mps=True)) == "mps"
def test_resolve_device_unavailable_request_degrades_gracefully(monkeypatch) -> None:
monkeypatch.delenv("ADVISOR_ZERO_GPU", raising=False)
# asking for cuda on an MPS-only box lands on mps, not a crash
assert _resolve_torch_device("cuda", FakeTorch(cuda=False, mps=True)) == "mps"
def test_resolve_device_skips_cuda_under_zero_gpu(monkeypatch) -> None:
# In a ZeroGPU main process there is no local CUDA, and probing it is avoided.
monkeypatch.setenv("ADVISOR_ZERO_GPU", "1")
assert _best_local_device(FakeTorch(cuda=True, mps=False)) == "cpu"
def test_runtime_status_reports_configured_device() -> None:
planner = MiniCPMTransformersPlanner("openbmb/MiniCPM5-1B", device="local")
assert runtime_status(planner).to_dict()["device"] == "local"
assert runtime_status(RuleBasedPlanner()).to_dict()["device"] == ""