From de809ed57f550e12ed06993bb6d7bf2b5be68132 Mon Sep 17 00:00:00 2001 From: QUSETIONS <184124238+QUSETIONS@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:17:59 +0800 Subject: [PATCH 1/4] Prepare release candidate package smoke --- README.md | 4 +- README.zh-CN.md | 4 +- benchmarks/package_smoke.py | 93 +++++++++++++++++++++++++++++++++++++ minicode/headless.py | 29 +++++++++--- tests/test_headless.py | 16 +++++++ tests/test_packaging.py | 3 ++ 6 files changed, 139 insertions(+), 10 deletions(-) create mode 100644 benchmarks/package_smoke.py diff --git a/README.md b/README.md index 4e299b0..d2a4363 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/README.zh-CN.md b/README.zh-CN.md index a314eef..651c1dd 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -196,10 +196,10 @@ flowchart LR 当前生效的主包是根目录 `minicode/`,由 `pyproject.toml` 里的 `minicode-py` 配置驱动。 -最近一次本地验证结果: +最近一次跨平台 CI 验证结果: ```text -1290 passed, 2 skipped +1311 passed, 2 skipped ``` 验证命令: diff --git a/benchmarks/package_smoke.py b/benchmarks/package_smoke.py new file mode 100644 index 0000000..2236798 --- /dev/null +++ b/benchmarks/package_smoke.py @@ -0,0 +1,93 @@ +"""Build and install package artifacts, then smoke-test their CLI entry points.""" + +from __future__ import annotations + +import os +import subprocess +import sys +import tempfile +import venv +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def _run(command: list[str], *, cwd: Path) -> None: + subprocess.run(command, cwd=cwd, check=True) + + +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() + + _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()) diff --git a/minicode/headless.py b/minicode/headless.py index 59536b1..1d2eed9 100644 --- a/minicode/headless.py +++ b/minicode/headless.py @@ -16,6 +16,7 @@ from __future__ import annotations +import argparse import json import os import sys @@ -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) diff --git a/tests/test_headless.py b/tests/test_headless.py index bcc6fa1..ab4752f 100644 --- a/tests/test_headless.py +++ b/tests/test_headless.py @@ -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 diff --git a/tests/test_packaging.py b/tests/test_packaging.py index bb0066e..16a7ae9 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -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 From 63af69f510478a42cf9a45a7b8412b22cc303b21 Mon Sep 17 00:00:00 2001 From: QUSETIONS <1418458861@qq.com> Date: Fri, 17 Jul 2026 21:18:52 +0800 Subject: [PATCH 2/4] Add package artifact smoke gate --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9838778..cafbcda 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,6 +109,10 @@ jobs: - name: Run packaging smoke tests run: python -m pytest tests/test_packaging.py -q + - name: Build and install package artifacts + run: | + python -m pip install build + python benchmarks/package_smoke.py - name: Run AGENTS mirror tests run: > python -m pytest -q --import-mode=importlib From c1c48cebe29567cf2208e5827792a42354f1140f Mon Sep 17 00:00:00 2001 From: QUSETIONS <184124238+QUSETIONS@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:26:37 +0800 Subject: [PATCH 3/4] Keep package smoke isolated from structure checks --- benchmarks/package_smoke.py | 59 +++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/benchmarks/package_smoke.py b/benchmarks/package_smoke.py index 2236798..30d9cce 100644 --- a/benchmarks/package_smoke.py +++ b/benchmarks/package_smoke.py @@ -2,7 +2,10 @@ from __future__ import annotations +from collections.abc import Iterator +from contextlib import contextmanager import os +import shutil import subprocess import sys import tempfile @@ -11,12 +14,43 @@ 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") @@ -37,18 +71,19 @@ def main() -> int: dist = work / "dist" dist.mkdir() - _run( - [ - sys.executable, - "-m", - "build", - "--wheel", - "--sdist", - "--outdir", - str(dist), - ], - cwd=ROOT, - ) + 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")) From e008f4d9667b679e8b42f3f3468f61875dd918cd Mon Sep 17 00:00:00 2001 From: QUSETIONS <1418458861@qq.com> Date: Fri, 17 Jul 2026 21:27:04 +0800 Subject: [PATCH 4/4] Run package smoke after repository checks --- .github/workflows/ci.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cafbcda..e83b813 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,10 +109,6 @@ jobs: - name: Run packaging smoke tests run: python -m pytest tests/test_packaging.py -q - - name: Build and install package artifacts - run: | - python -m pip install build - python benchmarks/package_smoke.py - name: Run AGENTS mirror tests run: > python -m pytest -q --import-mode=importlib @@ -127,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