diff --git a/src/xskill/ecosystems/_shared.py b/src/xskill/ecosystems/_shared.py index 378bc92..c9ccd90 100644 --- a/src/xskill/ecosystems/_shared.py +++ b/src/xskill/ecosystems/_shared.py @@ -669,6 +669,14 @@ def submit_trajectory( ``ingest.mask_patterns``(默认空 = 不替换)。命中段在写 md 之前替换为 占位符——剥掉评测 harness 的固定外壳,防聚类被任务外壳吸住。 """ + if not isinstance(content, str) or not content.strip(): + raise ValueError("trajectory content must be non-empty") + + if format == "json": + data = json.loads(content) + if not data: + raise ValueError("trajectory content must be non-empty") + traj_dir = Path(traj_dir) if traj_dir else get_traj_dir() traj_dir.mkdir(parents=True, exist_ok=True) diff --git a/tests/test_trajectory_submit_validation.py b/tests/test_trajectory_submit_validation.py new file mode 100644 index 0000000..4e893d4 --- /dev/null +++ b/tests/test_trajectory_submit_validation.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from types import SimpleNamespace + +import pytest +from fastapi import HTTPException + +from xskill.ecosystems import submit_trajectory + + +@pytest.mark.parametrize( + ("content", "format"), + [ + ("", "markdown"), + (" \n\t", "markdown"), + ("", "raw"), + (" \n\t", "raw"), + ("{}", "json"), + ], +) +def test_submit_rejects_empty_trajectories_without_writing_files( + tmp_path, content, format +): + with pytest.raises(ValueError, match="content"): + submit_trajectory(content=content, format=format, traj_dir=tmp_path) + + assert list(tmp_path.iterdir()) == [] + + +def test_submit_accepts_json_with_a_message(tmp_path): + result = submit_trajectory( + content='{"messages":[{"role":"user","content":"hello"}]}', + format="json", + traj_dir=tmp_path, + ) + + assert result["status"] == "stored" + assert "hello" in (tmp_path / "traj_0001.md").read_text(encoding="utf-8") + + +@pytest.mark.asyncio +@pytest.mark.parametrize("content", ["", " \n\t"]) +async def test_api_rejects_empty_content_before_resolving_watch_directory( + tmp_path, monkeypatch, content +): + import xskill.api.app as api_app + import xskill.ecosystems._shared as shared + + monkeypatch.setattr(api_app, "_ensure_loaded", lambda: None) + monkeypatch.setattr(api_app, "_config", {}) + monkeypatch.setattr(api_app, "_skill_dir", tmp_path / "skill") + def fail_get_traj_dir(): + raise AssertionError("invalid content should fail before resolving a watch directory") + + monkeypatch.setattr(shared, "get_traj_dir", fail_get_traj_dir) + + app = api_app.create_app(home_root=tmp_path) + route = next( + route for route in app.routes + if getattr(route, "path", None) == "/api/v1/trajectories/submit" + ) + + with pytest.raises(HTTPException) as exc_info: + await route.endpoint(SimpleNamespace( + content=content, + format="markdown", + metadata=None, + traj_id=None, + )) + + assert exc_info.value.status_code == 400 + assert list(tmp_path.iterdir()) == []