Files
rune/tests/test_orchestration_outbox.py
T
Nick ca4f7593d7
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
feat(corrections): #314 defect fix, credential rotation, release guard, task lifecycle, route scoping, PL1 records + metrics
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.
2026-09-11 15:36:08 +01:00

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",
]