Repository navigation
Expand file tree
/
Copy pathtest_deployments_api.py
More file actions
184 lines (149 loc) · 6.75 KB
/
Copy pathtest_deployments_api.py
File metadata and controls
184 lines (149 loc) · 6.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Unit tests for Agent Deployment route handlers."""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any
from unittest.mock import AsyncMock
from fastapi import FastAPI
from fastapi.testclient import TestClient
from nemo_agents_plugin.api.v2 import deployments as deployments_router_module
from nemo_agents_plugin.api.v2.dependencies import get_entity_client
from nemo_agents_plugin.entities import NEMO_AGENTS_SPEC_CONFIG_FORMAT, Agent, AgentDeployment, DeploymentStatus
from nemo_platform_plugin.entity_client import NemoEntityConflictError, NemoEntityNotFoundError
NOW = datetime.now(timezone.utc)
def _fabric_agent_config() -> dict[str, Any]:
return {
"config_format": NEMO_AGENTS_SPEC_CONFIG_FORMAT,
"name": "fabric-agent",
"description": "Fabric-backed agent",
"default_harness": "hermes",
"harnesses": {
"hermes": {
"kind": "hermes",
},
},
"models": {
"default": {
"provider": "openai",
"model": "openai/gpt-5.4",
},
},
}
def _make_agent(
*,
name: str = "fabric-agent",
workspace: str = "default",
config: dict[str, Any] | None = None,
config_format: str = NEMO_AGENTS_SPEC_CONFIG_FORMAT,
) -> Agent:
agent = Agent(
name=name,
workspace=workspace,
config=config or _fabric_agent_config(),
config_format=config_format,
)
agent._id = f"agent-{name}-id"
agent._created_at = NOW
return agent
def _make_deployment(
*,
name: str = "fabric-dep",
workspace: str = "default",
agent: str = "fabric-agent",
status: DeploymentStatus = "pending",
) -> AgentDeployment:
deployment = AgentDeployment(name=name, workspace=workspace, agent=agent, status=status)
deployment._id = f"deployment-{name}-id"
deployment._created_at = NOW
return deployment
def _test_client(mock_entity_client: AsyncMock) -> TestClient:
app = FastAPI()
app.include_router(
deployments_router_module.router,
prefix="/apis/agents/v2/workspaces/{workspace}",
)
app.dependency_overrides[get_entity_client] = lambda: mock_entity_client
return TestClient(app, raise_server_exceptions=False)
class TestCreateDeployment:
def test_create_preserves_platform_agent_config(self) -> None:
mock_entity_client = AsyncMock()
mock_entity_client.get = AsyncMock(return_value=_make_agent())
async def _save_deployment(deployment: AgentDeployment) -> AgentDeployment:
deployment._id = f"deployment-{deployment.name}-id"
deployment._created_at = NOW
return deployment
mock_entity_client.create = AsyncMock(side_effect=_save_deployment)
client = _test_client(mock_entity_client)
resp = client.post(
"/apis/agents/v2/workspaces/default/deployments",
json={"agent": "fabric-agent", "name": "fabric-dep"},
)
assert resp.status_code == 201
created_deployment: AgentDeployment = mock_entity_client.create.call_args[0][0]
assert created_deployment.config["config_format"] == NEMO_AGENTS_SPEC_CONFIG_FORMAT
assert created_deployment.config["environment"]["provider"] == "local"
assert "functions" not in created_deployment.config
assert "workflow" not in created_deployment.config
def test_create_rejects_invalid_platform_agent_config(self) -> None:
config = _fabric_agent_config()
config["default_harness"] = "missing"
mock_entity_client = AsyncMock()
mock_entity_client.get = AsyncMock(return_value=_make_agent(config=config))
client = _test_client(mock_entity_client)
resp = client.post(
"/apis/agents/v2/workspaces/default/deployments",
json={"agent": "fabric-agent", "name": "fabric-dep"},
)
assert resp.status_code == 400
assert "Invalid agent config" in resp.json()["detail"]
mock_entity_client.create.assert_not_called()
class TestDeleteDeployment:
def test_delete_marks_deployment_deleting(self) -> None:
mock_entity_client = AsyncMock()
mock_entity_client.get = AsyncMock(return_value=_make_deployment(status="starting"))
mock_entity_client.update = AsyncMock(return_value=None)
client = _test_client(mock_entity_client)
resp = client.delete("/apis/agents/v2/workspaces/default/deployments/fabric-dep")
assert resp.status_code == 204
updated: AgentDeployment = mock_entity_client.update.call_args[0][0]
assert updated.status == "deleting"
def test_delete_retries_concurrent_update_conflict(self) -> None:
mock_entity_client = AsyncMock()
mock_entity_client.get = AsyncMock(
side_effect=[
_make_deployment(status="pending"),
_make_deployment(status="starting"),
]
)
mock_entity_client.update = AsyncMock(side_effect=[NemoEntityConflictError("conflict"), None])
client = _test_client(mock_entity_client)
resp = client.delete("/apis/agents/v2/workspaces/default/deployments/fabric-dep")
assert resp.status_code == 204
assert mock_entity_client.get.await_count == 2
assert mock_entity_client.update.await_count == 2
def test_delete_returns_success_when_entity_disappears_during_retry(self) -> None:
mock_entity_client = AsyncMock()
mock_entity_client.get = AsyncMock(
side_effect=[
_make_deployment(status="pending"),
NemoEntityNotFoundError("gone"),
]
)
mock_entity_client.update = AsyncMock(side_effect=NemoEntityConflictError("conflict"))
client = _test_client(mock_entity_client)
resp = client.delete("/apis/agents/v2/workspaces/default/deployments/fabric-dep")
assert resp.status_code == 204
def test_delete_returns_409_when_conflicts_exhausted(self) -> None:
mock_entity_client = AsyncMock()
mock_entity_client.get = AsyncMock(
side_effect=[
_make_deployment(status="pending")
for _ in range(deployments_router_module._DELETE_MARK_ATTEMPTS) # noqa: SLF001
]
)
mock_entity_client.update = AsyncMock(side_effect=NemoEntityConflictError("conflict"))
client = _test_client(mock_entity_client)
resp = client.delete("/apis/agents/v2/workspaces/default/deployments/fabric-dep")
assert resp.status_code == 409
assert mock_entity_client.update.await_count == deployments_router_module._DELETE_MARK_ATTEMPTS # noqa: SLF001