Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,8 @@ jobs:

- name: Run test suite
run: python -m pytest -q --tb=short

- name: Build and install package artifacts
run: |
python -m pip install build
python benchmarks/package_smoke.py
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,10 +227,10 @@ This repository is past the prototype stage. It already behaves like a usable lo

The active package is the root `minicode/` package configured by `pyproject.toml` as `minicode-py`.

Current local verification result:
Current cross-platform CI verification result:

```text
1290 passed, 2 skipped
1311 passed, 2 skipped
```

Verification command:
Expand Down
4 changes: 2 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,10 @@ flowchart LR

当前生效的主包是根目录 `minicode/`,由 `pyproject.toml` 里的 `minicode-py` 配置驱动。

最近一次本地验证结果
最近一次跨平台 CI 验证结果

```text
1290 passed, 2 skipped
1311 passed, 2 skipped
```

验证命令:
Expand Down
128 changes: 128 additions & 0 deletions benchmarks/package_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""Build and install package artifacts, then smoke-test their CLI entry points."""

from __future__ import annotations

from collections.abc import Iterator
from contextlib import contextmanager
import os
import shutil
import subprocess
import sys
import tempfile
import venv
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
_BUILD_OUTPUT_NAMES = ("build", "minicode_py.egg-info")


def _run(command: list[str], *, cwd: Path) -> None:
subprocess.run(command, cwd=cwd, check=True)


def _remove_path(path: Path) -> None:
if path.is_symlink() or path.is_file():
path.unlink()
elif path.is_dir():
shutil.rmtree(path)


@contextmanager
def _preserve_build_outputs(work: Path) -> Iterator[None]:
"""Keep PEP 517's source-tree build outputs out of the checkout."""
backup_dir = work / "source-build-output-backups"
paths = [ROOT / name for name in _BUILD_OUTPUT_NAMES]
backups: dict[Path, Path] = {}

for path in paths:
if path.exists() or path.is_symlink():
backup = backup_dir / path.name
backup.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(path), str(backup))
backups[path] = backup

try:
yield
finally:
for path in paths:
_remove_path(path)
for path, backup in backups.items():
shutil.move(str(backup), str(path))


def _venv_python(venv_dir: Path) -> Path:
return venv_dir / ("Scripts/python.exe" if os.name == "nt" else "bin/python")


def _venv_entrypoint(venv_dir: Path, name: str) -> Path:
suffix = ".exe" if os.name == "nt" else ""
directory = "Scripts" if os.name == "nt" else "bin"
return venv_dir / directory / f"{name}{suffix}"


def main() -> int:
temp_dir = "/tmp" if os.name != "nt" and Path("/tmp").is_dir() else None
with tempfile.TemporaryDirectory(
prefix="minicode-package-smoke-",
dir=temp_dir,
) as raw:
work = Path(raw)
dist = work / "dist"
dist.mkdir()

with _preserve_build_outputs(work):
_run(
[
sys.executable,
"-m",
"build",
"--wheel",
"--sdist",
"--outdir",
str(dist),
],
cwd=ROOT,
)

wheels = sorted(dist.glob("*.whl"))
sdists = sorted(dist.glob("*.tar.gz"))
if len(wheels) != 1 or len(sdists) != 1:
raise RuntimeError(
f"expected one wheel and one sdist, found {len(wheels)} wheels "
f"and {len(sdists)} sdists"
)

for label, artifact in (("wheel", wheels[0]), ("sdist", sdists[0])):
env_dir = work / f"{label}-venv"
venv.EnvBuilder(
symlinks=os.name != "nt",
with_pip=True,
).create(env_dir)
python = _venv_python(env_dir)
_run(
[str(python), "-m", "pip", "install", "--no-deps", str(artifact)],
cwd=work,
)

for entrypoint in ("minicode-py", "minicode-headless", "minicode-readiness"):
command = _venv_entrypoint(env_dir, entrypoint)
completed = subprocess.run(
[str(command), "--help"],
cwd=work,
check=False,
capture_output=True,
text=True,
)
if completed.returncode != 0:
raise RuntimeError(
f"{label} {entrypoint} --help failed:\n"
f"{completed.stdout}\n{completed.stderr}"
)

print("package smoke passed: wheel, sdist, and three CLI entrypoints")
return 0


if __name__ == "__main__":
raise SystemExit(main())
29 changes: 23 additions & 6 deletions minicode/headless.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from __future__ import annotations

import argparse
import json
import os
import sys
Expand Down Expand Up @@ -273,14 +274,30 @@ def run_headless(prompt: str | None = None, allow_edits: bool = False) -> str:
pass


def _build_argument_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="minicode-headless",
description="Run one MiniCode agent turn without a TTY.",
)
parser.add_argument(
"--allow-edits",
action="store_true",
help="Auto-approve edits, commands, and out-of-cwd access for this run.",
)
parser.add_argument(
"prompt",
nargs="*",
help="Prompt to send; reads stdin when omitted.",
)
return parser


def main(argv: list[str] | None = None) -> int:
"""CLI entry point for headless mode."""
args = list(sys.argv[1:] if argv is None else argv)
# Strip the --allow-edits flag (handled separately); everything else is the prompt.
allow_edits = "--allow-edits" in args
prompt_args = [arg for arg in args if arg != "--allow-edits"]
prompt = " ".join(prompt_args) if prompt_args else None
response = run_headless(prompt, allow_edits=allow_edits)
parser = _build_argument_parser()
parsed = parser.parse_args(list(sys.argv[1:] if argv is None else argv))
prompt = " ".join(parsed.prompt) if parsed.prompt else None
response = run_headless(prompt, allow_edits=parsed.allow_edits)
print(response)
return _headless_response_exit_code(response)

Expand Down
16 changes: 16 additions & 0 deletions tests/test_headless.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,22 @@ def test_headless_main_returns_zero_for_success(monkeypatch, capsys) -> None:
assert capsys.readouterr().out.strip() == "OK"


def test_headless_main_help_does_not_load_config(monkeypatch, capsys) -> None:
import minicode.headless

monkeypatch.setattr(
minicode.headless,
"run_headless",
lambda *args, **kwargs: pytest.fail("--help must not run headless"),
)

with pytest.raises(SystemExit) as raised:
minicode.headless.main(["--help"])

assert raised.value.code == 0
assert "usage: minicode-headless" in capsys.readouterr().out


def test_run_headless_writes_messages_trace_when_requested(monkeypatch, tmp_path: Path) -> None:
import minicode.headless

Expand Down
3 changes: 3 additions & 0 deletions tests/test_packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ def test_ci_workflow_runs_release_quality_gates() -> None:
assert "AGENTS structure artifact gate" in content
assert "Run AGENTS mirror tests" in content
assert "StructureCompliance.Test.py" in content
assert "Build and install package artifacts" in content
assert "python -m pip install build" in content
assert "python benchmarks/package_smoke.py" in content
mypy_step = content.split("- name: Type check (mypy baseline)", 1)[1]
mypy_step = mypy_step.split("- name: Run packaging smoke tests", 1)[0]
assert "shell: bash" in mypy_step
Expand Down
Loading