from __future__ import annotations
import json
import subprocess
import xml.etree.ElementTree as ET
from datetime import date
from pathlib import Path
from gyxx_flow.catalog import WorkflowCatalog
from gyxx_flow.scheduler import (
CurrentScheduledTask,
PowerShellCurrentTaskProvider,
SchedulerConfig,
build_schedule_plan,
detect_schedule_drift,
write_schedule_plan_bundle,
)
PROJECT_ROOT = Path(__file__).resolve().parents[1]
NS = {"t": "http://schemas.microsoft.com/windows/2004/02/mit/task"}
def test_windows_provider_reads_managed_tasks_without_mutation(monkeypatch) -> None:
captured: dict[str, object] = {}
def fake_run(command, **kwargs):
captured["command"] = command
captured["kwargs"] = kwargs
return subprocess.CompletedProcess(
command,
0,
stdout=json.dumps(
[{"full_name": r"\GYXX\product.daily", "xml": ""}]
),
stderr="",
)
monkeypatch.setattr(subprocess, "run", fake_run)
tasks = PowerShellCurrentTaskProvider().current_tasks("\\GYXX\\")
assert tasks == [CurrentScheduledTask(r"\GYXX\product.daily", "")]
assert captured["command"][0] == "powershell.exe"
assert "Register-ScheduledTask" not in captured["command"][-1]
assert captured["kwargs"]["shell"] is False
def _plan(tmp_path: Path):
catalog = WorkflowCatalog.load(PROJECT_ROOT / "config")
return build_schedule_plan(
catalog,
SchedulerConfig(
python_executable=Path(r"C:\Portable\Python312\python.exe"),
project_root=tmp_path / "portable-project",
),
start_date=date(2026, 7, 27),
)
def test_plan_generates_21_inert_portable_windows_task_definitions(tmp_path: Path) -> None:
plan = _plan(tmp_path)
assert plan.timezone == "Asia/Shanghai"
assert len(plan.tasks) == 21
assert len({task.full_name for task in plan.tasks}) == 21
assert all(task.full_name.startswith("\\GYXX\\") for task in plan.tasks)
assert all("Register-ScheduledTask" not in task.xml for task in plan.tasks)
sample = next(task for task in plan.tasks if task.workflow_id == "product.daily")
root = ET.fromstring(sample.xml)
assert root.findtext(".//t:Exec/t:Command", namespaces=NS) == str(
plan.config.python_executable
)
assert root.findtext(".//t:Exec/t:Arguments", namespaces=NS) == (
"-m gyxx_flow run product.daily --scheduled"
)
assert root.findtext(".//t:Exec/t:WorkingDirectory", namespaces=NS) == str(
plan.config.project_root
)
assert root.findtext(".//t:MultipleInstancesPolicy", namespaces=NS) == "IgnoreNew"
def test_plan_preserves_daily_weekly_monthly_and_interval_semantics(tmp_path: Path) -> None:
plan = _plan(tmp_path)
by_id = {task.workflow_id: ET.fromstring(task.xml) for task in plan.tasks}
assert by_id["product.daily"].findtext(
".//t:ScheduleByDay/t:DaysInterval", namespaces=NS
) == "1"
weekly = by_id["content.comments.weekly"]
assert weekly.find(".//t:ScheduleByWeek/t:DaysOfWeek/t:Sunday", NS) is not None
monthly = by_id["content.summary.monthly"]
assert monthly.findtext(
".//t:ScheduleByMonth/t:DaysOfMonth/t:Day", namespaces=NS
) == "1"
interval = by_id["product.style_analysis.interval"]
assert interval.findtext(
".//t:ScheduleByDay/t:DaysInterval", namespaces=NS
) == "3"
assert interval.findtext(".//t:StartBoundary", namespaces=NS).startswith(
"2026-07-25T11:00:00"
)
def test_bundle_write_is_a_plan_only_and_never_applies_tasks(tmp_path: Path) -> None:
plan = _plan(tmp_path)
destination = tmp_path / "schedule-plan"
written = write_schedule_plan_bundle(destination, plan)
assert len(list((destination / "tasks").glob("*.xml"))) == 21
assert written == destination.resolve()
payload = json.loads((destination / "plan.json").read_text(encoding="utf-8"))
assert payload["apply_required"] is True
assert payload["applied"] is False
install = (destination / "install.ps1").read_text(encoding="utf-8")
assert "Register-ScheduledTask" in install
assert "China Standard Time" in install
assert "-WhatIf" in install
assert "[string]$WorkflowId" in install
assert "Apply requires exactly one safe -WorkflowId" in install
assert "ForEach-Object" not in install
def test_drift_reports_missing_changed_and_managed_extra_without_values(
tmp_path: Path,
) -> None:
plan = _plan(tmp_path)
first, second, *rest = plan.tasks
changed_xml = second.xml.replace("--scheduled", "--changed")
current = [
CurrentScheduledTask(first.full_name, first.xml),
CurrentScheduledTask(second.full_name, changed_xml),
*[CurrentScheduledTask(task.full_name, task.xml) for task in rest],
CurrentScheduledTask(r"\GYXX\obsolete.workflow", first.xml),
CurrentScheduledTask(r"\Unrelated\keep", "secret-value-must-not-appear"),
]
current = [item for item in current if item.full_name != rest[-1].full_name]
report = detect_schedule_drift(plan, current)
assert report.is_clean is False
assert [(item.full_name, item.status) for item in report.items] == [
(second.full_name, "changed"),
(rest[-1].full_name, "missing"),
(r"\GYXX\obsolete.workflow", "extra"),
]
assert "secret-value-must-not-appear" not in json.dumps(report.as_dict())