from __future__ import annotations import subprocess from pathlib import Path import pytest from gyxx_flow.adapters import ( BrowserProfileManager, FeishuOutboxAdapter, HermesCommandAdapter, PostgresOutboxAdapter, ) from gyxx_flow.core.context import RunContext from gyxx_flow.core.locks import ResourceBusyError from gyxx_flow.ops import Outbox def test_feishu_and_postgres_writes_share_idempotent_outbox_boundary(tmp_path: Path) -> None: outbox = Outbox(tmp_path) feishu = FeishuOutboxAdapter(outbox) postgres = PostgresOutboxAdapter(outbox) first = feishu.upsert( run_id="run-001", operation_key="weekly-report", artifact_id="artifact-report-001", ) duplicate = feishu.upsert( run_id="run-001", operation_key="weekly-report", artifact_id="artifact-report-001", ) db_message = postgres.upsert( run_id="run-001", operation_key="shop-metrics", artifact_id="artifact-metrics-001", ) assert duplicate == first assert first.topic == "feishu.upsert" assert db_message.topic == "postgres.upsert" assert first.payload == {"artifact_id": "artifact-report-001"} assert db_message.payload == {"artifact_id": "artifact-metrics-001"} assert first.idempotency_key != db_message.idempotency_key def test_hermes_adapter_builds_explicit_command_and_dry_run_starts_no_process( monkeypatch, tmp_path: Path ) -> None: prompt = tmp_path / "prompt.md" prompt.write_text("analyze artifact-001", encoding="utf-8") adapter = HermesCommandAdapter(executable="hermes", base_env={"PROFILE": "analysis"}) command = adapter.build(prompt_file=prompt, cwd=tmp_path) assert command.argv == ("hermes", "-z", "analyze artifact-001") assert command.cwd == tmp_path.resolve() assert command.env == {"PROFILE": "analysis"} monkeypatch.setattr( subprocess, "run", lambda *args, **kwargs: pytest.fail("dry-run must not start Hermes"), ) context = RunContext.create("product.style", "2026-07-27") outcome = command.execute(context=context, timeout_seconds=60, dry_run=True) assert outcome.skipped is True def test_browser_profiles_are_portable_and_exclusively_locked(tmp_path: Path) -> None: manager = BrowserProfileManager(tmp_path) with manager.acquire("shop-compass", owner="run-001") as first: assert first.profile_path == ( tmp_path / "state" / "browser_profiles" / "shop-compass" ).resolve() assert first.profile_path.is_dir() with pytest.raises(ResourceBusyError): with manager.acquire( "shop-compass", owner="run-002", timeout_seconds=0 ): pass with manager.acquire("shop-compass", owner="run-002", timeout_seconds=0) as second: assert second.profile_path == first.profile_path @pytest.mark.parametrize("unsafe", ["../escape", "a/b", "a\\b", "", ".."]) def test_browser_profile_id_rejects_path_escape(tmp_path: Path, unsafe: str) -> None: with pytest.raises(ValueError, match="profile"): with BrowserProfileManager(tmp_path).acquire(unsafe, owner="run-001"): pass