Files
openworker/tests/test_team_store.py
T

112 lines
3.7 KiB
Python

"""Event-store core: append-only doctrine, hash chain, deliveries, spaces, rebuild."""
import sqlite3
import pytest
from coworker.teams import Actor, ChainError, Role, TeamStore
USER = Actor(id="user", role=Role.USER)
LEAD = Actor(id="lead-1", role=Role.LEAD, persona="swe-lead")
@pytest.fixture
def store(tmp_path):
store = TeamStore(tmp_path / "teams.db")
yield store
store.close()
def seed(store, space="proj"):
return store.create_item(
space, LEAD, title="Review api", criteria="every route triaged"
)
def test_events_hash_chain_verifies(store):
seed(store)
store.create_item("proj", LEAD, title="Second", criteria="done means done")
assert store.verify_chain("proj") == 2
def test_out_of_band_edit_breaks_the_chain(store):
seed(store)
# The append-only property can't be enforced against someone with the sqlite
# file; the chain makes the edit visible. Tamper directly:
conn = sqlite3.connect(store.db_path)
conn.execute("UPDATE team_events SET payload = '{\"title\":\"forged\"}' WHERE seq = 1")
conn.commit()
conn.close()
with pytest.raises(ChainError):
store.verify_chain("proj")
def test_deleting_an_event_breaks_linkage(store):
seed(store)
store.create_item("proj", LEAD, title="Second", criteria="c")
conn = sqlite3.connect(store.db_path)
conn.execute("DELETE FROM team_events WHERE seq = 2")
conn.commit()
conn.close()
with pytest.raises(ChainError):
store.verify_chain("proj")
def test_chains_are_per_space(store):
seed(store, "alpha")
seed(store, "beta")
conn = sqlite3.connect(store.db_path)
conn.execute("UPDATE team_events SET taint = 1 WHERE space = 'beta'")
conn.commit()
conn.close()
assert store.verify_chain("alpha") == 1 # untouched space still verifies
with pytest.raises(ChainError):
store.verify_chain("beta")
def test_spaces_created_lazily_and_isolated(store):
assert store.spaces() == []
seed(store, "alpha")
seed(store, "beta")
assert store.spaces() == ["alpha", "beta"]
assert [i["id"] for i in store.list_items("alpha", USER)] == [1] # ids per space
assert [i["id"] for i in store.list_items("beta", USER)] == [1]
def test_assignment_lands_in_the_assignees_feed(store):
item = seed(store)
store.assign("proj", LEAD, item["id"], "worker-1")
deliveries = store.feed_for("proj", "worker-1")
assert [e["kind"] for e in deliveries] == ["item_created", "item_assigned"]
assert deliveries[-1]["payload"]["assignee"] == "worker-1"
assert store.feed_for("proj", "worker-2") == []
def test_rebuild_reproduces_the_projection(store):
item = seed(store)
worker = Actor(id="worker-1", role=Role.WORKER)
store.assign("proj", LEAD, item["id"], "worker-1")
store.transition("proj", worker, item["id"], "in_progress")
store.comment("proj", worker, item["id"], "halfway", taint=True)
before = store.list_items("proj", USER)
store.rebuild("proj")
after = store.list_items("proj", USER)
assert after == before
assert after[0]["state"] == "in_progress"
assert after[0]["assignee"] == "worker-1"
# created_ts comes from the event, so replay is deterministic
assert (
store.get_item("proj", item["id"], actor=USER)["created_ts"]
== item["created_ts"]
)
def test_taint_travels_with_the_record(store):
item = seed(store)
store.assign("proj", LEAD, item["id"], "worker-1")
worker = Actor(id="worker-1", role=Role.WORKER)
store.comment("proj", worker, item["id"], "repo says the bucket is public", taint=True)
comments = store.comments("proj", item["id"])
assert comments[-1]["taint"] == 1
assert comments[-1]["role"] == "worker"