feat: add versioned skill configuration
This commit is contained in:
1
backend/apps/skills/tests/__init__.py
Normal file
1
backend/apps/skills/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
58
backend/apps/skills/tests/test_services.py
Normal file
58
backend/apps/skills/tests/test_services.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import pytest
|
||||
|
||||
from apps.skills.models import SkillDefinition, SkillVersion
|
||||
from apps.skills.services import (
|
||||
SkillValidationError,
|
||||
render_prompt,
|
||||
validate_skill_version,
|
||||
)
|
||||
|
||||
VALID_INPUT_SCHEMA = {
|
||||
"type": "object",
|
||||
"properties": {"problem": {"type": "string"}},
|
||||
"required": ["problem"],
|
||||
}
|
||||
VALID_OUTPUT_SCHEMA = {
|
||||
"type": "object",
|
||||
"properties": {"summary": {"type": "string"}},
|
||||
"required": ["summary"],
|
||||
}
|
||||
|
||||
|
||||
def make_version(**overrides):
|
||||
definition = SkillDefinition.objects.create(code="summarize", name="问题摘要")
|
||||
values = {
|
||||
"definition": definition,
|
||||
"version": 1,
|
||||
"prompt_template": "请总结:{{ input.problem }}",
|
||||
"input_schema": VALID_INPUT_SCHEMA,
|
||||
"output_schema": VALID_OUTPUT_SCHEMA,
|
||||
"example_input": {"problem": "无法登录"},
|
||||
}
|
||||
values.update(overrides)
|
||||
return SkillVersion.objects.create(**values)
|
||||
|
||||
|
||||
def test_validates_and_renders_restricted_template(db):
|
||||
version = make_version()
|
||||
|
||||
validate_skill_version(version)
|
||||
|
||||
assert render_prompt(version.prompt_template, {"problem": "无法登录"}) == "请总结:无法登录"
|
||||
|
||||
|
||||
def test_rejects_expression_or_missing_variable(db):
|
||||
version = make_version(prompt_template="{{ steps.previous.output }}")
|
||||
|
||||
with pytest.raises(SkillValidationError, match="不允许的模板变量"):
|
||||
validate_skill_version(version)
|
||||
|
||||
with pytest.raises(SkillValidationError, match="模板变量不存在"):
|
||||
render_prompt("{{ input.environment }}", {"problem": "无法登录"})
|
||||
|
||||
|
||||
def test_rejects_example_that_does_not_match_schema(db):
|
||||
version = make_version(example_input={"problem": 123})
|
||||
|
||||
with pytest.raises(SkillValidationError, match="示例输入不符合"):
|
||||
validate_skill_version(version)
|
||||
79
backend/apps/skills/tests/test_skills_api.py
Normal file
79
backend/apps/skills/tests/test_skills_api.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.management import call_command
|
||||
|
||||
VALID_SKILL = {
|
||||
"code": "issue-structure",
|
||||
"name": "问题结构化",
|
||||
"description": "提取问题关键信息",
|
||||
"prompt_template": "请分析:{{ input.problem }}",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {"problem": {"type": "string"}},
|
||||
"required": ["problem"],
|
||||
},
|
||||
"output_schema": {
|
||||
"type": "object",
|
||||
"properties": {"summary": {"type": "string"}},
|
||||
"required": ["summary"],
|
||||
},
|
||||
"example_input": {"problem": "采集任务没有进度"},
|
||||
}
|
||||
|
||||
|
||||
def login_admin(client):
|
||||
call_command("bootstrap_governance")
|
||||
user = get_user_model().objects.get(username="admin")
|
||||
client.force_login(user)
|
||||
return user
|
||||
|
||||
|
||||
def test_skill_draft_publish_clone_and_rollback(client, db):
|
||||
login_admin(client)
|
||||
created = client.post("/api/skills", data=VALID_SKILL, content_type="application/json")
|
||||
assert created.status_code == 200
|
||||
skill_id = created.json()["id"]
|
||||
|
||||
published_v1 = client.post(f"/api/skills/{skill_id}/publish")
|
||||
assert published_v1.status_code == 200
|
||||
assert published_v1.json()["version"] == 1
|
||||
assert published_v1.json()["status"] == "published"
|
||||
|
||||
draft_v2 = client.post(f"/api/skills/{skill_id}/draft")
|
||||
assert draft_v2.status_code == 200
|
||||
assert draft_v2.json()["version"] == 2
|
||||
updated = client.patch(
|
||||
f"/api/skills/{skill_id}/draft",
|
||||
data={"prompt_template": "新版:{{ input.problem }}"},
|
||||
content_type="application/json",
|
||||
)
|
||||
assert updated.status_code == 200
|
||||
assert updated.json()["prompt_template"].startswith("新版")
|
||||
|
||||
published_v2 = client.post(f"/api/skills/{skill_id}/publish")
|
||||
assert published_v2.status_code == 200
|
||||
rolled_back = client.post(f"/api/skills/{skill_id}/rollback/1")
|
||||
assert rolled_back.status_code == 200
|
||||
assert rolled_back.json()["active_version"] == 1
|
||||
|
||||
|
||||
def test_invalid_schema_or_template_cannot_be_saved(client, db):
|
||||
login_admin(client)
|
||||
created = client.post("/api/skills", data=VALID_SKILL, content_type="application/json")
|
||||
skill_id = created.json()["id"]
|
||||
|
||||
invalid = client.patch(
|
||||
f"/api/skills/{skill_id}/draft",
|
||||
data={"prompt_template": "{{ input.problem.upper() }}"},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
assert invalid.status_code == 400
|
||||
|
||||
|
||||
def test_skill_permission_is_required(client, db):
|
||||
user = get_user_model().objects.create_user(username="requester", password="password")
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get("/api/skills")
|
||||
|
||||
assert response.status_code == 403
|
||||
Reference in New Issue
Block a user