feat: expand module workflows, dynamic config, notifications and console API
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
@@ -17,9 +19,23 @@ from gyxx_flow.adapters.native import (
|
||||
ModuleSourceRoots,
|
||||
)
|
||||
from gyxx_flow.catalog import WorkflowEntry
|
||||
from gyxx_flow.core.config import Settings
|
||||
from gyxx_flow.core.context import RunContext
|
||||
from gyxx_flow.notification_routing import (
|
||||
ANALYZER_ACCEPTANCE_RECIPIENT_OPEN_ID,
|
||||
GYXX_NOTIFICATION_APP_PROFILE,
|
||||
GYXX_NOTIFICATION_HERMES_PROFILE,
|
||||
GYXX_NOTIFICATION_RECIPIENTS_JSON,
|
||||
GYXX_NOTIFICATION_ROUTE_MODE,
|
||||
GYXX_NOTIFICATION_ROUTE_REVISION,
|
||||
GYXX_NOTIFICATION_ROUTE_WORKFLOW_ID,
|
||||
NotificationRoutingStore,
|
||||
resolve_notification_route,
|
||||
)
|
||||
from gyxx_flow.workflow.steps import StepExecution
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _entry(**overrides: object) -> WorkflowEntry:
|
||||
values: dict[str, object] = {
|
||||
@@ -43,6 +59,15 @@ def _context() -> RunContext:
|
||||
)
|
||||
|
||||
|
||||
def _context_for(workflow_id: str) -> RunContext:
|
||||
return RunContext.create(
|
||||
workflow_id,
|
||||
"2026-07-27",
|
||||
now=datetime(2026, 7, 27, tzinfo=timezone.utc),
|
||||
random_suffix="native2",
|
||||
)
|
||||
|
||||
|
||||
def test_module_adapter_resolves_only_inside_new_module_root(tmp_path: Path) -> None:
|
||||
root = tmp_path / "product_commerce" / "runtime"
|
||||
entry = root / "jobs" / "backfill.py"
|
||||
@@ -273,3 +298,143 @@ def test_module_adapter_rejects_unsafe_acceptance_recipient(tmp_path: Path) -> N
|
||||
|
||||
with pytest.raises(WorkflowAcceptancePolicyError, match="Wang Yunlong"):
|
||||
adapter.build(_entry(entry="job.py", args=()), context=_context())
|
||||
|
||||
|
||||
def test_module_adapter_injects_stable_notification_route_before_acceptance(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
project_root = tmp_path / "project"
|
||||
config_root = project_root / "config"
|
||||
config_root.mkdir(parents=True)
|
||||
shutil.copyfile(
|
||||
PROJECT_ROOT / "config" / "notification-routing.json",
|
||||
config_root / "notification-routing.json",
|
||||
)
|
||||
data_root = tmp_path / "data"
|
||||
settings = Settings(project_root=project_root, data_root=data_root)
|
||||
store = NotificationRoutingStore(settings)
|
||||
payload, revision = store.snapshot()
|
||||
payload["routes"]["content.marketing_report.daily"] = {
|
||||
"enabled": True,
|
||||
"app_profile": "hermes-analyzer",
|
||||
"person_ids": ["he_yingwei", "wang_yunlong"],
|
||||
}
|
||||
store.update(payload, expected_revision=revision)
|
||||
|
||||
module_root = tmp_path / "content_marketing"
|
||||
module_root.mkdir()
|
||||
(module_root / "daily.py").write_text("pass", encoding="utf-8")
|
||||
adapter = ModuleCommandAdapter(
|
||||
ModuleSourceRoots({"content_marketing": module_root}),
|
||||
base_env={"GYXX_WORKFLOW_ACCEPTANCE": "1"},
|
||||
project_root=project_root,
|
||||
data_root=data_root,
|
||||
)
|
||||
entry = _entry(
|
||||
workflow_id="content.marketing_report.daily",
|
||||
module="content_marketing",
|
||||
entry="daily.py",
|
||||
args=(),
|
||||
)
|
||||
context = RunContext.create(
|
||||
entry.workflow_id,
|
||||
"2026-07-27",
|
||||
now=datetime(2026, 7, 27, tzinfo=timezone.utc),
|
||||
random_suffix="notify1",
|
||||
)
|
||||
|
||||
command = adapter.build(entry, context=context)
|
||||
|
||||
assert command.env[GYXX_NOTIFICATION_ROUTE_MODE] == "enabled"
|
||||
assert command.env[GYXX_NOTIFICATION_APP_PROFILE] == "hermes-analyzer"
|
||||
assert json.loads(command.env[GYXX_NOTIFICATION_RECIPIENTS_JSON]) == [
|
||||
ANALYZER_ACCEPTANCE_RECIPIENT_OPEN_ID
|
||||
]
|
||||
assert (
|
||||
command.env["GYXX_NOTIFICATION_RECIPIENT_OPEN_ID"]
|
||||
== WORKFLOW_ACCEPTANCE_RECIPIENT_OPEN_ID
|
||||
)
|
||||
assert resolve_notification_route(entry.workflow_id, (), command.env).open_ids == (
|
||||
ANALYZER_ACCEPTANCE_RECIPIENT_OPEN_ID,
|
||||
)
|
||||
|
||||
|
||||
def test_module_adapter_strips_route_snapshot_from_synthetic_manual_command(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
module_root = tmp_path / "product_commerce"
|
||||
module_root.mkdir()
|
||||
(module_root / "run_alerts.py").write_text("pass", encoding="utf-8")
|
||||
adapter = ModuleCommandAdapter(
|
||||
ModuleSourceRoots({"product_commerce": module_root}),
|
||||
base_env={
|
||||
GYXX_NOTIFICATION_ROUTE_MODE: "enabled",
|
||||
GYXX_NOTIFICATION_APP_PROFILE: "hermes-analyzer",
|
||||
GYXX_NOTIFICATION_HERMES_PROFILE: "data-analyzer",
|
||||
GYXX_NOTIFICATION_RECIPIENTS_JSON: (
|
||||
'["ou_89bcff110ccbb09a23548dc0fb3d880c"]'
|
||||
),
|
||||
GYXX_NOTIFICATION_ROUTE_REVISION: "0" * 64,
|
||||
GYXX_NOTIFICATION_ROUTE_WORKFLOW_ID: "content.marketing_report.daily",
|
||||
},
|
||||
project_root=tmp_path,
|
||||
data_root=tmp_path / "var",
|
||||
)
|
||||
|
||||
synthetic_id = "script.product_commerce.0123456789ab"
|
||||
command = adapter.build(
|
||||
_entry(
|
||||
workflow_id=synthetic_id,
|
||||
entry="run_alerts.py",
|
||||
args=(),
|
||||
),
|
||||
context=_context_for(synthetic_id),
|
||||
)
|
||||
|
||||
for variable in (
|
||||
GYXX_NOTIFICATION_ROUTE_MODE,
|
||||
GYXX_NOTIFICATION_APP_PROFILE,
|
||||
GYXX_NOTIFICATION_HERMES_PROFILE,
|
||||
GYXX_NOTIFICATION_RECIPIENTS_JSON,
|
||||
GYXX_NOTIFICATION_ROUTE_REVISION,
|
||||
GYXX_NOTIFICATION_ROUTE_WORKFLOW_ID,
|
||||
):
|
||||
assert variable not in command.env
|
||||
|
||||
|
||||
def test_module_adapter_snapshots_declared_route_for_public_manual_command(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
project_root = tmp_path / "project"
|
||||
config_root = project_root / "config"
|
||||
config_root.mkdir(parents=True)
|
||||
shutil.copyfile(
|
||||
PROJECT_ROOT / "config" / "notification-routing.json",
|
||||
config_root / "notification-routing.json",
|
||||
)
|
||||
module_root = tmp_path / "content_marketing"
|
||||
module_root.mkdir()
|
||||
(module_root / "daily.py").write_text("pass", encoding="utf-8")
|
||||
synthetic_id = "script.content_marketing.0123456789ab"
|
||||
adapter = ModuleCommandAdapter(
|
||||
ModuleSourceRoots({"content_marketing": module_root}),
|
||||
project_root=project_root,
|
||||
data_root=tmp_path / "data",
|
||||
)
|
||||
|
||||
command = adapter.build(
|
||||
_entry(
|
||||
workflow_id=synthetic_id,
|
||||
module="content_marketing",
|
||||
entry="daily.py",
|
||||
args=(),
|
||||
notification_workflow_id="content.marketing_report.daily",
|
||||
),
|
||||
context=_context_for(synthetic_id),
|
||||
)
|
||||
|
||||
assert command.env[GYXX_NOTIFICATION_ROUTE_MODE] == "legacy"
|
||||
assert (
|
||||
command.env[GYXX_NOTIFICATION_ROUTE_WORKFLOW_ID]
|
||||
== "content.marketing_report.daily"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user