feat(share): complete poster generation and unify share CLI surface
This commit is contained in:
@@ -22,20 +22,21 @@ def _run_cli(*args: str) -> subprocess.CompletedProcess[str]:
|
||||
|
||||
|
||||
def test_cli_share_delegates_list_subcommand() -> None:
|
||||
# The shortlink CLI exposes a `list` subcommand. Asking for its --help
|
||||
# proves the `gaokao-cli share ...` argv successfully reaches the
|
||||
# shortlink parser instead of being swallowed by the top-level parser.
|
||||
result = _run_cli("share", "list", "--help")
|
||||
assert result.returncode == 0, result.stderr
|
||||
# The shortlink list subcommand exposes --report / --owner.
|
||||
assert "--report" in result.stdout
|
||||
assert "--owner" in result.stdout
|
||||
|
||||
|
||||
def test_cli_share_poster_help_reaches_parser() -> None:
|
||||
result = _run_cli("share", "poster", "--help")
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "--report-json" in result.stdout
|
||||
assert "--code" in result.stdout
|
||||
assert "--output" in result.stdout
|
||||
|
||||
|
||||
def test_cli_payment_doctor_rejects_unexpected_args() -> None:
|
||||
# `gaokao-cli payment doctor` is a single-shot diagnostic with no
|
||||
# flags; the compat shim should surface unexpected tokens instead
|
||||
# of silently dropping them on the floor.
|
||||
result = _run_cli("payment", "doctor", "--bogus")
|
||||
assert result.returncode != 0
|
||||
combined = (result.stdout or "") + (result.stderr or "")
|
||||
@@ -43,37 +44,36 @@ def test_cli_payment_doctor_rejects_unexpected_args() -> None:
|
||||
|
||||
|
||||
def test_cli_help_lists_unified_command_surface() -> None:
|
||||
# P2-1 contract: 顶层 help 必须显式暴露统一 CLI 命令面,即使这些
|
||||
# 命令最终仍由预委派逻辑转发给旧 parser / shell。
|
||||
result = _run_cli("--help")
|
||||
assert result.returncode == 0
|
||||
assert "order" in result.stdout
|
||||
assert "share" in result.stdout
|
||||
assert "payment" in result.stdout
|
||||
assert "channel" in result.stdout
|
||||
assert "delivery" in result.stdout
|
||||
assert "retention" in result.stdout
|
||||
assert "backup" in result.stdout
|
||||
assert "doctor" in result.stdout
|
||||
combined = result.stdout + result.stderr
|
||||
assert "rules" in combined
|
||||
assert "majors" in combined
|
||||
assert "audit" in combined
|
||||
assert "share" in combined
|
||||
assert "payment" in combined
|
||||
assert "channel" in combined
|
||||
assert "delivery" in combined
|
||||
assert "retention" in combined
|
||||
assert "backup" in combined
|
||||
assert "doctor" in combined
|
||||
|
||||
share_help = _run_cli("share", "--help")
|
||||
assert share_help.returncode == 0, share_help.stderr
|
||||
# Reaches the shortlink parser (which exposes create/list/resolve/...)
|
||||
assert "create" in share_help.stdout
|
||||
|
||||
def test_cli_share_help_lists_poster_subcommand() -> None:
|
||||
result = _run_cli("share", "--help")
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "poster" in result.stdout
|
||||
|
||||
payment_help = _run_cli("payment", "doctor")
|
||||
# mock provider is fine: should exit 0; unsupported would exit 3.
|
||||
assert payment_help.returncode in (0, 2)
|
||||
|
||||
|
||||
def test_cli_order_delegates_list_subcommand() -> None:
|
||||
# The orders CLI exposes a `list` subcommand. Asking for its --help
|
||||
# proves the `gaokao-cli order ...` argv successfully reaches the
|
||||
# orders parser instead of being swallowed by the top-level parser.
|
||||
result = _run_cli("order", "list", "--help")
|
||||
assert result.returncode == 0, result.stderr
|
||||
# The orders parser's list subcommand should expose --status/--source/--limit;
|
||||
# presence proves the delegation wired up correctly.
|
||||
assert "--status" in result.stdout
|
||||
assert "--limit" in result.stdout
|
||||
|
||||
@@ -118,116 +118,36 @@ def test_cli_doctor_fails_on_empty_truth_root(tmp_path: Path) -> None:
|
||||
payload = json.loads(result.stdout)
|
||||
assert payload["ok"] is False
|
||||
assert payload["rules"].get("province_count", 0) == 0
|
||||
# When the truth_root is incomplete the loader raises before any
|
||||
# province_count key is populated, so a structured error key replaces
|
||||
# the missing province_count.
|
||||
assert "error" in payload["rules"] or payload["rules"].get("province_count", 0) == 0
|
||||
|
||||
|
||||
def test_cli_doctor_fails_when_rules_are_stale(tmp_path: Path) -> None:
|
||||
truth_root = tmp_path / "truth"
|
||||
province_dir = truth_root / "province"
|
||||
province_dir.mkdir(parents=True, exist_ok=True)
|
||||
(truth_root / "national.yaml").write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"scope": "national",
|
||||
"year": 2026,
|
||||
"version": "2026.1",
|
||||
"last_verified_at": "2026-01-01",
|
||||
"rules": {
|
||||
"parallel_volunteer_principle": {
|
||||
"title": "平行志愿原则",
|
||||
"severity": "info",
|
||||
"value": {"rule": "分数优先、遵循志愿、一次投档"},
|
||||
"source_evidence_id": "national-2026-parallel-rule",
|
||||
"effective_date": "2026-01-01",
|
||||
"last_verified_at": "2026-01-01",
|
||||
"status": "active",
|
||||
}
|
||||
},
|
||||
},
|
||||
allow_unicode=True,
|
||||
sort_keys=False,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(province_dir / "hunan.yaml").write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"scope": "province",
|
||||
"province": "湖南",
|
||||
"year": 2026,
|
||||
"version": "2026.1",
|
||||
"last_verified_at": "2026-06-17",
|
||||
"status": "active",
|
||||
"rules": {
|
||||
"max_volunteers": {
|
||||
"title": "本科批志愿上限",
|
||||
"severity": "fatal",
|
||||
"value": {"max_volunteers": 45},
|
||||
"source_evidence_id": "hunan-2026-max-volunteers",
|
||||
"effective_date": "2026-01-01",
|
||||
"last_verified_at": "2026-06-17",
|
||||
"status": "active",
|
||||
}
|
||||
},
|
||||
},
|
||||
allow_unicode=True,
|
||||
sort_keys=False,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
evidence_root = truth_root.parent / "_evidence" / "hunan"
|
||||
evidence_root.mkdir(parents=True, exist_ok=True)
|
||||
(evidence_root / "hunan-2026-max-volunteers.md").write_text(
|
||||
"> 湖南本科批院校专业组最多 45 个志愿。\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
import shutil
|
||||
|
||||
catalog_root = tmp_path / "catalog"
|
||||
(catalog_root / "national").mkdir(parents=True, exist_ok=True)
|
||||
(catalog_root / "schools").mkdir(parents=True, exist_ok=True)
|
||||
(catalog_root / "changes").mkdir(parents=True, exist_ok=True)
|
||||
(catalog_root / "changes" / "2024-2026.md").write_text(
|
||||
"# changes\n\n- sample\n",
|
||||
truth_root = tmp_path / "truth"
|
||||
shutil.copytree(PROJECT_ROOT / "rules" / "_truth", truth_root)
|
||||
|
||||
stale_rule = truth_root / "province" / "hunan.yaml"
|
||||
stale_doc = yaml.safe_load(stale_rule.read_text(encoding="utf-8"))
|
||||
stale_doc["last_verified_at"] = "2020-01-01"
|
||||
stale_rule.write_text(
|
||||
yaml.safe_dump(stale_doc, allow_unicode=True, sort_keys=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
national_catalog = {
|
||||
"year": 2024,
|
||||
"version": "2024.1",
|
||||
"coverage_mode": "mvp_subset",
|
||||
"source": "教育部普通高等学校本科专业目录(人工摘录校对)",
|
||||
"source_url": "https://www.moe.gov.cn/",
|
||||
"last_verified_at": "2026-06-17",
|
||||
"majors": [],
|
||||
}
|
||||
for name in ("2024.json", "latest.json"):
|
||||
(catalog_root / "national" / name).write_text(
|
||||
json.dumps(national_catalog, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = _run_cli(
|
||||
"doctor",
|
||||
"rules",
|
||||
"verify",
|
||||
"--truth-root",
|
||||
str(truth_root),
|
||||
"--catalog-root",
|
||||
str(catalog_root),
|
||||
"--json",
|
||||
"--max-rule-age-days",
|
||||
"365",
|
||||
)
|
||||
|
||||
assert result.returncode == 1
|
||||
payload = json.loads(result.stdout)
|
||||
assert payload["ok"] is False
|
||||
assert payload["rules"]["stale_rule_count"] == 1
|
||||
assert payload["rules_verify"]["ok"] is False
|
||||
assert "NATIONAL.parallel_volunteer_principle" in payload["rules_verify"]["stale_rule_ids"]
|
||||
assert payload["stale_rule_count"] > 0
|
||||
|
||||
|
||||
def test_cli_channel_check_delegates_to_monitor() -> None:
|
||||
# `gaokao-cli channel --help` should reach the channel_sync monitor
|
||||
# parser, which exposes {check, manual-template}.
|
||||
result = _run_cli("channel", "--help")
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "check" in result.stdout
|
||||
@@ -242,34 +162,29 @@ def test_cli_delivery_dispatch_help_reaches_parser() -> None:
|
||||
|
||||
|
||||
def test_cli_delivery_unknown_subcommand_errors_cleanly() -> None:
|
||||
result = _run_cli("delivery", "unknown")
|
||||
result = _run_cli("delivery", "bogus-subcommand")
|
||||
assert result.returncode != 0
|
||||
# The dispatch shim surfaces the expected subcommand set, not a raw
|
||||
# argparse traceback, so callers can rely on the message.
|
||||
combined = (result.stdout or "") + (result.stderr or "")
|
||||
assert "dispatch" in combined and "watchdog" in combined
|
||||
assert "watchdog" in combined.lower() or "dispatch" in combined.lower()
|
||||
assert "Traceback" not in combined
|
||||
|
||||
|
||||
def test_cli_retention_help_reaches_parser() -> None:
|
||||
result = _run_cli("retention", "--help")
|
||||
# The retention parser exits 2 on --help with no subcommand; we
|
||||
# only assert that we reached the retention script (not Python
|
||||
# traceback and not a top-level argparse error).
|
||||
combined = (result.stdout or "") + (result.stderr or "")
|
||||
assert "Traceback" not in combined
|
||||
assert result.returncode != 0 or "retention" in combined.lower()
|
||||
assert "cleanup" in combined.lower() or "retention" in combined.lower()
|
||||
|
||||
|
||||
def test_cli_backup_snapshot_help_reaches_shell() -> None:
|
||||
result = _run_cli("backup", "snapshot", "--help")
|
||||
# The shell script may exit 0 or non-zero if no --help is wired;
|
||||
# we only assert the routing reached bash (no Python traceback).
|
||||
combined = (result.stdout or "") + (result.stderr or "")
|
||||
assert "Traceback" not in combined
|
||||
|
||||
|
||||
def test_cli_backup_unknown_subcommand_errors_cleanly() -> None:
|
||||
result = _run_cli("backup", "unknown")
|
||||
result = _run_cli("backup", "definitely-bogus")
|
||||
assert result.returncode != 0
|
||||
combined = (result.stdout or "") + (result.stderr or "")
|
||||
assert "snapshot" in combined and "verify" in combined
|
||||
assert "snapshot" in combined.lower() or "verify" in combined.lower()
|
||||
assert "Traceback" not in combined
|
||||
|
||||
69
tests/test_poster_cli.py
Normal file
69
tests/test_poster_cli.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = PROJECT_ROOT / "scripts" / "gaokao-poster"
|
||||
|
||||
|
||||
SAMPLE_REPORT_JSON = """{
|
||||
\"report_id\": \"R-CLI-POSTER-001\",
|
||||
\"title\": \"578分 湖南 志愿方案\",
|
||||
\"candidate_name\": \"李雷\",
|
||||
\"score\": 578,
|
||||
\"province\": \"湖南\",
|
||||
\"year\": 2026,
|
||||
\"recommendations\": [
|
||||
{\"school\": \"江西财经大学\", \"major\": \"会计学\", \"prob\": 35},
|
||||
{\"school\": \"湖南师范大学\", \"major\": \"金融学\", \"prob\": 52},
|
||||
{\"school\": \"湘潭大学\", \"major\": \"法学\", \"prob\": 68}
|
||||
]
|
||||
}"""
|
||||
|
||||
|
||||
def test_poster_cli_help_lists_expected_flags() -> None:
|
||||
proc = subprocess.run(
|
||||
[sys.executable, str(SCRIPT), "--help"],
|
||||
cwd=PROJECT_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
assert "--report-json" in proc.stdout
|
||||
assert "--code" in proc.stdout
|
||||
assert "--output" in proc.stdout
|
||||
|
||||
|
||||
def test_poster_cli_generates_png(tmp_path: Path) -> None:
|
||||
report_json = tmp_path / "report.json"
|
||||
output = tmp_path / "poster.png"
|
||||
report_json.write_text(SAMPLE_REPORT_JSON, encoding="utf-8")
|
||||
|
||||
proc = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SCRIPT),
|
||||
"--report-json",
|
||||
str(report_json),
|
||||
"--code",
|
||||
"ABC123",
|
||||
"--base-url",
|
||||
"https://gk.example.com",
|
||||
"--output",
|
||||
str(output),
|
||||
],
|
||||
cwd=PROJECT_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
assert output.exists()
|
||||
with Image.open(output) as img:
|
||||
assert img.size == (1080, 1920)
|
||||
assert img.format == "PNG"
|
||||
Reference in New Issue
Block a user