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())