feat: consolidate legacy workflows into gyxx-flow
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from gyxx_flow.adapters.native import (
|
||||
DeferredModuleCommandStep,
|
||||
ModuleCommandAdapter,
|
||||
ModuleSourceError,
|
||||
ModuleSourceRoots,
|
||||
)
|
||||
from gyxx_flow.catalog import WorkflowEntry
|
||||
from gyxx_flow.core.context import RunContext
|
||||
|
||||
|
||||
def _entry(**overrides: object) -> WorkflowEntry:
|
||||
values: dict[str, object] = {
|
||||
"workflow_id": "product.backfill",
|
||||
"module": "product_commerce",
|
||||
"trigger": "manual",
|
||||
"entry": "jobs/backfill.py",
|
||||
"args": ("--limit", "25"),
|
||||
"source_project": "product",
|
||||
}
|
||||
values.update(overrides)
|
||||
return WorkflowEntry(**values) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def _context() -> RunContext:
|
||||
return RunContext.create(
|
||||
"product.backfill",
|
||||
"2026-07-27",
|
||||
now=datetime(2026, 7, 27, tzinfo=timezone.utc),
|
||||
random_suffix="native1",
|
||||
)
|
||||
|
||||
|
||||
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"
|
||||
entry.parent.mkdir(parents=True)
|
||||
entry.write_text("pass", encoding="utf-8")
|
||||
adapter = ModuleCommandAdapter(
|
||||
ModuleSourceRoots({"product_commerce": root}),
|
||||
python_executable=sys.executable,
|
||||
base_env={"PATH": "portable"},
|
||||
project_root=tmp_path,
|
||||
data_root=tmp_path / "var",
|
||||
)
|
||||
|
||||
command = adapter.build(_entry(), context=_context())
|
||||
|
||||
assert command.argv == (sys.executable, str(entry.resolve()), "--limit", "25")
|
||||
assert command.cwd == root.resolve()
|
||||
assert command.env["GYXX_PROJECT_ROOT"] == str(tmp_path.resolve())
|
||||
assert command.env["GYXX_DATA_ROOT"] == str((tmp_path / "var").resolve())
|
||||
assert command.env["GYXX_MODULE_ROOT"] == str(root.resolve())
|
||||
assert not any(name.startswith("GYXX_LEGACY_") for name in command.env)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("entry", ("../outside.py", "/outside.py", "C:/outside.py"))
|
||||
def test_module_adapter_rejects_paths_outside_module(tmp_path: Path, entry: str) -> None:
|
||||
root = tmp_path / "runtime"
|
||||
root.mkdir()
|
||||
adapter = ModuleCommandAdapter(ModuleSourceRoots({"product_commerce": root}))
|
||||
|
||||
with pytest.raises(ModuleSourceError, match="relative|escape"):
|
||||
adapter.build(_entry(entry=entry), context=_context())
|
||||
|
||||
|
||||
def test_deferred_module_step_dry_run_never_resolves_or_executes() -> None:
|
||||
def fail(*args: object, **kwargs: object) -> object:
|
||||
raise AssertionError("dry-run must not resolve or execute module source")
|
||||
|
||||
execution = DeferredModuleCommandStep(_entry(), command_factory=fail).execute(
|
||||
context=_context(),
|
||||
timeout_seconds=60,
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
assert execution.exit_code == 0
|
||||
assert execution.skipped is True
|
||||
assert execution.reason == "dry-run"
|
||||
|
||||
|
||||
def test_module_adapter_uses_explicit_windows_launchers_for_batch_and_powershell(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
root = tmp_path / "runtime"
|
||||
root.mkdir()
|
||||
(root / "daily.bat").write_text("@echo off", encoding="utf-8")
|
||||
(root / "maint.ps1").write_text("Write-Output ok", encoding="utf-8")
|
||||
adapter = ModuleCommandAdapter(
|
||||
ModuleSourceRoots({"product_commerce": root}),
|
||||
base_env={},
|
||||
project_root=tmp_path,
|
||||
data_root=tmp_path / "var",
|
||||
)
|
||||
|
||||
batch = adapter.build(_entry(entry="daily.bat", args=()), context=_context())
|
||||
powershell = adapter.build(
|
||||
_entry(entry="maint.ps1", args=()), context=_context()
|
||||
)
|
||||
|
||||
assert batch.argv[:4] == ("cmd.exe", "/d", "/s", "/c")
|
||||
assert str((root / "daily.bat").resolve()) in batch.argv[4]
|
||||
assert powershell.argv[:5] == (
|
||||
"powershell.exe",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
)
|
||||
|
||||
|
||||
def test_product_config_uses_only_the_new_environment_name(tmp_path: Path) -> None:
|
||||
root = tmp_path / "runtime"
|
||||
root.mkdir()
|
||||
(root / "job.py").write_text("pass", encoding="utf-8")
|
||||
adapter = ModuleCommandAdapter(
|
||||
ModuleSourceRoots({"product_commerce": root}),
|
||||
base_env={"GYXX_PRODUCT_CONFIG": "D:/portable/product.json"},
|
||||
project_root=tmp_path,
|
||||
data_root=tmp_path / "var",
|
||||
)
|
||||
|
||||
command = adapter.build(_entry(entry="job.py", args=()), context=_context())
|
||||
|
||||
assert command.env["GYXX_PRODUCT_CONFIG"] == "D:/portable/product.json"
|
||||
assert "AUTO_FLOW_CONFIG" not in command.env
|
||||
Reference in New Issue
Block a user