Files
gyxx-flow/tests/test_script_catalog.py

365 lines
11 KiB
Python

from __future__ import annotations
import io
import json
from pathlib import Path
import pytest
from gyxx_flow.cli import EXIT_SUCCESS, main
from gyxx_flow.core.config import Settings
from gyxx_flow.script_catalog import ScriptCatalog, ScriptCatalogError
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def _write_registry(path: Path, commands: list[dict[str, object]]) -> None:
path.write_text(
json.dumps({"schema_version": 1, "commands": commands}),
encoding="utf-8",
)
def test_catalog_exposes_only_explicit_commands(tmp_path: Path) -> None:
runtime = tmp_path / "content_marketing" / "runtime"
(runtime / "tools").mkdir(parents=True)
registered = runtime / "tools" / "run_job.py"
registered.write_text(
'if __name__ == "__main__":\n raise SystemExit(0)\n', encoding="utf-8"
)
(runtime / "undeclared.py").write_text(
'if __name__ == "__main__":\n raise SystemExit(0)\n', encoding="utf-8"
)
(runtime / "undeclared.bat").write_text("@echo off\n", encoding="utf-8")
(runtime / "undeclared.ps1").write_text("Write-Output ok\n", encoding="utf-8")
config = tmp_path / "commands.json"
_write_registry(
config,
[
{
"id": "content.test.run",
"module": "content_marketing",
"entry": "tools/run_job.py",
"kind": "python",
}
],
)
catalog = ScriptCatalog.load(config, roots={"content_marketing": runtime})
assert catalog.command_ids == ("content.test.run",)
assert catalog.script_ids == ("content_marketing:tools/run_job.py",)
command = catalog.get("content.test.run")
assert command.entry == "tools/run_job.py"
assert catalog.get("content_marketing:tools/run_job.py") is command
with pytest.raises(ScriptCatalogError, match="unknown command"):
catalog.get("content_marketing:undeclared.py")
def test_default_registry_covers_every_executable_workflow_entry() -> None:
catalog = ScriptCatalog.load_default()
payload = json.loads(
(PROJECT_ROOT / "config" / "workflows.json").read_text(encoding="utf-8")
)
workflow_entries: set[str] = set()
scheduled_workflows = 0
manual_workflows = 0
for workflow in payload["workflows"]:
assert workflow["trigger"] in {"scheduled", "manual"}
if workflow["trigger"] == "scheduled":
scheduled_workflows += 1
else:
manual_workflows += 1
execution = workflow["execution"]
steps = execution.get("steps")
entries = (
[step["entry"] for step in steps]
if steps is not None
else [execution["entry"]]
)
for entry in entries:
workflow_entries.add(f"{workflow['module']}:{entry}")
assert scheduled_workflows == 27
assert manual_workflows == 2
assert len(workflow_entries) == 44
assert len(catalog.scripts) == 49
assert workflow_entries < set(catalog.script_ids)
assert {
"content.mapping.rebuild",
"content.failed.retry",
"shop.douyin_price_appeal",
"shop.jd_self_operated.collect_brand",
"product.backfill.run",
"product.erp_all_shop_daily",
"product.review.orchestrate",
"product.video_upload.run",
"product.jd_video_upload.run",
"supply.workflow.run",
} < set(catalog.command_ids)
assert {item.module for item in catalog.scripts} == {
"content_marketing",
"product_commerce",
"shop_intelligence",
"supply_chain",
}
assert all(item.path.is_relative_to(PROJECT_ROOT) for item in catalog.scripts)
assert catalog.get("shop.jd_self_operated.collect_brand").default_args == (
"--start-date",
"{business_date}",
"--end-date",
"{business_date}",
"--headless",
)
assert catalog.get("shop.douyin_price_appeal").default_args == (
"--price-appeal",
"--execute",
)
assert catalog.get("product.backfill.run").default_args == (
"--from",
"{business_date}",
"--to",
"{business_date}",
)
assert catalog.get("product.erp_all_shop_daily").default_args == (
"--from",
"{business_date}",
"--to",
"{business_date}",
)
assert catalog.get("product.review.orchestrate").default_args == (
"--target-date",
"{business_date}",
)
assert catalog.get("product.import.tmall_ads").default_args == (
"--date",
"{business_date}",
"--execute",
)
assert catalog.get("supply.workflow.run").default_args == (
"mcp-run",
"purchase-order-update",
)
assert (
catalog.get("content.marketing_report.generate").notification_workflow_id
== "content.marketing_report.daily"
)
assert (
catalog.get("content.login.refresh").notification_workflow_id
== "content.relogin.weekly"
)
@pytest.mark.parametrize(
("record", "message"),
(
(
{
"id": "content.bad.path",
"module": "content_marketing",
"entry": "../outside.py",
"kind": "python",
},
"unsafe command entry",
),
(
{
"id": "content.bad.kind",
"module": "content_marketing",
"entry": "job.py",
"kind": "batch",
},
"kind does not match",
),
(
{
"id": "Content Bad",
"module": "content_marketing",
"entry": "job.py",
"kind": "python",
},
"invalid command id",
),
(
{
"id": "content.bad.args",
"module": "content_marketing",
"entry": "job.py",
"kind": "python",
"default_args": "--unsafe-shape",
},
"invalid command default_args",
),
(
{
"id": "content.bad.notification",
"module": "content_marketing",
"entry": "job.py",
"kind": "python",
"notification_workflow_id": "content.not-a-capability",
},
"invalid command notification_workflow_id",
),
),
)
def test_catalog_rejects_unsafe_or_invalid_declarations(
tmp_path: Path, record: dict[str, object], message: str
) -> None:
runtime = tmp_path / "runtime"
runtime.mkdir()
(runtime / "job.py").write_text("print('ok')\n", encoding="utf-8")
config = tmp_path / "commands.json"
_write_registry(config, [record])
with pytest.raises(ScriptCatalogError, match=message):
ScriptCatalog.load(config, roots={"content_marketing": runtime})
def test_catalog_rejects_duplicate_command_ids(tmp_path: Path) -> None:
runtime = tmp_path / "runtime"
runtime.mkdir()
for name in ("first.py", "second.py"):
(runtime / name).write_text("print('ok')\n", encoding="utf-8")
config = tmp_path / "commands.json"
_write_registry(
config,
[
{
"id": "content.test.run",
"module": "content_marketing",
"entry": "first.py",
"kind": "python",
},
{
"id": "content.test.run",
"module": "content_marketing",
"entry": "second.py",
"kind": "python",
},
],
)
with pytest.raises(ScriptCatalogError, match="duplicate command id"):
ScriptCatalog.load(config, roots={"content_marketing": runtime})
def test_cli_lists_and_dry_runs_registered_commands(tmp_path: Path) -> None:
output = io.StringIO()
settings = Settings(project_root=PROJECT_ROOT, data_root=tmp_path)
assert main(["scripts", "list", "--json"], settings=settings, stdout=output) == 0
rows = json.loads(output.getvalue())
assert len(rows) == 49
assert {row["module"] for row in rows} == {
"content_marketing",
"product_commerce",
"shop_intelligence",
"supply_chain",
}
assert {
"id",
"command_id",
"legacy_script_id",
"module",
"entry",
"kind",
"default_args",
"notification_workflow_id",
} <= rows[0].keys()
output = io.StringIO()
exit_code = main(
[
"scripts",
"run",
"content.metrics.collect_collaborators",
"--date",
"2026-07-27",
],
settings=settings,
stdout=output,
)
payload = json.loads(output.getvalue())
assert exit_code == EXIT_SUCCESS
assert payload["dry_run"] is True
assert payload["command_id"] == "content.metrics.collect_collaborators"
assert payload["script_id"] == "content_marketing:run_all.py"
assert payload["steps"] == {"module_script": "skipped"}
def test_cli_accepts_legacy_script_id_alias(tmp_path: Path) -> None:
output = io.StringIO()
settings = Settings(project_root=PROJECT_ROOT, data_root=tmp_path)
exit_code = main(
[
"scripts",
"run",
"content_marketing:run_all.py",
"--date",
"2026-07-27",
],
settings=settings,
stdout=output,
)
payload = json.loads(output.getvalue())
assert exit_code == EXIT_SUCCESS
assert payload["command_id"] == "content.metrics.collect_collaborators"
assert payload["script_id"] == "content_marketing:run_all.py"
def test_cli_script_effect_identity_includes_arguments(tmp_path: Path) -> None:
settings = Settings(project_root=PROJECT_ROOT, data_root=tmp_path)
def dry_run(*extra: str) -> dict[str, object]:
output = io.StringIO()
exit_code = main(
[
"scripts",
"run",
"content.marketing_report.generate",
"--date",
"2026-07-27",
*extra,
],
settings=settings,
stdout=output,
)
assert exit_code == EXIT_SUCCESS
return json.loads(output.getvalue())
prompt_only = dry_run("--arg=--prompt-only")
prompt_only_again = dry_run("--arg=--prompt-only")
generate = dry_run()
assert prompt_only["workflow_id"] == prompt_only_again["workflow_id"]
assert prompt_only["workflow_id"] != generate["workflow_id"]
def test_cli_renders_command_business_date_defaults_into_effect_identity(
tmp_path: Path,
) -> None:
settings = Settings(project_root=PROJECT_ROOT, data_root=tmp_path)
def dry_run(business_date: str) -> dict[str, object]:
output = io.StringIO()
exit_code = main(
[
"scripts",
"run",
"product.backfill.run",
"--date",
business_date,
],
settings=settings,
stdout=output,
)
assert exit_code == EXIT_SUCCESS
return json.loads(output.getvalue())
first = dry_run("2026-07-27")
second = dry_run("2026-07-28")
assert first["workflow_id"] != second["workflow_id"]