Agent/JOY_JSON mode returns {ok,count,items,_joy} for array results
(board ls etc). Compat escape: JOY_LIST_ROOT=array keeps bare arrays.
140 lines
4.2 KiB
Python
140 lines
4.2 KiB
Python
"""AI 原生契约:JSON、exit code、可发现、agent 模式包装。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
SRC = Path(__file__).parent.parent / "src"
|
||
sys.path.insert(0, str(SRC))
|
||
|
||
|
||
def run_joy(args: list[str], *, env_extra: dict | None = None, identity: str = "tux") -> subprocess.CompletedProcess:
|
||
env = os.environ.copy()
|
||
env["JOY_IDENTITY"] = identity
|
||
env["PYTHONPATH"] = str(SRC)
|
||
# 测试默认 agent 模式(与管道一致)
|
||
env.setdefault("JOY_JSON", "1")
|
||
if env_extra:
|
||
env.update(env_extra)
|
||
return subprocess.run(
|
||
[sys.executable, "-m", "joy"] + args,
|
||
capture_output=True,
|
||
text=True,
|
||
env=env,
|
||
timeout=45,
|
||
)
|
||
|
||
|
||
def test_help_json_default_in_agent_mode():
|
||
r = run_joy(["help"])
|
||
assert r.returncode == 0
|
||
data = json.loads(r.stdout)
|
||
assert data.get("name") == "joy"
|
||
assert "categories" in data
|
||
assert data.get("ai_native")
|
||
|
||
|
||
def test_help_text_when_forced():
|
||
r = run_joy(["help", "--text"], env_extra={"JOY_JSON": "0", "JOY_AGENT": "0"})
|
||
# --text 优先;若仍 agent 环境用 --text on help via cli
|
||
r = run_joy(["help"], env_extra={"JOY_JSON": "", "JOY_AGENT": ""})
|
||
# 无 env 时非 TTY 仍 agent — 用显式 --text flag 路径:
|
||
r = run_joy(["--text", "help"])
|
||
assert r.returncode == 0
|
||
assert "joy" in r.stdout
|
||
assert not r.stdout.strip().startswith("{")
|
||
|
||
|
||
def test_usage_exit_code_2():
|
||
r = run_joy(["board"])
|
||
assert r.returncode == 2
|
||
err = json.loads(r.stderr)
|
||
assert err["code"] == "E_USAGE"
|
||
assert err.get("ok") is False
|
||
|
||
|
||
def test_unknown_exit_code_1():
|
||
r = run_joy(["no-such-cmd-xyz"])
|
||
assert r.returncode == 1
|
||
err = json.loads(r.stderr)
|
||
assert err["code"] == "E_UNKNOWN_CMD"
|
||
|
||
|
||
def test_manifest_ai_native_block():
|
||
r = run_joy(["tools", "--manifest"])
|
||
assert r.returncode == 0
|
||
data = json.loads(r.stdout)
|
||
assert "ai_native" in data
|
||
assert "stdout_json_success" in data["ai_native"]["principles"]
|
||
assert "pipeline" in data["ai_native"]["core_abi"]
|
||
assert data["_joy"]["schema_version"] == 1
|
||
|
||
|
||
def test_string_wrapped_as_object_in_agent_mode():
|
||
"""agent 模式下若命令仍返回 str,cli 应包装。"""
|
||
from joy.cli import _normalize_result
|
||
|
||
out = _normalize_result("hello", command_name="demo", as_agent=True)
|
||
assert isinstance(out, dict)
|
||
assert out["content"] == "hello"
|
||
assert out["_joy"]["schema_version"] == 1
|
||
|
||
|
||
def test_work_transcript_structured():
|
||
r = run_joy(["work", "devops", "transcript", "2"])
|
||
# 可能无 work-transcript 工具,但结构应是 JSON
|
||
if r.returncode == 0 and r.stdout.strip():
|
||
data = json.loads(r.stdout)
|
||
assert "content" in data or "vm" in data
|
||
assert data.get("_joy", {}).get("command") == "work" or "vm" in data
|
||
|
||
|
||
def test_version_json_agent():
|
||
r = run_joy(["--version"])
|
||
assert r.returncode == 0
|
||
data = json.loads(r.stdout)
|
||
assert data.get("name") == "joy"
|
||
assert "version" in data
|
||
|
||
|
||
def test_perm_error_json():
|
||
r = run_joy(["tux", "pending"], identity="wenpai")
|
||
assert r.returncode == 1
|
||
err = json.loads(r.stderr)
|
||
assert err["code"] == "E_PERM"
|
||
|
||
|
||
def test_list_wrapped_in_agent_mode():
|
||
from joy.cli import _normalize_result
|
||
|
||
out = _normalize_result([{"id": 1}, {"id": 2}], command_name="board", as_agent=True)
|
||
assert isinstance(out, dict)
|
||
assert out["format"] == "list"
|
||
assert out["count"] == 2
|
||
assert out["items"][0]["id"] == 1
|
||
assert out["_joy"].get("list_wrapped") is True
|
||
|
||
|
||
def test_list_raw_compat_env():
|
||
"""JOY_LIST_ROOT=array 保持根数组(旧脚本兼容)。"""
|
||
r = run_joy(["board", "ls"], env_extra={"JOY_JSON": "1", "JOY_LIST_ROOT": "array"})
|
||
if r.returncode == 0 and r.stdout.strip():
|
||
data = json.loads(r.stdout)
|
||
assert isinstance(data, list), f"expected raw list, got {type(data)}"
|
||
|
||
|
||
def test_board_ls_agent_object():
|
||
r = run_joy(["board", "ls"])
|
||
if r.returncode != 0:
|
||
pytest.skip("board unavailable")
|
||
data = json.loads(r.stdout)
|
||
assert isinstance(data, dict)
|
||
assert "items" in data
|
||
assert data.get("format") == "list"
|
||
assert data.get("_joy", {}).get("schema_version") == 1
|