tests: cover m2pack cli and mcp additions
Add discovery-helper tests (env var override, PATH fallback, missing binary) and command tests that point M2PACK_BINARY at a Python stub script echoing canned JSON per subcommand. Cover success paths, non-zero exit, non-JSON output, JSON-array output, and missing binary. Extend the MCP schema mirror test to cover all 12 tools and add dispatch tests for the new m2pack_* argv translation.
This commit is contained in:
307
tests/test_m2pack_commands.py
Normal file
307
tests/test_m2pack_commands.py
Normal file
@@ -0,0 +1,307 @@
|
||||
"""Tests for the m2pack wrapper subcommands.
|
||||
|
||||
All tests use a stub binary — a small Python script that echoes a canned
|
||||
JSON envelope based on the subcommand name — pointed at via the
|
||||
``M2PACK_BINARY`` env var. The real m2pack-secure binary is never invoked.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import stat
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from metin_release.cli import main as cli_main
|
||||
|
||||
|
||||
STUB_TEMPLATE = r"""#!{python}
|
||||
import json
|
||||
import sys
|
||||
|
||||
argv = sys.argv[1:]
|
||||
sub = argv[0] if argv else "unknown"
|
||||
MODE = {mode!r}
|
||||
|
||||
if MODE == "fail":
|
||||
sys.stderr.write("boom\n")
|
||||
sys.exit(2)
|
||||
if MODE == "nonjson":
|
||||
sys.stdout.write("not json at all\n")
|
||||
sys.exit(0)
|
||||
if MODE == "notobject":
|
||||
sys.stdout.write(json.dumps([1, 2, 3]) + "\n")
|
||||
sys.exit(0)
|
||||
|
||||
# mode == "ok": echo a canned envelope per subcommand
|
||||
if sub == "build":
|
||||
env = {{"ok": True, "command": "build", "stats": {{"files": 3, "bytes": 12345}}}}
|
||||
elif sub == "verify":
|
||||
env = {{"ok": True, "command": "verify", "signature": "valid"}}
|
||||
elif sub == "diff":
|
||||
env = {{"ok": True, "command": "diff", "added": ["a"], "removed": [], "changed": ["b", "c"], "unchanged": 7}}
|
||||
elif sub == "export-runtime-key":
|
||||
env = {{"ok": True, "command": "export-runtime-key", "key_id": 1, "format": "json"}}
|
||||
else:
|
||||
env = {{"ok": True, "command": sub}}
|
||||
|
||||
# Record argv so the test can assert translation
|
||||
import os
|
||||
log = os.environ.get("M2PACK_STUB_LOG")
|
||||
if log:
|
||||
with open(log, "a") as fh:
|
||||
fh.write(json.dumps(argv) + "\n")
|
||||
|
||||
sys.stdout.write(json.dumps(env) + "\n")
|
||||
sys.exit(0)
|
||||
"""
|
||||
|
||||
|
||||
def _install_stub(tmp_path: Path, monkeypatch, mode: str = "ok") -> tuple[Path, Path]:
|
||||
stub = tmp_path / "m2pack_stub.py"
|
||||
stub.write_text(STUB_TEMPLATE.format(python=sys.executable, mode=mode))
|
||||
stub.chmod(stub.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
||||
log = tmp_path / "stub.log"
|
||||
monkeypatch.setenv("M2PACK_BINARY", str(stub))
|
||||
monkeypatch.setenv("M2PACK_STUB_LOG", str(log))
|
||||
return stub, log
|
||||
|
||||
|
||||
def _run_cli(argv: list[str], capsys) -> dict:
|
||||
rc = cli_main(argv)
|
||||
out = capsys.readouterr().out
|
||||
envelope = json.loads(out)
|
||||
return {"rc": rc, "env": envelope}
|
||||
|
||||
|
||||
def _read_stub_call(log: Path) -> list[str]:
|
||||
lines = [ln for ln in log.read_text().splitlines() if ln.strip()]
|
||||
assert lines, "stub was never invoked"
|
||||
return json.loads(lines[-1])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Success paths
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_build_success(tmp_path, monkeypatch, capsys):
|
||||
_, log = _install_stub(tmp_path, monkeypatch)
|
||||
(tmp_path / "in").mkdir()
|
||||
key = tmp_path / "k.hex"
|
||||
key.write_text("ff")
|
||||
sk = tmp_path / "sk.hex"
|
||||
sk.write_text("aa")
|
||||
out = tmp_path / "out.m2p"
|
||||
|
||||
r = _run_cli(
|
||||
[
|
||||
"--json",
|
||||
"m2pack",
|
||||
"build",
|
||||
"--input", str(tmp_path / "in"),
|
||||
"--output", str(out),
|
||||
"--key", str(key),
|
||||
"--sign-secret-key", str(sk),
|
||||
"--key-id", "7",
|
||||
],
|
||||
capsys,
|
||||
)
|
||||
assert r["rc"] == 0
|
||||
assert r["env"]["ok"] is True
|
||||
assert r["env"]["command"] == "m2pack build"
|
||||
assert r["env"]["status"] == "built"
|
||||
assert r["env"]["artifacts"]["archive_path"] == str(out)
|
||||
assert r["env"]["m2pack"]["stats"]["files"] == 3
|
||||
|
||||
call = _read_stub_call(log)
|
||||
assert call[0] == "build"
|
||||
assert "--input" in call and str(tmp_path / "in") in call
|
||||
assert "--output" in call and str(out) in call
|
||||
assert "--key" in call and str(key) in call
|
||||
assert "--sign-secret-key" in call and str(sk) in call
|
||||
assert "--key-id" in call and "7" in call
|
||||
assert "--json" in call
|
||||
|
||||
|
||||
def test_build_omits_optional_key_id(tmp_path, monkeypatch, capsys):
|
||||
_, log = _install_stub(tmp_path, monkeypatch)
|
||||
(tmp_path / "in").mkdir()
|
||||
(tmp_path / "k").write_text("a")
|
||||
(tmp_path / "sk").write_text("b")
|
||||
_run_cli(
|
||||
[
|
||||
"--json", "m2pack", "build",
|
||||
"--input", str(tmp_path / "in"),
|
||||
"--output", str(tmp_path / "o.m2p"),
|
||||
"--key", str(tmp_path / "k"),
|
||||
"--sign-secret-key", str(tmp_path / "sk"),
|
||||
],
|
||||
capsys,
|
||||
)
|
||||
call = _read_stub_call(log)
|
||||
assert "--key-id" not in call
|
||||
|
||||
|
||||
def test_verify_success_without_keys(tmp_path, monkeypatch, capsys):
|
||||
_, log = _install_stub(tmp_path, monkeypatch)
|
||||
archive = tmp_path / "a.m2p"
|
||||
archive.write_bytes(b"x")
|
||||
r = _run_cli(
|
||||
["--json", "m2pack", "verify", "--archive", str(archive)], capsys
|
||||
)
|
||||
assert r["rc"] == 0
|
||||
assert r["env"]["status"] == "verified"
|
||||
assert r["env"]["m2pack"]["signature"] == "valid"
|
||||
call = _read_stub_call(log)
|
||||
assert call[0] == "verify"
|
||||
assert "--public-key" not in call
|
||||
assert "--key" not in call
|
||||
|
||||
|
||||
def test_verify_with_public_and_content_keys(tmp_path, monkeypatch, capsys):
|
||||
_, log = _install_stub(tmp_path, monkeypatch)
|
||||
archive = tmp_path / "a.m2p"
|
||||
archive.write_bytes(b"x")
|
||||
pk = tmp_path / "pub"
|
||||
pk.write_text("ff")
|
||||
ck = tmp_path / "ck"
|
||||
ck.write_text("aa")
|
||||
_run_cli(
|
||||
[
|
||||
"--json", "m2pack", "verify",
|
||||
"--archive", str(archive),
|
||||
"--public-key", str(pk),
|
||||
"--key", str(ck),
|
||||
],
|
||||
capsys,
|
||||
)
|
||||
call = _read_stub_call(log)
|
||||
assert "--public-key" in call and str(pk) in call
|
||||
assert "--key" in call and str(ck) in call
|
||||
|
||||
|
||||
def test_diff_success_promotes_counts(tmp_path, monkeypatch, capsys):
|
||||
_, log = _install_stub(tmp_path, monkeypatch)
|
||||
left = tmp_path / "L"
|
||||
left.mkdir()
|
||||
right = tmp_path / "R.m2p"
|
||||
right.write_bytes(b"x")
|
||||
r = _run_cli(
|
||||
[
|
||||
"--json", "m2pack", "diff",
|
||||
"--left", str(left),
|
||||
"--right", str(right),
|
||||
],
|
||||
capsys,
|
||||
)
|
||||
assert r["rc"] == 0
|
||||
assert r["env"]["status"] == "diffed"
|
||||
stats = r["env"]["stats"]
|
||||
assert stats["added_count"] == 1
|
||||
assert stats["removed_count"] == 0
|
||||
assert stats["changed_count"] == 2
|
||||
assert stats["unchanged_count"] == 7
|
||||
call = _read_stub_call(log)
|
||||
assert call[0] == "diff"
|
||||
|
||||
|
||||
def test_export_runtime_key_success(tmp_path, monkeypatch, capsys):
|
||||
_, log = _install_stub(tmp_path, monkeypatch)
|
||||
key = tmp_path / "ck"
|
||||
key.write_text("aa")
|
||||
pk = tmp_path / "pk"
|
||||
pk.write_text("ff")
|
||||
out = tmp_path / "runtime.json"
|
||||
r = _run_cli(
|
||||
[
|
||||
"--json", "m2pack", "export-runtime-key",
|
||||
"--key", str(key),
|
||||
"--public-key", str(pk),
|
||||
"--output", str(out),
|
||||
"--key-id", "3",
|
||||
"--format", "blob",
|
||||
],
|
||||
capsys,
|
||||
)
|
||||
assert r["rc"] == 0
|
||||
assert r["env"]["status"] == "exported"
|
||||
assert r["env"]["artifacts"]["runtime_key_path"] == str(out)
|
||||
call = _read_stub_call(log)
|
||||
assert call[0] == "export-runtime-key"
|
||||
assert "--format" in call and "blob" in call
|
||||
assert "--key-id" in call and "3" in call
|
||||
|
||||
|
||||
def test_export_runtime_key_rejects_bad_format(tmp_path, monkeypatch, capsys):
|
||||
_install_stub(tmp_path, monkeypatch)
|
||||
with pytest.raises(SystemExit):
|
||||
cli_main(
|
||||
[
|
||||
"--json", "m2pack", "export-runtime-key",
|
||||
"--key", str(tmp_path / "k"),
|
||||
"--public-key", str(tmp_path / "p"),
|
||||
"--output", str(tmp_path / "o"),
|
||||
"--format", "yaml",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Error paths
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_nonzero_exit_maps_to_subprocess_failed(tmp_path, monkeypatch, capsys):
|
||||
_install_stub(tmp_path, monkeypatch, mode="fail")
|
||||
(tmp_path / "in").mkdir()
|
||||
(tmp_path / "k").write_text("a")
|
||||
(tmp_path / "sk").write_text("b")
|
||||
rc = cli_main(
|
||||
[
|
||||
"--json", "m2pack", "build",
|
||||
"--input", str(tmp_path / "in"),
|
||||
"--output", str(tmp_path / "o.m2p"),
|
||||
"--key", str(tmp_path / "k"),
|
||||
"--sign-secret-key", str(tmp_path / "sk"),
|
||||
],
|
||||
)
|
||||
assert rc == 1
|
||||
env = json.loads(capsys.readouterr().out)
|
||||
assert env["ok"] is False
|
||||
assert env["error"]["code"] == "m2pack_failed"
|
||||
|
||||
|
||||
def test_nonjson_output_maps_to_invalid_json(tmp_path, monkeypatch, capsys):
|
||||
_install_stub(tmp_path, monkeypatch, mode="nonjson")
|
||||
archive = tmp_path / "a.m2p"
|
||||
archive.write_bytes(b"x")
|
||||
rc = cli_main(["--json", "m2pack", "verify", "--archive", str(archive)])
|
||||
assert rc == 1
|
||||
env = json.loads(capsys.readouterr().out)
|
||||
assert env["ok"] is False
|
||||
assert env["error"]["code"] == "m2pack_invalid_json"
|
||||
|
||||
|
||||
def test_json_array_output_maps_to_invalid_json(tmp_path, monkeypatch, capsys):
|
||||
_install_stub(tmp_path, monkeypatch, mode="notobject")
|
||||
archive = tmp_path / "a.m2p"
|
||||
archive.write_bytes(b"x")
|
||||
rc = cli_main(["--json", "m2pack", "verify", "--archive", str(archive)])
|
||||
assert rc == 1
|
||||
env = json.loads(capsys.readouterr().out)
|
||||
assert env["error"]["code"] == "m2pack_invalid_json"
|
||||
|
||||
|
||||
def test_missing_binary_raises_validation_error(tmp_path, monkeypatch, capsys):
|
||||
monkeypatch.delenv("M2PACK_BINARY", raising=False)
|
||||
monkeypatch.setenv("PATH", "/nonexistent")
|
||||
archive = tmp_path / "a.m2p"
|
||||
archive.write_bytes(b"x")
|
||||
rc = cli_main(["--json", "m2pack", "verify", "--archive", str(archive)])
|
||||
assert rc == 1
|
||||
env = json.loads(capsys.readouterr().out)
|
||||
assert env["ok"] is False
|
||||
assert env["error"]["code"] == "m2pack_not_found"
|
||||
Reference in New Issue
Block a user