94 lines
3.1 KiB
Python
94 lines
3.1 KiB
Python
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
|
|
|