|
17 | 17 | from openhands.agent_server.api import create_app |
18 | 18 | from openhands.agent_server.config import Config |
19 | 19 | from openhands.agent_server.persistence import reset_stores |
20 | | -from openhands.sdk.profiles import AgentProfileStore, OpenHandsAgentProfile |
| 20 | +from openhands.sdk.llm import LLM |
| 21 | +from openhands.sdk.llm.llm_profile_store import LLMProfileStore |
| 22 | +from openhands.sdk.profiles import ( |
| 23 | + ACPAgentProfile, |
| 24 | + AgentProfileStore, |
| 25 | + OpenHandsAgentProfile, |
| 26 | +) |
21 | 27 |
|
22 | 28 |
|
23 | 29 | @pytest.fixture |
@@ -676,3 +682,139 @@ def test_save_at_limit_returns_409(client, store, monkeypatch): |
676 | 682 | response = client.post("/api/agent-profiles/second", json={"llm_profile_ref": "y"}) |
677 | 683 | assert response.status_code == 409 |
678 | 684 | assert "limit" in response.json()["detail"].lower() |
| 685 | + |
| 686 | + |
| 687 | +# ── Materialize (resolve dry-run) ──────────────────────────────────────────── |
| 688 | + |
| 689 | + |
| 690 | +@pytest.fixture |
| 691 | +def temp_llm_profiles_dir(): |
| 692 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 693 | + llm_dir = Path(tmpdir) / "llm-profiles" |
| 694 | + llm_dir.mkdir(parents=True, exist_ok=True) |
| 695 | + yield llm_dir |
| 696 | + |
| 697 | + |
| 698 | +@pytest.fixture |
| 699 | +def client_with_llm_store( |
| 700 | + temp_agent_profiles_dir, temp_settings_dir, temp_llm_profiles_dir, monkeypatch |
| 701 | +): |
| 702 | + """Test client with isolated agent-profile/settings/llm-profile dirs, no cipher.""" |
| 703 | + reset_stores() |
| 704 | + monkeypatch.setenv("OH_PERSISTENCE_DIR", str(temp_settings_dir)) |
| 705 | + config = Config(static_files_path=None, session_api_keys=[], secret_key=None) |
| 706 | + app = create_app(config) |
| 707 | + with ( |
| 708 | + patch( |
| 709 | + "openhands.agent_server.agent_profiles_router.AgentProfileStore", |
| 710 | + lambda: AgentProfileStore(base_dir=temp_agent_profiles_dir), |
| 711 | + ), |
| 712 | + patch( |
| 713 | + "openhands.agent_server.agent_profiles_router.LLMProfileStore", |
| 714 | + lambda: LLMProfileStore(base_dir=temp_llm_profiles_dir), |
| 715 | + ), |
| 716 | + ): |
| 717 | + yield TestClient(app) |
| 718 | + reset_stores() |
| 719 | + |
| 720 | + |
| 721 | +@pytest.fixture |
| 722 | +def llm_store(temp_llm_profiles_dir): |
| 723 | + return LLMProfileStore(base_dir=temp_llm_profiles_dir) |
| 724 | + |
| 725 | + |
| 726 | +def test_materialize_valid_openhands_profile(client_with_llm_store, store, llm_store): |
| 727 | + """Valid OpenHands profile with a resolved LLM returns 200 + valid=True.""" |
| 728 | + llm_store.save("base-llm", LLM(model="gpt-4o"), include_secrets=True) |
| 729 | + store.save(OpenHandsAgentProfile(name="p", llm_profile_ref="base-llm")) |
| 730 | + |
| 731 | + response = client_with_llm_store.post("/api/agent-profiles/p/materialize") |
| 732 | + |
| 733 | + assert response.status_code == 200 |
| 734 | + body = response.json() |
| 735 | + assert body["valid"] is True |
| 736 | + assert body["agent_kind"] == "openhands" |
| 737 | + assert body["llm_profile_ref"] == "base-llm" |
| 738 | + assert body["llm_profile_resolved"] is True |
| 739 | + assert body["errors"] == [] |
| 740 | + assert body["resolved_settings"] is not None |
| 741 | + assert body["dangling_mcp_server_refs"] == [] |
| 742 | + |
| 743 | + |
| 744 | +def test_materialize_valid_acp_profile(client_with_llm_store, store): |
| 745 | + """Valid ACP profile returns 200 + valid=True (no LLM ref needed).""" |
| 746 | + store.save(ACPAgentProfile(name="acp-p", acp_server="codex", acp_model="gpt-5.5")) |
| 747 | + |
| 748 | + response = client_with_llm_store.post("/api/agent-profiles/acp-p/materialize") |
| 749 | + |
| 750 | + assert response.status_code == 200 |
| 751 | + body = response.json() |
| 752 | + assert body["valid"] is True |
| 753 | + assert body["agent_kind"] == "acp" |
| 754 | + assert body["errors"] == [] |
| 755 | + assert body["resolved_settings"] is not None |
| 756 | + |
| 757 | + |
| 758 | +def test_materialize_dangling_llm_ref(client_with_llm_store, store): |
| 759 | + """A profile referencing a missing LLM profile returns 200, valid=False.""" |
| 760 | + store.save(OpenHandsAgentProfile(name="p", llm_profile_ref="nonexistent")) |
| 761 | + |
| 762 | + response = client_with_llm_store.post("/api/agent-profiles/p/materialize") |
| 763 | + |
| 764 | + assert response.status_code == 200 |
| 765 | + body = response.json() |
| 766 | + assert body["valid"] is False |
| 767 | + assert body["llm_profile_ref"] == "nonexistent" |
| 768 | + assert body["llm_profile_resolved"] is False |
| 769 | + assert body["resolved_settings"] is None |
| 770 | + assert any("nonexistent" in e for e in body["errors"]) |
| 771 | + |
| 772 | + |
| 773 | +def test_materialize_dangling_mcp_ref(client_with_llm_store, store, llm_store): |
| 774 | + """A profile with a missing MCP server ref returns 200, valid=False.""" |
| 775 | + llm_store.save("base-llm", LLM(model="gpt-4o"), include_secrets=True) |
| 776 | + store.save( |
| 777 | + OpenHandsAgentProfile( |
| 778 | + name="p", |
| 779 | + llm_profile_ref="base-llm", |
| 780 | + mcp_server_refs=["missing-server"], |
| 781 | + ) |
| 782 | + ) |
| 783 | + |
| 784 | + response = client_with_llm_store.post("/api/agent-profiles/p/materialize") |
| 785 | + |
| 786 | + assert response.status_code == 200 |
| 787 | + body = response.json() |
| 788 | + assert body["valid"] is False |
| 789 | + assert body["dangling_mcp_server_refs"] == ["missing-server"] |
| 790 | + assert body["resolved_settings"] is None |
| 791 | + |
| 792 | + |
| 793 | +def test_materialize_unknown_name_returns_404(client_with_llm_store): |
| 794 | + """Materializing an unknown profile name returns 404.""" |
| 795 | + response = client_with_llm_store.post("/api/agent-profiles/ghost/materialize") |
| 796 | + |
| 797 | + assert response.status_code == 404 |
| 798 | + assert "not found" in response.json()["detail"].lower() |
| 799 | + |
| 800 | + |
| 801 | +def test_materialize_no_raw_secrets_in_resolved_settings( |
| 802 | + client_with_llm_store, store, llm_store |
| 803 | +): |
| 804 | + """resolved_settings must not contain raw API key values.""" |
| 805 | + raw_key = "sk-secret-key-should-not-appear" |
| 806 | + from pydantic import SecretStr |
| 807 | + |
| 808 | + llm_store.save( |
| 809 | + "base-llm", |
| 810 | + LLM(model="gpt-4o", api_key=SecretStr(raw_key)), |
| 811 | + include_secrets=True, |
| 812 | + ) |
| 813 | + store.save(OpenHandsAgentProfile(name="p", llm_profile_ref="base-llm")) |
| 814 | + |
| 815 | + response = client_with_llm_store.post("/api/agent-profiles/p/materialize") |
| 816 | + |
| 817 | + assert response.status_code == 200 |
| 818 | + body = response.json() |
| 819 | + assert body["valid"] is True |
| 820 | + assert raw_key not in response.text |
0 commit comments