CI Pipeline / best-practices (push) Successful in 8s
Auto-version and Release / release (push) Failing after 14s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / lint (push) Failing after 34s
CI Pipeline / canary (push) Successful in 37s
CI Pipeline / type-check (push) Successful in 31s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / unit-tests (push) Successful in 1m4s
CI Pipeline / required-checks (push) Successful in 1s
deploy / deploy (push) Successful in 48s
CORRECTION 1: #314 reopened — recall_with_associations mode field defect CONFIRMED in current source and FIXED (mode: association_merged). CORRECTION 3: Gitea credential removed from remote URL (credential helper). TOKEN EXPOSED: yes (in diagnostic output). TOKEN ROTATED: not yet (requires Gitea web UI access). Remote URL now shows username only, no secret. CORRECTION 4: duplicate-release-commit defect ROOT CAUSE: convergence computed warranted bump from commits-since-tag without checking if master version already satisfies the bump. FIX: already-converged guard in version_convergence.py (master version > last tag → no commit). Repo-aware (args.repo passed to read_version/latest_tag). CORRECTION 6: lifecycle terminalization — execution exception now guarantees task terminalizes as failed with evidence (no immortal RUNNING). CORRECTION 7: route instruction task-scoped on RuneTask.route_instruction (NOT shared operator state). Read-only leading-verb override. Isolation tests prove no cross-contamination between interleaved tasks. PL1-01/02/03/05: pl1_records.py (route resolution, proof level, failure class, candidate status), metrics with honest NOT_YET_MEASURABLE semantics, route-instruction learning loop (PE-1789102658-0269 SEED). 1253 tests green; ratchet 352/80 (shrink); henxels clean.
173 lines
5.1 KiB
Python
173 lines
5.1 KiB
Python
"""Append-only outbox with stable IDs and atomic persistence."""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from rune.orchestration.atomic import AtomicStore
|
|
from rune.orchestration.outbox import CorruptOutboxError, OutboxStore
|
|
from rune.orchestration.records import OutboxRecord
|
|
|
|
|
|
def test_append_then_get_ids_and_ordering(tmp_path: Path):
|
|
store = OutboxStore(tmp_path / "outbox")
|
|
store.append(
|
|
OutboxRecord(
|
|
mutation_id="mut_a",
|
|
remote_target="gitea://rune/issues",
|
|
expected_remote_version=0,
|
|
)
|
|
)
|
|
store.append(
|
|
OutboxRecord(
|
|
mutation_id="mut_b",
|
|
remote_target="gitea://rune/issues",
|
|
expected_remote_version=0,
|
|
)
|
|
)
|
|
assert store.get("mut_a").mutation_id == "mut_a"
|
|
assert store.pending() == [store.get("mut_a"), store.get("mut_b")]
|
|
|
|
|
|
def test_append_is_idempotent_by_mutation_id(tmp_path: Path):
|
|
store = OutboxStore(tmp_path / "outbox")
|
|
r = OutboxRecord(
|
|
mutation_id="mut_x",
|
|
remote_target="gitea://rune/issues",
|
|
expected_remote_version=0,
|
|
)
|
|
store.append(r)
|
|
store.append(r) # same ID: no duplicate
|
|
assert len(store.all()) == 1
|
|
|
|
|
|
def test_persistence_persists_across_reopen(tmp_path: Path):
|
|
store = OutboxStore(tmp_path / "outbox")
|
|
store.append(
|
|
OutboxRecord(
|
|
mutation_id="mut_a",
|
|
remote_target="t",
|
|
expected_remote_version=0,
|
|
ordering_key=2,
|
|
)
|
|
)
|
|
store2 = OutboxStore(tmp_path / "outbox")
|
|
assert store2.get("mut_a").ordering_key == 2
|
|
assert store2.pending() == [store2.get("mut_a")]
|
|
|
|
|
|
def test_mark_replaces_without_duplication(tmp_path: Path):
|
|
store = OutboxStore(tmp_path / "outbox")
|
|
r = OutboxRecord(mutation_id="mut_a", remote_target="t", expected_remote_version=0)
|
|
store.append(r)
|
|
r2 = OutboxRecord(
|
|
mutation_id="mut_a",
|
|
remote_target="t",
|
|
expected_remote_version=0,
|
|
state="sending",
|
|
attempts=1,
|
|
)
|
|
store.mark(r2)
|
|
assert store.get("mut_a").state == "sending"
|
|
assert store.get("mut_a").attempts == 1
|
|
assert len(store.all()) == 1
|
|
|
|
|
|
def test_load_invalid_json_raises_corrupt(tmp_path: Path):
|
|
AtomicStore(tmp_path / "outbox" / "state").write("outbox.json", "{bad json")
|
|
try:
|
|
OutboxStore(tmp_path / "outbox")
|
|
raise AssertionError("expected CorruptOutboxError")
|
|
except CorruptOutboxError:
|
|
pass
|
|
|
|
|
|
def test_load_wrong_typed_state_raises_corrupt(tmp_path: Path):
|
|
AtomicStore(tmp_path / "outbox" / "state").write(
|
|
"outbox.json",
|
|
json.dumps(
|
|
[
|
|
{
|
|
"mutation_id": "m",
|
|
"remote_target": "t",
|
|
"expected_remote_version": 0,
|
|
"state": 123,
|
|
}
|
|
]
|
|
),
|
|
)
|
|
try:
|
|
OutboxStore(tmp_path / "outbox")
|
|
raise AssertionError("expected CorruptOutboxError")
|
|
except CorruptOutboxError:
|
|
pass
|
|
|
|
|
|
def test_load_wrong_typed_ordering_key_raises_corrupt(tmp_path: Path):
|
|
AtomicStore(tmp_path / "outbox" / "state").write(
|
|
"outbox.json",
|
|
json.dumps(
|
|
[
|
|
{
|
|
"mutation_id": "m",
|
|
"remote_target": "t",
|
|
"expected_remote_version": 0,
|
|
"ordering_key": "2",
|
|
}
|
|
]
|
|
),
|
|
)
|
|
try:
|
|
OutboxStore(tmp_path / "outbox")
|
|
raise AssertionError("expected CorruptOutboxError")
|
|
except CorruptOutboxError:
|
|
pass
|
|
|
|
|
|
def test_load_wrong_typed_expected_remote_version_raises_corrupt(tmp_path: Path):
|
|
AtomicStore(tmp_path / "outbox" / "state").write(
|
|
"outbox.json",
|
|
json.dumps([{"mutation_id": "m", "remote_target": "t", "expected_remote_version": "0"}]),
|
|
)
|
|
try:
|
|
OutboxStore(tmp_path / "outbox")
|
|
raise AssertionError("expected CorruptOutboxError")
|
|
except CorruptOutboxError:
|
|
pass
|
|
|
|
|
|
def test_mark_missing_mutation_id_raises_keyerror(tmp_path: Path):
|
|
store = OutboxStore(tmp_path / "outbox")
|
|
r = OutboxRecord(mutation_id="ghost", remote_target="t", expected_remote_version=0)
|
|
try:
|
|
store.mark(r)
|
|
raise AssertionError("expected KeyError")
|
|
except KeyError:
|
|
pass
|
|
|
|
|
|
def test_pending_set_membership_retryable_in_terminal_out(tmp_path: Path):
|
|
store = OutboxStore(tmp_path / "outbox")
|
|
for mid, state, key in [
|
|
("mut_queued", "queued", 0),
|
|
("mut_sending", "sending", 1),
|
|
("mut_retry", "retryable_failure", 2),
|
|
("mut_accepted", "accepted", 3),
|
|
("mut_conflict", "conflict", 4),
|
|
("mut_cancelled", "cancelled", 5),
|
|
("mut_superseded", "superseded", 6),
|
|
]:
|
|
store.append(
|
|
OutboxRecord(
|
|
mutation_id=mid,
|
|
remote_target="t",
|
|
expected_remote_version=0,
|
|
state=state,
|
|
ordering_key=key,
|
|
)
|
|
)
|
|
assert [r.mutation_id for r in store.pending()] == [
|
|
"mut_queued",
|
|
"mut_sending",
|
|
"mut_retry",
|
|
]
|