feat: 添加 mycodex 入口,支持 OpenAI Codex CLI
- 在 launcher 中注册 codex 后端,兼容 ~/.codex/sessions 日期嵌套目录 - 解析 session_meta.payload.cwd 过滤当前目录会话 - 添加 myagents codex 子命令与 mycodex 独立入口、补全及 Makefile 链接 - 更新 pyproject.toml 控制台脚本与 README 文档 - 补充 codex 相关单元测试,45 tests passed
This commit is contained in:
+137
-1
@@ -1,10 +1,12 @@
|
||||
"""Tests for myagents CLI."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
import myagents.launcher
|
||||
from myagents.cli import cli
|
||||
|
||||
|
||||
@@ -12,12 +14,13 @@ class TestMyagentsHelp:
|
||||
"""Tests for top-level myagents command."""
|
||||
|
||||
def test_help_shows_agent_subcommands(self) -> None:
|
||||
"""--help should list claude and kimi subcommands."""
|
||||
"""--help should list claude, kimi and codex subcommands."""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "claude" in result.output
|
||||
assert "kimi" in result.output
|
||||
assert "codex" in result.output
|
||||
assert "update" in result.output
|
||||
assert "upgrade" in result.output
|
||||
|
||||
@@ -226,3 +229,136 @@ class TestUpdateSubcommand:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["upgrade", "--help"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
class TestCodexSubcommand:
|
||||
"""Tests for ``myagents codex``."""
|
||||
|
||||
def test_help_shows_options(self) -> None:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["codex", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "--cwd" in result.output
|
||||
assert "--list" in result.output
|
||||
|
||||
def test_runs_codex(self) -> None:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch("myagents.launcher.shutil.which", return_value="/usr/bin/codex"),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(cli, ["codex"])
|
||||
assert result.exit_code == 0
|
||||
mock_run.assert_called_once()
|
||||
assert mock_run.call_args[0][0] == ["/usr/bin/codex"]
|
||||
|
||||
def test_missing_binary_error(self) -> None:
|
||||
runner = CliRunner()
|
||||
with patch("myagents.launcher.shutil.which", return_value=None):
|
||||
result = runner.invoke(cli, ["codex"])
|
||||
assert result.exit_code == 127
|
||||
assert "not found" in result.output.lower()
|
||||
|
||||
def test_cwd_option_passed(self, tmp_path: Path) -> None:
|
||||
runner = CliRunner()
|
||||
test_dir = tmp_path / "test_cwd"
|
||||
test_dir.mkdir()
|
||||
|
||||
with (
|
||||
patch("myagents.launcher.shutil.which", return_value="/usr/bin/codex"),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(cli, ["codex", "--cwd", str(test_dir)])
|
||||
assert result.exit_code == 0
|
||||
assert mock_run.call_args.kwargs.get("cwd") == str(test_dir.resolve())
|
||||
|
||||
|
||||
class TestCodexPassthrough:
|
||||
"""Codex resume subcommand forwards to codex instead of erroring."""
|
||||
|
||||
def _invoke(self, args: list[str], tmp_path: Path) -> list[str]:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch("myagents.launcher.shutil.which", return_value="/usr/bin/codex"),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(
|
||||
cli, ["codex", "--cwd", str(tmp_path), *args]
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_run.assert_called_once()
|
||||
return mock_run.call_args[0][0]
|
||||
|
||||
def test_resume_subcommand_passes_through(self, tmp_path: Path) -> None:
|
||||
cmd = self._invoke(["resume", "abc123"], tmp_path)
|
||||
assert cmd == ["/usr/bin/codex", "resume", "abc123"]
|
||||
|
||||
def test_continue_subcommand_passes_through(self, tmp_path: Path) -> None:
|
||||
cmd = self._invoke(["continue"], tmp_path)
|
||||
assert cmd == ["/usr/bin/codex", "continue"]
|
||||
|
||||
|
||||
class TestCodexListSessions:
|
||||
"""``myagents codex --list`` reports resumable sessions."""
|
||||
|
||||
def test_list_empty_directory(self, tmp_path: Path) -> None:
|
||||
runner = CliRunner()
|
||||
with patch("myagents.launcher.subprocess.run") as mock_run:
|
||||
result = runner.invoke(cli, ["codex", "--cwd", str(tmp_path), "--list"])
|
||||
assert result.exit_code == 0
|
||||
assert "no sessions" in result.output.lower()
|
||||
mock_run.assert_not_called()
|
||||
|
||||
def test_list_shows_session_id_and_snippet(self, tmp_path: Path) -> None:
|
||||
runner = CliRunner()
|
||||
sessions_root = tmp_path / "sessions"
|
||||
sessions_root.mkdir(parents=True)
|
||||
session_file = (
|
||||
sessions_root
|
||||
/ "2026"
|
||||
/ "01"
|
||||
/ "02"
|
||||
/ "rollout-2026-01-02T12-00-00-019aeb67-0620-71c1-8cbb-756bc8845c6e.jsonl"
|
||||
)
|
||||
session_file.parent.mkdir(parents=True)
|
||||
session_file.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "session_meta",
|
||||
"timestamp": "2026-01-02T12:00:00Z",
|
||||
"payload": {
|
||||
"id": "019aeb67-0620-71c1-8cbb-756bc8845c6e",
|
||||
"cwd": str(tmp_path),
|
||||
},
|
||||
}
|
||||
)
|
||||
+ "\n"
|
||||
+ json.dumps(
|
||||
{
|
||||
"type": "user_message",
|
||||
"payload": {
|
||||
"type": "user_message",
|
||||
"message": "hello codex",
|
||||
},
|
||||
}
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with patch(
|
||||
"myagents.launcher._BACKENDS",
|
||||
{
|
||||
"codex": {
|
||||
**myagents.launcher._BACKENDS["codex"],
|
||||
"sessions_root": lambda: sessions_root,
|
||||
}
|
||||
},
|
||||
):
|
||||
result = runner.invoke(cli, ["codex", "--cwd", str(tmp_path), "--list"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "019aeb67-0620-71c1-8cbb-756bc8845c6e" in result.output
|
||||
assert "hello codex" in result.output
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""Tests for standalone myclaude / mykimi entrypoints."""
|
||||
"""Tests for standalone myclaude / mykimi / mycodex entrypoints."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from myagents.entrypoints import claude_cli, kimi_cli
|
||||
from myagents.entrypoints import claude_cli, codex_cli, kimi_cli
|
||||
|
||||
|
||||
class TestMyclaudeEntrypoint:
|
||||
@@ -73,3 +73,36 @@ class TestMykimiEntrypoint:
|
||||
result = runner.invoke(kimi_cli, ["--cwd", str(tmp_path), "--resume"])
|
||||
assert result.exit_code == 0
|
||||
assert "--resume" in mock_run.call_args[0][0]
|
||||
|
||||
|
||||
class TestMycodexEntrypoint:
|
||||
"""``mycodex`` standalone entrypoint."""
|
||||
|
||||
def test_runs_codex(self) -> None:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch("myagents.launcher.shutil.which", return_value="/usr/bin/codex"),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(codex_cli, [])
|
||||
assert result.exit_code == 0
|
||||
assert mock_run.call_args[0][0] == ["/usr/bin/codex"]
|
||||
|
||||
def test_version_shows_mycodex(self) -> None:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(codex_cli, ["--version"])
|
||||
assert result.exit_code == 0
|
||||
assert "mycodex" in result.output
|
||||
|
||||
def test_passthrough(self, tmp_path: Path) -> None:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch("myagents.launcher.shutil.which", return_value="/usr/bin/codex"),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(codex_cli, ["--cwd", str(tmp_path), "resume", "abc123"])
|
||||
assert result.exit_code == 0
|
||||
assert "resume" in mock_run.call_args[0][0]
|
||||
assert "abc123" in mock_run.call_args[0][0]
|
||||
|
||||
Reference in New Issue
Block a user