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:
Zhengshou Lai
2026-07-07 08:40:16 +08:00
parent 20e7eba93b
commit 10530479d0
11 changed files with 311 additions and 23 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
ROOT_DIR := $(shell pwd) ROOT_DIR := $(shell pwd)
VENV_BIN_DIR := $(ROOT_DIR)/.venv/bin VENV_BIN_DIR := $(ROOT_DIR)/.venv/bin
USER_BIN_DIR := $(HOME)/.local/bin USER_BIN_DIR := $(HOME)/.local/bin
COMMANDS := myagents myclaude mykimi COMMANDS := myagents myclaude mykimi mycodex
.PHONY: help install uninstall _symlink-commands .PHONY: help install uninstall _symlink-commands
+9 -5
View File
@@ -1,11 +1,11 @@
# MyAgents # MyAgents
一个统一的 AI coding agent 启动器,封装 Claude Code、Kimi Code CLI 等工具。 一个统一的 AI coding agent 启动器,封装 Claude Code、Kimi Code CLI、OpenAI Codex 等工具。
## 功能 ## 功能
- 🤖 **多 Agent 统一入口**`myagents claude` / `myagents kimi` - 🤖 **多 Agent 统一入口**`myagents claude` / `myagents kimi` / `myagents codex`
- 🚀 **独立快捷命令**`myclaude``mykimi` - 🚀 **独立快捷命令**`myclaude``mykimi``mycodex`
- 📂 **自动 Workspace 解析**:默认 `~/workspace`,可通过环境变量覆盖 - 📂 **自动 Workspace 解析**:默认 `~/workspace`,可通过环境变量覆盖
- 📝 **Session 管理**`--list` 列出可恢复会话 - 📝 **Session 管理**`--list` 列出可恢复会话
- ➡️ **原生参数透传**:任意底层 agent 支持的参数直接透传 - ➡️ **原生参数透传**:任意底层 agent 支持的参数直接透传
@@ -48,7 +48,7 @@ make install
`make install` 会: `make install` 会:
1. 同步/创建虚拟环境并做可编辑安装 1. 同步/创建虚拟环境并做可编辑安装
2.`~/.local/bin/` 创建 `myagents``myclaude``mykimi` 符号链接 2.`~/.local/bin/` 创建 `myagents``myclaude``mykimi``mycodex` 符号链接
3. 自动安装 shell 补全到 `~/.local/share/zsh/site-functions``~/.local/share/bash-completion/completions` 3. 自动安装 shell 补全到 `~/.local/share/zsh/site-functions``~/.local/share/bash-completion/completions`
请确保 `~/.local/bin``PATH` 中,并在 `~/.zshrc` 里加上: 请确保 `~/.local/bin``PATH` 中,并在 `~/.zshrc` 里加上:
@@ -81,6 +81,8 @@ myagents claude -l # 列出可恢复 sessions
myagents claude -r <session-id> # 恢复指定 session myagents claude -r <session-id> # 恢复指定 session
myagents kimi # 启动 Kimi Code CLI myagents kimi # 启动 Kimi Code CLI
myagents kimi -S <session-id> # 恢复 Kimi session myagents kimi -S <session-id> # 恢复 Kimi session
myagents codex # 启动 OpenAI Codex CLI
myagents codex resume <id> # 恢复 Codex session
myagents update # 重装 myagents 并同步 workspace 链接 myagents update # 重装 myagents 并同步 workspace 链接
myagents upgrade # update 别名 myagents upgrade # update 别名
myagents completion # 安装/查看/卸载 shell 补全 myagents completion # 安装/查看/卸载 shell 补全
@@ -90,9 +92,11 @@ myclaude
myclaude --cwd . -r <session-id> myclaude --cwd . -r <session-id>
mykimi mykimi
mykimi -S <session-id> mykimi -S <session-id>
mycodex
mycodex resume <session-id>
``` ```
各后端原生参数会原样透传。例如 Kimi npm 版支持 `--session``-c`continue)、`-y`yolo)等,直接加在命令后即可。 各后端原生参数会原样透传。例如 Kimi npm 版支持 `--session``-c`continue)、`-y`yolo)等,Codex 支持 `resume``continue``exec` 等子命令,直接加在命令后即可。
## 配置 ## 配置
+7 -3
View File
@@ -11,12 +11,13 @@ from myagents.launcher import build_cli
def _progs(): def _progs():
"""Return (prog_name, cli_factory) pairs that need shell completions.""" """Return (prog_name, cli_factory) pairs that need shell completions."""
from myagents.entrypoints import claude_cli, kimi_cli from myagents.entrypoints import claude_cli, codex_cli, kimi_cli
return [ return [
("myagents", lambda: cli), ("myagents", lambda: cli),
("myclaude", lambda: claude_cli), ("myclaude", lambda: claude_cli),
("mykimi", lambda: kimi_cli), ("mykimi", lambda: kimi_cli),
("mycodex", lambda: codex_cli),
] ]
@@ -33,7 +34,7 @@ def _package_version() -> str:
def cli(ctx: click.Context) -> None: def cli(ctx: click.Context) -> None:
"""Myagents: unified launcher for AI coding agents. """Myagents: unified launcher for AI coding agents.
Use ``myagents claude`` or ``myagents kimi`` to start an agent in workspace/. Use ``myagents claude``, ``myagents kimi`` or ``myagents codex`` to start an agent in workspace/.
""" """
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
click.echo(ctx.get_help()) click.echo(ctx.get_help())
@@ -42,13 +43,16 @@ def cli(ctx: click.Context) -> None:
cli.add_command(build_cli("claude"), name="claude") cli.add_command(build_cli("claude"), name="claude")
cli.add_command(build_cli("kimi"), name="kimi") cli.add_command(build_cli("kimi"), name="kimi")
cli.add_command(build_cli("codex"), name="codex")
cli.add_command(update_cmd) cli.add_command(update_cmd)
cli.add_command(upgrade_cmd, name="upgrade") cli.add_command(upgrade_cmd, name="upgrade")
cli.add_command(build_completion_group(_progs)) cli.add_command(build_completion_group(_progs))
def main() -> None: def main() -> None:
from myagents.commands.completion_install import ensure_completions_installed from myagents.commands.completion_install import (
ensure_completions_installed,
)
ensure_completions_installed(_progs()) ensure_completions_installed(_progs())
cli() cli()
+5 -1
View File
@@ -35,6 +35,10 @@ def _print_script(prog_name: str, shell: str) -> None:
from myagents.entrypoints import kimi_cli from myagents.entrypoints import kimi_cli
cli_obj = kimi_cli cli_obj = kimi_cli
elif prog_name == "mycodex":
from myagents.entrypoints import codex_cli
cli_obj = codex_cli
else: else:
raise click.ClickException(f"Unknown command: {prog_name}") raise click.ClickException(f"Unknown command: {prog_name}")
@@ -56,7 +60,7 @@ def build_completion_group(progs_fn: ProgListFn) -> click.Group:
@completion.command("install") @completion.command("install")
def install_cmd() -> None: def install_cmd() -> None:
"""Write completion scripts for myagents, myclaude and mykimi.""" """Write completion scripts for myagents, myclaude, mykimi and mycodex."""
installed = install_completions(progs_fn()) installed = install_completions(progs_fn())
if not installed: if not installed:
raise click.ClickException( raise click.ClickException(
+1 -2
View File
@@ -9,14 +9,13 @@ from __future__ import annotations
import os import os
import shlex import shlex
import subprocess import subprocess
from collections.abc import Callable, Sequence
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Callable, Sequence
import click import click
from click.shell_completion import get_completion_class from click.shell_completion import get_completion_class
ProgList = Sequence[tuple[str, Callable[[], click.Command]]] ProgList = Sequence[tuple[str, Callable[[], click.Command]]]
+19 -3
View File
@@ -1,9 +1,10 @@
"""Standalone entrypoints for myclaude and mykimi command names.""" """Standalone entrypoints for myclaude, mykimi, and mycodex command names."""
from myagents.launcher import build_cli from myagents.launcher import build_cli
claude_cli = build_cli("claude", prog_name="myclaude") claude_cli = build_cli("claude", prog_name="myclaude")
kimi_cli = build_cli("kimi", prog_name="mykimi") kimi_cli = build_cli("kimi", prog_name="mykimi")
codex_cli = build_cli("codex", prog_name="mycodex")
def _progs(): def _progs():
@@ -13,12 +14,15 @@ def _progs():
("myagents", lambda: cli), ("myagents", lambda: cli),
("myclaude", lambda: claude_cli), ("myclaude", lambda: claude_cli),
("mykimi", lambda: kimi_cli), ("mykimi", lambda: kimi_cli),
("mycodex", lambda: codex_cli),
] ]
def claude_main() -> None: def claude_main() -> None:
"""Run ``myclaude``.""" """Run ``myclaude``."""
from myagents.commands.completion_install import ensure_completions_installed from myagents.commands.completion_install import (
ensure_completions_installed,
)
ensure_completions_installed(_progs()) ensure_completions_installed(_progs())
claude_cli() claude_cli()
@@ -26,7 +30,19 @@ def claude_main() -> None:
def kimi_main() -> None: def kimi_main() -> None:
"""Run ``mykimi``.""" """Run ``mykimi``."""
from myagents.commands.completion_install import ensure_completions_installed from myagents.commands.completion_install import (
ensure_completions_installed,
)
ensure_completions_installed(_progs()) ensure_completions_installed(_progs())
kimi_cli() kimi_cli()
def codex_main() -> None:
"""Run ``mycodex``."""
from myagents.commands.completion_install import (
ensure_completions_installed,
)
ensure_completions_installed(_progs())
codex_cli()
+94 -3
View File
@@ -42,6 +42,17 @@ _BACKENDS: dict[str, dict] = {
"[cyan]KIMI_BIN[/cyan]." "[cyan]KIMI_BIN[/cyan]."
), ),
}, },
"codex": {
"binary": "codex",
"env_bin": "CODEX_BIN",
"sessions_root": lambda: Path.home() / ".codex" / "sessions",
"session_pattern": "**/*.jsonl",
"default_args": [],
"not_found_msg": (
"[red]codex CLI not found in PATH.[/red] Install with "
"[cyan]npm install -g @openai/codex[/cyan] or set [cyan]CODEX_BIN[/cyan]."
),
},
} }
@@ -69,6 +80,12 @@ def _launch(backend: str, chat_cwd: Path, extra: list[str]) -> None:
raise SystemExit(proc.returncode) raise SystemExit(proc.returncode)
_SESSION_ID_UUID_RE = re.compile(
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
re.IGNORECASE,
)
def _sessions_dir(backend: str, chat_cwd: Path) -> Path: def _sessions_dir(backend: str, chat_cwd: Path) -> Path:
"""Directory where the backend stores sessions for chat_cwd.""" """Directory where the backend stores sessions for chat_cwd."""
config = _BACKENDS[backend] config = _BACKENDS[backend]
@@ -76,6 +93,9 @@ def _sessions_dir(backend: str, chat_cwd: Path) -> Path:
if backend == "kimi": if backend == "kimi":
# Kimi hashes the cwd with md5. # Kimi hashes the cwd with md5.
munged = hashlib.md5(cwd_str.encode("utf-8")).hexdigest() # noqa: S324 munged = hashlib.md5(cwd_str.encode("utf-8")).hexdigest() # noqa: S324
elif backend == "codex":
# Codex stores all sessions under a single dated tree; cwd is in metadata.
return config["sessions_root"]()
else: else:
munged = re.sub(r"[^A-Za-z0-9]", "-", cwd_str) munged = re.sub(r"[^A-Za-z0-9]", "-", cwd_str)
return config["sessions_root"]() / munged return config["sessions_root"]() / munged
@@ -125,11 +145,71 @@ def _first_prompt_kimi(session_file: Path) -> str:
return "" return ""
def _codex_session_cwd(session_file: Path) -> str | None:
"""Read cwd from a codex session's session_meta record."""
try:
with session_file.open(encoding="utf-8", errors="ignore") as fh:
for line in fh:
if not line.strip():
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
if entry.get("type") == "session_meta":
return entry.get("payload", {}).get("cwd")
except OSError:
pass
return None
def _first_prompt_codex(session_file: Path) -> str:
"""Best-effort snippet of the first human prompt in a codex rollout jsonl."""
try:
with session_file.open(encoding="utf-8", errors="ignore") as fh:
for line in fh:
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
if entry.get("type") != "user_message":
continue
payload = entry.get("payload", {})
if payload.get("type") != "user_message":
continue
content = payload.get("message")
if isinstance(content, str) and content.strip():
return " ".join(content.split())[:80]
except OSError:
pass
return ""
def _codex_session_id(session_file: Path) -> str:
"""Extract the UUID session id from a codex rollout filename."""
match = _SESSION_ID_UUID_RE.search(session_file.stem)
return match.group(0) if match else session_file.stem
def _session_files(backend: str, chat_cwd: Path) -> list[Path]: def _session_files(backend: str, chat_cwd: Path) -> list[Path]:
"""Return session files/directories sorted by newest first.""" """Return session files/directories sorted by newest first."""
config = _BACKENDS[backend] config = _BACKENDS[backend]
sessions_dir = _sessions_dir(backend, chat_cwd) sessions_dir = _sessions_dir(backend, chat_cwd)
if backend == "codex":
files = [p for p in sessions_dir.glob(config["session_pattern"]) if p.is_file()]
cwd_str = str(chat_cwd)
def _matches_cwd(p: Path) -> bool:
stored = _codex_session_cwd(p)
if stored is None:
return False
return stored == cwd_str or Path(stored).resolve() == chat_cwd
files = [p for p in files if _matches_cwd(p)]
files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
return files
if backend == "claude": if backend == "claude":
files = sorted( files = sorted(
sessions_dir.glob(config["session_pattern"]), sessions_dir.glob(config["session_pattern"]),
@@ -153,6 +233,8 @@ def _session_files(backend: str, chat_cwd: Path) -> list[Path]:
def _first_prompt(backend: str, session_file: Path) -> str: def _first_prompt(backend: str, session_file: Path) -> str:
if backend == "claude": if backend == "claude":
return _first_prompt_claude(session_file) return _first_prompt_claude(session_file)
if backend == "codex":
return _first_prompt_codex(session_file)
return _first_prompt_kimi(session_file) return _first_prompt_kimi(session_file)
@@ -160,6 +242,8 @@ def _resume_syntax(backend: str) -> str:
"""Return the backend's resume flag syntax for use in help text.""" """Return the backend's resume flag syntax for use in help text."""
if backend == "kimi": if backend == "kimi":
return "--session, -S <id>" return "--session, -S <id>"
if backend == "codex":
return "resume <id>"
return "--resume, -r <id>" return "--resume, -r <id>"
@@ -174,7 +258,12 @@ def _list_sessions(backend: str, chat_cwd: Path) -> None:
for session_file in files: for session_file in files:
mtime = datetime.fromtimestamp(session_file.stat().st_mtime) mtime = datetime.fromtimestamp(session_file.stat().st_mtime)
snippet = _first_prompt(backend, session_file) or "[dim](empty)[/dim]" snippet = _first_prompt(backend, session_file) or "[dim](empty)[/dim]"
session_id = session_file.parent.name if backend == "kimi" else session_file.stem if backend == "kimi":
session_id = session_file.parent.name
elif backend == "codex":
session_id = _codex_session_id(session_file)
else:
session_id = session_file.stem
console.print( console.print(
f" [cyan]{session_id}[/cyan] " f" [cyan]{session_id}[/cyan] "
f"[dim]{mtime:%Y-%m-%d %H:%M}[/dim] {snippet}" f"[dim]{mtime:%Y-%m-%d %H:%M}[/dim] {snippet}"
@@ -204,12 +293,14 @@ class LaunchGroup(click.Group):
def build_cli(backend: str, prog_name: str | None = None) -> click.Group: def build_cli(backend: str, prog_name: str | None = None) -> click.Group:
"""Build a click CLI that wraps ``backend`` (claude or kimi). """Build a click CLI that wraps ``backend`` (claude, kimi, or codex).
``prog_name`` is used in --version output. When omitted it defaults to ``prog_name`` is used in --version output. When omitted it defaults to
``myagents <backend>`` (suitable for use as a subcommand of ``myagents``). ``myagents <backend>`` (suitable for use as a subcommand of ``myagents``).
""" """
backend_title = backend.capitalize() backend_title = backend.capitalize()
if backend == "codex":
backend_title = "OpenAI Codex"
@click.group( @click.group(
cls=LaunchGroup, cls=LaunchGroup,
@@ -262,7 +353,7 @@ def build_cli(backend: str, prog_name: str | None = None) -> click.Group:
_launch(backend, chat_cwd, extra) _launch(backend, chat_cwd, extra)
cli.help = ( cli.help = (
f"Launch {backend_title} Code in workspace/.\n\n" f"Launch {backend_title} in workspace/.\n\n"
f"Unknown arguments ({_resume_syntax(backend)}, --continue, …) pass through to {backend}." f"Unknown arguments ({_resume_syntax(backend)}, --continue, …) pass through to {backend}."
) )
try: try:
+1
View File
@@ -12,6 +12,7 @@ dependencies = [
myagents = "myagents.cli:cli" myagents = "myagents.cli:cli"
myclaude = "myagents.entrypoints:claude_main" myclaude = "myagents.entrypoints:claude_main"
mykimi = "myagents.entrypoints:kimi_main" mykimi = "myagents.entrypoints:kimi_main"
mycodex = "myagents.entrypoints:codex_main"
[dependency-groups] [dependency-groups]
dev = ["pytest>=8.0"] dev = ["pytest>=8.0"]
+2 -2
View File
@@ -1,12 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Remove ~/.local/bin/{myagents,myclaude,mykimi} symlinks to this repo's venv.""" """Remove ~/.local/bin/{myagents,myclaude,mykimi,mycodex} symlinks to this repo's venv."""
from __future__ import annotations from __future__ import annotations
import os import os
import sys import sys
_COMMANDS = ("myagents", "myclaude", "mykimi") _COMMANDS = ("myagents", "myclaude", "mykimi", "mycodex")
def _remove_link(link: str, want: str) -> bool: def _remove_link(link: str, want: str) -> bool:
+137 -1
View File
@@ -1,10 +1,12 @@
"""Tests for myagents CLI.""" """Tests for myagents CLI."""
import json
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from click.testing import CliRunner from click.testing import CliRunner
import myagents.launcher
from myagents.cli import cli from myagents.cli import cli
@@ -12,12 +14,13 @@ class TestMyagentsHelp:
"""Tests for top-level myagents command.""" """Tests for top-level myagents command."""
def test_help_shows_agent_subcommands(self) -> None: 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() runner = CliRunner()
result = runner.invoke(cli, ["--help"]) result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0 assert result.exit_code == 0
assert "claude" in result.output assert "claude" in result.output
assert "kimi" in result.output assert "kimi" in result.output
assert "codex" in result.output
assert "update" in result.output assert "update" in result.output
assert "upgrade" in result.output assert "upgrade" in result.output
@@ -226,3 +229,136 @@ class TestUpdateSubcommand:
runner = CliRunner() runner = CliRunner()
result = runner.invoke(cli, ["upgrade", "--help"]) result = runner.invoke(cli, ["upgrade", "--help"])
assert result.exit_code == 0 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
+35 -2
View File
@@ -1,11 +1,11 @@
"""Tests for standalone myclaude / mykimi entrypoints.""" """Tests for standalone myclaude / mykimi / mycodex entrypoints."""
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from click.testing import CliRunner 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: class TestMyclaudeEntrypoint:
@@ -73,3 +73,36 @@ class TestMykimiEntrypoint:
result = runner.invoke(kimi_cli, ["--cwd", str(tmp_path), "--resume"]) result = runner.invoke(kimi_cli, ["--cwd", str(tmp_path), "--resume"])
assert result.exit_code == 0 assert result.exit_code == 0
assert "--resume" in mock_run.call_args[0][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]