314 lines
9.4 KiB
Python
314 lines
9.4 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from agent import runner
|
|
|
|
|
|
def test_runner_help_text(capsys: pytest.CaptureFixture[str]) -> None:
|
|
with pytest.raises(SystemExit, match="0"):
|
|
runner.main(["--help"])
|
|
|
|
help_text = capsys.readouterr().out
|
|
assert "--prompt" in help_text
|
|
assert "--image" in help_text
|
|
assert (
|
|
"--provider {anthropic,codex-cli,dashscope,gemini,openai,openrouter,deepseek}" in help_text
|
|
)
|
|
assert "--openai-transport {http,websocket}" in help_text
|
|
assert "--data-dir DATA_DIR" in help_text
|
|
assert "--category CATEGORY" in help_text
|
|
assert "--max-cost-usd MAX_COST_USD" in help_text
|
|
assert "--flash" not in help_text
|
|
assert "--hybrid-sdk" not in help_text
|
|
assert "--openai-reasoning-summary" not in help_text
|
|
assert "Currently supported only with --provider openai." not in help_text
|
|
|
|
|
|
def test_runner_rejects_removed_flash_flag(capsys: pytest.CaptureFixture[str]) -> None:
|
|
with pytest.raises(SystemExit, match="2"):
|
|
runner.main(["--prompt", "test", "--flash"])
|
|
|
|
assert "unrecognized arguments: --flash" in capsys.readouterr().err
|
|
|
|
|
|
def test_runner_rejects_removed_collection_flag(
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
with pytest.raises(SystemExit, match="2"):
|
|
runner.main(["--prompt", "test", "--collection", "dataset"])
|
|
|
|
assert "unrecognized arguments: --collection dataset" in capsys.readouterr().err
|
|
|
|
|
|
def test_runner_dump_provider_payload_supports_sdk(
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
exit_code = runner.main(
|
|
[
|
|
"--prompt",
|
|
"test prompt",
|
|
"--dump-provider-payload",
|
|
]
|
|
)
|
|
|
|
assert exit_code == 0
|
|
payload = json.loads(capsys.readouterr().out)
|
|
docs_message = payload["input"][0]["content"][0]["text"]
|
|
assert "## docs/sdk/references/quickstart.md" in docs_message
|
|
assert "Import from `sdk` in `model.py`." in docs_message
|
|
|
|
|
|
def test_runner_accepts_openai_api_keys_env(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
captured: dict[str, object] = {}
|
|
|
|
async def _fake_run_from_input(*args, **kwargs) -> int: # type: ignore[no-untyped-def]
|
|
captured.update(kwargs)
|
|
return 0
|
|
|
|
monkeypatch.setattr(runner, "run_from_input", _fake_run_from_input)
|
|
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
monkeypatch.setenv("OPENAI_API_KEYS", "sk-first,sk-second")
|
|
monkeypatch.setenv("ARTICRAFT_MAX_COST_USD", "1.25")
|
|
monkeypatch.setenv("ARTICRAFT_THINKING_LEVEL", "xhigh")
|
|
|
|
exit_code = runner.main(
|
|
[
|
|
"--prompt",
|
|
"test prompt",
|
|
"--provider",
|
|
"openai",
|
|
"--repo-root",
|
|
str(tmp_path),
|
|
]
|
|
)
|
|
|
|
assert exit_code == 0
|
|
assert captured["max_cost_usd"] == 1.25
|
|
assert captured["thinking_level"] == "xhigh"
|
|
|
|
|
|
def test_runner_rejects_codex_cli_without_explicit_model(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
async def _fake_run_from_input(*args, **kwargs) -> int: # type: ignore[no-untyped-def]
|
|
raise AssertionError("run_from_input should not be called")
|
|
|
|
monkeypatch.setattr(runner, "run_from_input", _fake_run_from_input)
|
|
monkeypatch.delenv("ARTICRAFT_CODEX_MODEL", raising=False)
|
|
|
|
exit_code = runner.main(
|
|
[
|
|
"--prompt",
|
|
"test prompt",
|
|
"--provider",
|
|
"codex-cli",
|
|
"--repo-root",
|
|
str(tmp_path),
|
|
]
|
|
)
|
|
|
|
assert exit_code == 1
|
|
assert "requires an explicit model" in capsys.readouterr().err
|
|
|
|
|
|
def test_runner_dump_provider_payload_supports_openrouter(
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
exit_code = runner.main(
|
|
[
|
|
"--prompt",
|
|
"test prompt",
|
|
"--provider",
|
|
"openrouter",
|
|
"--thinking",
|
|
"high",
|
|
"--dump-provider-payload",
|
|
]
|
|
)
|
|
|
|
assert exit_code == 0
|
|
payload = json.loads(capsys.readouterr().out)
|
|
assert payload["base_url"] == "https://openrouter.ai/api/v1"
|
|
assert payload["model"] == "tencent/hy3-preview:free"
|
|
assert payload["extra_body"]["reasoning"]["enabled"] is True
|
|
assert payload["extra_body"]["reasoning"]["effort"] == "high"
|
|
assert payload["messages"][0]["role"] == "system"
|
|
assert "<process>" in payload["messages"][0]["content"]
|
|
assert (
|
|
"Work evidence-first. Before editing, read `model.py`" in payload["messages"][0]["content"]
|
|
)
|
|
assert payload["messages"][1]["role"] == "user"
|
|
|
|
|
|
def test_runner_dump_provider_payload_supports_dashscope(
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
exit_code = runner.main(
|
|
[
|
|
"--prompt",
|
|
"test prompt",
|
|
"--provider",
|
|
"dashscope",
|
|
"--dump-provider-payload",
|
|
]
|
|
)
|
|
|
|
assert exit_code == 0
|
|
payload = json.loads(capsys.readouterr().out)
|
|
assert payload["base_url"] == "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
assert payload["model"] == "qwen3.6-flash"
|
|
assert payload["extra_body"] == {"enable_thinking": True}
|
|
assert payload["messages"][0]["role"] == "system"
|
|
assert "<process>" in payload["messages"][0]["content"]
|
|
assert payload["messages"][1]["role"] == "user"
|
|
|
|
|
|
def test_runner_dump_provider_payload_supports_anthropic(
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
exit_code = runner.main(
|
|
[
|
|
"--prompt",
|
|
"test prompt",
|
|
"--provider",
|
|
"anthropic",
|
|
"--thinking",
|
|
"high",
|
|
"--dump-provider-payload",
|
|
]
|
|
)
|
|
|
|
assert exit_code == 0
|
|
payload = json.loads(capsys.readouterr().out)
|
|
assert payload["base_url"] == "https://api.anthropic.com"
|
|
assert payload["model"] == "claude-opus-4-7"
|
|
assert payload["thinking"] == {"type": "adaptive"}
|
|
assert payload["output_config"] == {"effort": "high"}
|
|
system_text = payload["system"][0]["text"]
|
|
assert payload["system"][0]["cache_control"] == {"type": "ephemeral"}
|
|
assert "<process>" in system_text
|
|
assert "Work evidence-first. Before editing, read `model.py`" in system_text
|
|
assert payload["messages"][0]["role"] == "user"
|
|
|
|
|
|
def test_runner_dump_provider_payload_uses_openai_env_defaults(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
monkeypatch.setenv("ARTICRAFT_MODEL", "gpt-5.5")
|
|
monkeypatch.setenv("ARTICRAFT_THINKING_LEVEL", "xhigh")
|
|
|
|
exit_code = runner.main(
|
|
[
|
|
"--prompt",
|
|
"test prompt",
|
|
"--provider",
|
|
"openai",
|
|
"--dump-provider-payload",
|
|
]
|
|
)
|
|
|
|
assert exit_code == 0
|
|
payload = json.loads(capsys.readouterr().out)
|
|
assert payload["model"] == "gpt-5.5"
|
|
assert payload["reasoning"]["effort"] == "xhigh"
|
|
|
|
|
|
def test_runner_infers_provider_from_env_default_model(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
repo_root = tmp_path / "repo"
|
|
repo_root.mkdir()
|
|
(repo_root / ".env").write_text(
|
|
"ARTICRAFT_MODEL=gemini-3-flash-preview\nARTICRAFT_THINKING_LEVEL=high\n",
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.delenv("ARTICRAFT_MODEL", raising=False)
|
|
monkeypatch.delenv("ARTICRAFT_THINKING_LEVEL", raising=False)
|
|
|
|
exit_code = runner.main(
|
|
[
|
|
"--prompt",
|
|
"test prompt",
|
|
"--repo-root",
|
|
str(repo_root),
|
|
"--dump-provider-payload",
|
|
]
|
|
)
|
|
|
|
assert exit_code == 0
|
|
payload = json.loads(capsys.readouterr().out)
|
|
assert payload["model"] == "gemini-3-flash-preview"
|
|
assert "contents" in payload
|
|
assert "reasoning" not in payload
|
|
|
|
|
|
def test_runner_rejects_invalid_env_thinking_level(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
repo_root = tmp_path / "repo"
|
|
repo_root.mkdir()
|
|
(repo_root / ".env").write_text("ARTICRAFT_THINKING_LEVEL=not-a-level\n", encoding="utf-8")
|
|
monkeypatch.delenv("ARTICRAFT_THINKING_LEVEL", raising=False)
|
|
|
|
with pytest.raises(SystemExit, match="2"):
|
|
runner.main(
|
|
[
|
|
"--prompt",
|
|
"test prompt",
|
|
"--repo-root",
|
|
str(repo_root),
|
|
"--dump-provider-payload",
|
|
]
|
|
)
|
|
|
|
assert "ARTICRAFT_THINKING_LEVEL must be one of" in capsys.readouterr().err
|
|
|
|
|
|
def test_runner_loads_env_defaults_from_repo_root(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
repo_root = tmp_path / "repo"
|
|
repo_root.mkdir()
|
|
(repo_root / ".env").write_text(
|
|
"ARTICRAFT_MODEL=gpt-5.5-direct-runner\nARTICRAFT_THINKING_LEVEL=xhigh\n",
|
|
encoding="utf-8",
|
|
)
|
|
other_cwd = tmp_path / "cwd"
|
|
other_cwd.mkdir()
|
|
monkeypatch.chdir(other_cwd)
|
|
monkeypatch.delenv("ARTICRAFT_MODEL", raising=False)
|
|
monkeypatch.delenv("ARTICRAFT_THINKING_LEVEL", raising=False)
|
|
|
|
exit_code = runner.main(
|
|
[
|
|
"--prompt",
|
|
"test prompt",
|
|
"--provider",
|
|
"openai",
|
|
"--repo-root",
|
|
str(repo_root),
|
|
"--dump-provider-payload",
|
|
]
|
|
)
|
|
|
|
assert exit_code == 0
|
|
payload = json.loads(capsys.readouterr().out)
|
|
assert payload["model"] == "gpt-5.5-direct-runner"
|
|
assert payload["reasoning"]["effort"] == "xhigh"
|