feat: complete production workflow migration
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from gyxx_flow.cli import EXIT_CONFIGURATION, EXIT_SUCCESS, main
|
||||
from gyxx_flow.core.config import Settings
|
||||
from gyxx_flow.ops.effects import (
|
||||
EffectAlreadyApplied,
|
||||
EffectLedger,
|
||||
@@ -64,3 +68,165 @@ def test_effect_ledger_leaves_in_progress_claim_ambiguous_after_crash(
|
||||
run_id="run-002",
|
||||
)
|
||||
|
||||
|
||||
def test_effect_ledger_reconcile_retry_preserves_audit_and_allows_one_new_claim(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
ledger = EffectLedger(tmp_path)
|
||||
failed = ledger.begin(
|
||||
workflow_id="product.daily",
|
||||
business_date="2026-08-01",
|
||||
step_id="collect",
|
||||
run_id="run-original",
|
||||
)
|
||||
ledger.mark_ambiguous(failed, reason="preflight-failed")
|
||||
|
||||
result = ledger.reconcile(
|
||||
workflow_id="product.daily",
|
||||
business_date="2026-08-01",
|
||||
step_id="collect",
|
||||
expected_run_id="run-original",
|
||||
action="retry",
|
||||
operator="operator@example",
|
||||
reason="verified command did not reach the external sink",
|
||||
evidence="journal:run-original",
|
||||
)
|
||||
retry = ledger.begin(
|
||||
workflow_id="product.daily",
|
||||
business_date="2026-08-01",
|
||||
step_id="collect",
|
||||
run_id="run-retry",
|
||||
)
|
||||
|
||||
assert result["status"] == "retry_allowed"
|
||||
assert retry.run_id == "run-retry"
|
||||
receipt_path = next((tmp_path / "state" / "ops" / "effects").glob("*.json"))
|
||||
receipt = json.loads(receipt_path.read_text(encoding="utf-8"))
|
||||
assert receipt["status"] == "in_progress"
|
||||
assert receipt["run_id"] == "run-retry"
|
||||
assert receipt["reconciliations"] == [
|
||||
{
|
||||
"action": "retry",
|
||||
"operator": "operator@example",
|
||||
"reason": "verified command did not reach the external sink",
|
||||
"evidence": "journal:run-original",
|
||||
"expected_run_id": "run-original",
|
||||
"reconciled_at": result["reconciled_at"],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_effect_ledger_reconcile_can_mark_verified_effect_applied(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
ledger = EffectLedger(tmp_path)
|
||||
claim = ledger.begin(
|
||||
workflow_id="product.alert.daily",
|
||||
business_date="2026-08-01",
|
||||
step_id="notify",
|
||||
run_id="run-original",
|
||||
)
|
||||
ledger.mark_ambiguous(claim, reason="response-lost")
|
||||
|
||||
result = ledger.reconcile(
|
||||
workflow_id="product.alert.daily",
|
||||
business_date="2026-08-01",
|
||||
step_id="notify",
|
||||
expected_run_id="run-original",
|
||||
action="applied",
|
||||
operator="operator@example",
|
||||
reason="verified the message receipt in Feishu",
|
||||
evidence="message:verified-receipt",
|
||||
)
|
||||
|
||||
assert result["status"] == "applied"
|
||||
with pytest.raises(EffectAlreadyApplied):
|
||||
ledger.begin(
|
||||
workflow_id="product.alert.daily",
|
||||
business_date="2026-08-01",
|
||||
step_id="notify",
|
||||
run_id="run-second",
|
||||
)
|
||||
|
||||
|
||||
def test_effect_ledger_reconcile_rejects_active_or_changed_claims(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
ledger = EffectLedger(tmp_path)
|
||||
claim = ledger.begin(
|
||||
workflow_id="product.daily",
|
||||
business_date="2026-08-02",
|
||||
step_id="collect",
|
||||
run_id="run-active",
|
||||
)
|
||||
|
||||
with pytest.raises(EffectStateAmbiguous, match="only ambiguous"):
|
||||
ledger.reconcile(
|
||||
workflow_id="product.daily",
|
||||
business_date="2026-08-02",
|
||||
step_id="collect",
|
||||
expected_run_id="run-active",
|
||||
action="retry",
|
||||
operator="operator@example",
|
||||
reason="should not unlock a live claim",
|
||||
evidence="journal:run-active",
|
||||
)
|
||||
|
||||
ledger.mark_ambiguous(claim, reason="failed")
|
||||
with pytest.raises(EffectStateAmbiguous, match="run_id changed"):
|
||||
ledger.reconcile(
|
||||
workflow_id="product.daily",
|
||||
business_date="2026-08-02",
|
||||
step_id="collect",
|
||||
expected_run_id="run-other",
|
||||
action="retry",
|
||||
operator="operator@example",
|
||||
reason="stale operator view",
|
||||
evidence="journal:run-other",
|
||||
)
|
||||
|
||||
|
||||
def test_effect_reconcile_cli_requires_confirmation_and_returns_safe_projection(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
data_root = tmp_path / "data"
|
||||
settings = Settings(project_root=tmp_path, data_root=data_root)
|
||||
ledger = EffectLedger(data_root)
|
||||
claim = ledger.begin(
|
||||
workflow_id="product.daily",
|
||||
business_date="2026-08-03",
|
||||
step_id="collect",
|
||||
run_id="run-original",
|
||||
)
|
||||
ledger.mark_ambiguous(claim, reason="preflight-failed")
|
||||
base_args = [
|
||||
"effects",
|
||||
"reconcile",
|
||||
"product.daily",
|
||||
"--date",
|
||||
"2026-08-03",
|
||||
"--step",
|
||||
"collect",
|
||||
"--expected-run-id",
|
||||
"run-original",
|
||||
"--action",
|
||||
"retry",
|
||||
"--operator",
|
||||
"operator@example",
|
||||
"--reason",
|
||||
"verified no external write",
|
||||
"--evidence",
|
||||
"journal:run-original",
|
||||
]
|
||||
|
||||
errors = StringIO()
|
||||
assert main(base_args, settings=settings, stderr=errors) == EXIT_CONFIGURATION
|
||||
assert "requires --execute" in errors.getvalue()
|
||||
|
||||
output = StringIO()
|
||||
assert main([*base_args, "--execute"], settings=settings, stdout=output) == EXIT_SUCCESS
|
||||
result = json.loads(output.getvalue())
|
||||
assert result["status"] == "retry_allowed"
|
||||
assert "operator" not in result
|
||||
assert "reason" not in result
|
||||
assert "evidence" not in result
|
||||
|
||||
Reference in New Issue
Block a user