fix: require Agent App role (#37601)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
zyssyz123 2026-06-18 09:23:45 +08:00 committed by GitHub
parent 0dd966f7b8
commit 43192036fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 89 additions and 10 deletions

View File

@ -66,14 +66,30 @@ class AgentIdPath(BaseModel):
class AgentAppCreatePayload(BaseModel):
name: str = Field(..., min_length=1, description="Agent name")
description: str | None = Field(default=None, description="Agent description (max 400 chars)", max_length=400)
role: str = Field(default="", description="Agent role", max_length=255)
role: str = Field(..., min_length=1, description="Agent role", max_length=255)
icon_type: IconType | None = Field(default=None, description="Icon type")
icon: str | None = Field(default=None, description="Icon")
icon_background: str | None = Field(default=None, description="Icon background color")
@field_validator("role")
@classmethod
def validate_role(cls, value: str) -> str:
role = value.strip()
if not role:
raise ValueError("Agent role is required.")
return role
class AgentAppUpdatePayload(UpdateAppPayload):
role: str | None = Field(default=None, description="Agent role", max_length=255)
role: str = Field(..., min_length=1, description="Agent role", max_length=255)
@field_validator("role")
@classmethod
def validate_role(cls, value: str) -> str:
role = value.strip()
if not role:
raise ValueError("Agent role is required.")
return role
class AgentAppPublishedReferenceResponse(BaseModel):

View File

@ -11376,7 +11376,7 @@ Default namespace
| icon_background | string | Icon background color | No |
| icon_type | [IconType](#icontype) | Icon type | No |
| name | string | Agent name | Yes |
| role | string | Agent role | No |
| role | string | Agent role | Yes |
#### AgentAppFeaturesPayload
@ -11458,7 +11458,7 @@ default (the config form sends the full desired feature state on save).
| icon_type | [IconType](#icontype) | Icon type | No |
| max_active_requests | integer | Maximum active requests | No |
| name | string | App name | Yes |
| role | string | Agent role | No |
| role | string | Agent role | Yes |
| use_icon_as_answer_icon | boolean | Use icon as answer icon | No |
#### AgentAverageResponseTimeStatisticResponse

View File

@ -274,7 +274,13 @@ def test_agent_app_list_and_create_use_agent_route(
with app.test_request_context(
"/console/api/agent",
json={"name": "Iris", "description": "Agent app", "icon_type": "emoji", "icon": "robot"},
json={
"name": "Iris",
"description": "Agent app",
"role": "Coordinator",
"icon_type": "emoji",
"icon": "robot",
},
):
created, status = unwrap(AgentAppListApi.post)(AgentAppListApi(), "tenant-1", SimpleNamespace(id=account_id))
@ -287,6 +293,23 @@ def test_agent_app_list_and_create_use_agent_route(
create_call = cast(dict[str, object], captured["create"])
create_params = cast(Any, create_call["params"])
assert create_params.mode == "agent"
assert create_params.agent_role == "Coordinator"
def test_agent_app_create_requires_role(app: Flask, account_id: str) -> None:
with app.test_request_context(
"/console/api/agent",
json={"name": "Iris", "description": "Agent app", "icon_type": "emoji", "icon": "robot"},
):
with pytest.raises(ValueError, match="Field required"):
unwrap(AgentAppListApi.post)(AgentAppListApi(), "tenant-1", SimpleNamespace(id=account_id))
with app.test_request_context(
"/console/api/agent",
json={"name": "Iris", "description": "Agent app", "role": " ", "icon_type": "emoji", "icon": "robot"},
):
with pytest.raises(ValueError, match="Agent role is required"):
unwrap(AgentAppListApi.post)(AgentAppListApi(), "tenant-1", SimpleNamespace(id=account_id))
def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
@ -335,7 +358,7 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
with app.test_request_context(
"/console/api/agent/00000000-0000-0000-0000-000000000001",
json={"name": "Renamed", "description": "", "icon_type": "emoji", "icon": "R"},
json={"name": "Renamed", "description": "", "role": "Reviewer", "icon_type": "emoji", "icon": "R"},
):
updated = unwrap(AgentAppApi.put)(AgentAppApi(), "tenant-1", agent_id)
@ -347,6 +370,7 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
assert "bound_agent_id" not in updated
update_call = cast(dict[str, object], captured["update"])
assert update_call["app"] is app_model
assert cast(dict[str, object], update_call["args"])["role"] == "Reviewer"
deleted, status = unwrap(AgentAppApi.delete)(AgentAppApi(), "tenant-1", agent_id)
assert (deleted, status) == ("", 204)
@ -399,6 +423,45 @@ def test_agent_app_copy_uses_agent_id_and_returns_agent_detail(
}
def test_agent_app_update_rejects_empty_role(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
agent_id = "00000000-0000-0000-0000-000000000001"
app_model = _app_detail_obj(id="app-1", bound_agent_id=agent_id)
captured: dict[str, object] = {}
monkeypatch.setattr(
roster_controller.AgentRosterService,
"get_agent_app_model",
lambda _self, **kwargs: app_model,
)
monkeypatch.setattr(
roster_controller.AgentRosterService,
"get_app_backing_agent",
lambda _self, **kwargs: SimpleNamespace(id=agent_id, role="", active_config_snapshot_id=None),
)
monkeypatch.setattr(
roster_controller.FeatureService,
"get_system_features",
lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)),
)
class FakeAppService:
def get_app(self, app_obj: object) -> object:
return app_obj
def update_app(self, app_obj: object, args: dict[str, object]) -> object:
captured["update"] = {"app": app_obj, "args": args}
return _app_detail_obj(id="app-1", name=args["name"], bound_agent_id=agent_id)
monkeypatch.setattr(roster_controller, "AppService", FakeAppService)
with app.test_request_context(
"/console/api/agent/00000000-0000-0000-0000-000000000001",
json={"name": "Renamed", "description": "", "role": "", "icon_type": "emoji", "icon": "R"},
):
with pytest.raises(ValueError, match="String should have at least 1 character"):
unwrap(AgentAppApi.put)(AgentAppApi(), "tenant-1", agent_id)
def test_invite_options_get_parses_app_id(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, object] = {}

View File

@ -18,7 +18,7 @@ export type AgentAppCreatePayload = {
icon_background?: string | null
icon_type?: IconType | null
name: string
role?: string
role: string
}
export type AppDetailWithSite = {
@ -67,7 +67,7 @@ export type AgentAppUpdatePayload = {
icon_type?: IconType | null
max_active_requests?: number | null
name: string
role?: string | null
role: string
use_icon_as_answer_icon?: boolean | null
}

View File

@ -92,7 +92,7 @@ export const zAgentAppCreatePayload = z.object({
icon_background: z.string().nullish(),
icon_type: zIconType.nullish(),
name: z.string().min(1),
role: z.string().max(255).optional().default(''),
role: z.string().min(1).max(255),
})
/**
@ -105,7 +105,7 @@ export const zAgentAppUpdatePayload = z.object({
icon_type: zIconType.nullish(),
max_active_requests: z.int().nullish(),
name: z.string().min(1),
role: z.string().max(255).nullish(),
role: z.string().min(1).max(255),
use_icon_as_answer_icon: z.boolean().nullish(),
})