eduard/cobra-boards
Cobra Boards, team task boards you can run from Claude or ChatGPT. FastAPI + DataK3 + appid + Dodil Models + MCP.
A remix template · 1 remixes · Remix eduard/cobra-boards
tests/test_core.py
"""The rules that decide whether the board can be trusted: who sees what, who changes what, what
the history remembers, and what happens when two people edit at once."""
from datetime import timedelta
import pytest
from conftest import MEMBER, OWNER, STRANGER, TODAY, VIEWER
from boards.application.use_cases import NotFound
from boards.domain.model import Conflict, Forbidden, Invalid
# ---------------------------------------------------------------- visibility
def test_a_board_you_are_not_on_does_not_exist_for_you(svc):
s, bid = svc
card = s.add_card(OWNER, bid, "Secret plan")
with pytest.raises(NotFound):
s.show_board(STRANGER, bid)
with pytest.raises(NotFound):
s.card_detail(STRANGER, card["id"])
with pytest.raises(NotFound):
s.show_board(STRANGER, "Launch") # not even by name
assert s.my_boards(STRANGER) == []
assert s.overdue(STRANGER) == [] and s.search_cards(STRANGER, "secret") == []
def test_each_person_sees_their_role(svc):
s, bid = svc
roles = {p.email: s.my_boards(p)[0]["role"] for p in (OWNER, MEMBER, VIEWER)}
assert roles == {OWNER.email: "owner", MEMBER.email: "member", VIEWER.email: "viewer"}
view = s.show_board(VIEWER, bid)
assert view["can_edit"] is False and view["is_owner"] is False
# ---------------------------------------------------------------- roles
def test_viewer_can_look_but_not_change_anything(svc):
s, bid = svc
card = s.add_card(MEMBER, bid, "Write copy")
assert s.card_detail(VIEWER, card["id"])["title"] == "Write copy"
for attempt in (lambda: s.add_card(VIEWER, bid, "x"),
lambda: s.move_card(VIEWER, card["id"], "Done"),
lambda: s.update_card(VIEWER, card["id"], title="hacked"),
lambda: s.add_list(VIEWER, bid, "Ideas"),
lambda: s.add_checklist_items(VIEWER, card["id"], ["x"]),
lambda: s.invite(VIEWER, bid, "eve@cobra.test", "member")):
with pytest.raises(Forbidden):
attempt()
assert s.card_detail(OWNER, card["id"])["title"] == "Write copy"
def test_only_the_owner_manages_members(svc):
s, bid = svc
with pytest.raises(Forbidden):
s.invite(MEMBER, bid, "eve@cobra.test", "member")
with pytest.raises(Forbidden):
s.remove_member(MEMBER, bid, VIEWER.email)
with pytest.raises(Forbidden):
s.rename_board(MEMBER, bid, "Mine now")
with pytest.raises(Invalid):
s.invite(OWNER, bid, "eve@cobra.test", "owner") # one owner per board
def test_removing_a_member_unassigns_their_cards_with_history(svc):
s, bid = svc
card = s.add_card(OWNER, bid, "Call the printer", assignee=MEMBER.email)
out = s.remove_member(OWNER, bid, MEMBER.email)
assert out["cards_unassigned"] == 1
assert s.card_detail(OWNER, card["id"])["assignee"] is None
with pytest.raises(NotFound):
s.show_board(MEMBER, bid)
assert any("unassigned" in h["summary"] for h in s.card_history(OWNER, card["id"]))
def test_viewer_promoted_to_member_can_then_edit(svc):
s, bid = svc
s.invite(OWNER, bid, VIEWER.email, "member")
assert s.add_card(VIEWER, bid, "Now I can")["number"] == 1
def test_me_means_the_person_asking(svc):
s, bid = svc
assert s.add_card(MEMBER, bid, "Mine", assignee="me")["assignee"] == MEMBER.email
card = s.add_card(OWNER, bid, "Take it")
assert s.update_card(MEMBER, card["id"], assignee="Me")["assignee"] == MEMBER.email
def test_you_can_only_assign_people_on_the_board(svc):
s, bid = svc
with pytest.raises(Invalid):
s.add_card(OWNER, bid, "x", assignee=STRANGER.email)
assert s.add_card(OWNER, bid, "y", assignee=MEMBER.email)["assignee"] == MEMBER.email
# ---------------------------------------------------------------- cards & lists
def test_cards_get_short_numbers_per_board(svc):
s, bid = svc
a, b = s.add_card(OWNER, bid, "First"), s.add_card(MEMBER, bid, "Second")
other = s.create_board(OWNER, "Ops")
c = s.add_card(OWNER, other["id"], "Elsewhere")
assert (a["number"], b["number"], c["number"]) == (1, 2, 1)
def test_a_card_is_found_by_number_and_board_name(svc):
s, bid = svc
s.add_card(OWNER, bid, "First")
twelve = [s.add_card(OWNER, bid, f"c{i}") for i in range(2, 13)][-1]
moved = s.move_card(MEMBER, "12", "Done", board_ref="launch")
assert moved["id"] == twelve["id"] and moved["list"] == "Done" and moved["done"]
assert s.card_detail(OWNER, "#12")["title"] == "c12"
def test_ambiguous_card_numbers_ask_which_board(svc):
s, bid = svc
s.add_card(OWNER, bid, "Launch one")
ops = s.create_board(OWNER, "Ops")
s.add_card(OWNER, ops["id"], "Ops one")
with pytest.raises(Invalid, match="several of your boards"):
s.move_card(OWNER, "1", "Doing")
assert s.move_card(OWNER, "1", "doing", board_ref="Ops")["title"] == "Ops one"
def test_ambiguous_board_names_ask_instead_of_guessing(svc):
s, _ = svc
s.create_board(OWNER, "Ops north")
s.create_board(OWNER, "Ops south")
with pytest.raises(Invalid, match="2 boards match"):
s.show_board(OWNER, "ops")
assert s.show_board(OWNER, "Ops north")["title"] == "Ops north"
def test_lists_are_the_boards_own(svc):
s, bid = svc
s.add_list(OWNER, bid, "Review")
with pytest.raises(Invalid):
s.add_list(OWNER, bid, "review") # same name, any case
card = s.add_card(MEMBER, bid, "Proofread", list_name="REVIEW")
assert card["list"] == "Review"
with pytest.raises(Invalid, match="no list called"):
s.move_card(MEMBER, card["id"], "Someday")
s.update_list(OWNER, bid, "Review", name="QA")
assert s.card_detail(OWNER, card["id"])["list"] == "QA"
def test_drag_and_drop_puts_a_card_above_another(svc):
s, bid = svc
a, b, c = (s.add_card(OWNER, bid, t, list_name="Doing") for t in "abc")
s.move_card(OWNER, c["id"], "Doing", before_card=a["id"])
doing = next(l for l in s.show_board(OWNER, bid)["lists"] if l["name"] == "Doing")
assert [x["title"] for x in doing["cards"]] == ["c", "a", "b"]
def test_every_field_and_its_validation(svc):
s, bid = svc
card = s.add_card(OWNER, bid, "Plan", description="Rough idea", due_date="tomorrow",
labels=["Urgent", "urgent", "design"])
assert card["labels"] == ["urgent", "design"]
assert card["due_date"] == (TODAY + timedelta(days=1)).isoformat()
with pytest.raises(Invalid):
s.add_card(OWNER, bid, " ")
with pytest.raises(Invalid):
s.add_card(OWNER, bid, "x" * 201)
with pytest.raises(Invalid):
s.update_card(OWNER, card["id"], due_date="someday")
out = s.update_card(OWNER, card["id"], due_date=None, labels=[])
assert out["due_date"] is None and out["labels"] == []
# ---------------------------------------------------------------- history & conflicts
def test_history_remembers_who_what_and_before_after(svc):
s, bid = svc
card = s.add_card(OWNER, bid, "Send invoice")
s.update_card(MEMBER, card["id"], assignee=MEMBER.email, due_date="2026-11-12")
s.move_card(MEMBER, card["id"], "Done")
h = s.card_history(OWNER, card["id"])
assert [e["actor"] for e in h] == [MEMBER.email, MEMBER.email, OWNER.email]
assert h[0]["changes"]["list"] == ["To do", "Done"]
assert h[1]["changes"]["assignee"] == [None, MEMBER.email]
assert h[1]["changes"]["due_date"] == [None, "2026-11-12"]
assert all(e["at"] for e in h)
def test_a_stale_edit_is_refused_and_nothing_is_written(svc):
s, bid = svc
card = s.add_card(OWNER, bid, "Budget")
v = card["version"]
s.update_card(MEMBER, card["id"], expected_version=v, title="Budget v2")
with pytest.raises(Conflict):
s.update_card(OWNER, card["id"], expected_version=v, title="Budget (mine)")
assert s.card_detail(OWNER, card["id"])["title"] == "Budget v2"
def test_an_edit_that_changes_nothing_writes_no_history(svc):
s, bid = svc
card = s.add_card(OWNER, bid, "Same")
s.update_card(OWNER, card["id"], title="Same")
assert len(s.card_history(OWNER, card["id"])) == 1
# ---------------------------------------------------------------- my work & overdue
def test_my_week_groups_my_open_cards(svc):
s, bid = svc
s.add_card(OWNER, bid, "Late", assignee=MEMBER.email, due_date="2026-11-01")
s.add_card(OWNER, bid, "Soon", assignee=MEMBER.email, due_date="2026-11-13")
s.add_card(OWNER, bid, "Far", assignee=MEMBER.email, due_date="2026-12-30")
s.add_card(OWNER, bid, "Whenever", assignee=MEMBER.email)
done = s.add_card(OWNER, bid, "Finished", assignee=MEMBER.email, due_date="2026-11-02")
s.move_card(OWNER, done["id"], "Done")
s.add_card(OWNER, bid, "Not mine", assignee=OWNER.email, due_date="2026-11-11")
week = s.my_cards(MEMBER)
titles = {k: [c["title"] for c in v] for k, v in week.items() if k != "today"}
assert titles == {"overdue": ["Late"], "due_soon": ["Soon"], "no_due_date": ["Whenever"],
"later": ["Far"]}
def test_overdue_skips_done_cards_and_marks_the_rest(svc):
s, bid = svc
late = s.add_card(OWNER, bid, "Late", due_date="2026-11-09")
s.add_card(OWNER, bid, "Today is fine", due_date="2026-11-10")
finished = s.add_card(OWNER, bid, "Late but done", due_date="2026-11-01")
s.move_card(OWNER, finished["id"], "Done")
assert [c["title"] for c in s.overdue(VIEWER)] == ["Late"]
assert s.card_detail(OWNER, late["id"])["overdue"] is True
assert s.my_boards(OWNER)[0]["overdue"] == 1
# ---------------------------------------------------------------- dependencies
def test_what_is_blocking_walks_the_chain_and_says_where_to_start(svc):
s, bid = svc
ship = s.add_card(OWNER, bid, "Ship v1")
qa = s.add_card(OWNER, bid, "QA pass")
bug = s.add_card(OWNER, bid, "Fix login bug")
review = s.add_card(OWNER, bid, "Security review")
s.add_blocker(MEMBER, ship["id"], qa["id"])
s.add_blocker(MEMBER, qa["id"], bug["id"])
s.add_blocker(MEMBER, ship["id"], review["id"])
out = s.what_is_blocking(VIEWER, ship["id"])
assert {c["number"] for c in out["chain"]} == {2, 3, 4}
assert {c["title"] for c in out["start_with"]} == {"Fix login bug", "Security review"}
s.move_card(OWNER, bug["id"], "Done")
s.move_card(OWNER, review["id"], "Done")
assert [c["title"] for c in s.what_is_blocking(OWNER, ship["id"])["chain"]] == ["QA pass"]
assert s.card_detail(OWNER, ship["id"])["blocked_by"] == [2]
def test_cards_cannot_wait_on_each_other_in_a_loop(svc):
s, bid = svc
a, b, c = (s.add_card(OWNER, bid, t) for t in "abc")
s.add_blocker(OWNER, a["id"], b["id"])
s.add_blocker(OWNER, b["id"], c["id"])
with pytest.raises(Invalid, match="forever"):
s.add_blocker(OWNER, c["id"], a["id"])
with pytest.raises(Invalid):
s.add_blocker(OWNER, a["id"], a["id"])
def test_blockers_stay_on_one_board(svc):
s, bid = svc
a = s.add_card(OWNER, bid, "here")
ops = s.create_board(OWNER, "Ops")
b = s.add_card(OWNER, ops["id"], "there")
with pytest.raises((Invalid, NotFound)):
s.add_blocker(OWNER, a["id"], b["id"])
# ---------------------------------------------------------------- AI extras
def test_checklist_suggestion_changes_nothing_until_you_add_it(svc):
s, bid = svc
card = s.add_card(OWNER, bid, "Launch page")
sug = s.suggest_checklist(VIEWER, card["id"]) # viewers may ask; it saves nothing
assert sug["suggested_steps"] == ["Draft it", "Review it", "Ship it"]
assert s.card_detail(OWNER, card["id"])["checklist"] == []
out = s.add_checklist_items(MEMBER, card["id"], sug["suggested_steps"][:2])
first = out["checklist"][0]
s.set_checklist_item(MEMBER, card["id"], first["id"], done=True)
items = s.card_detail(OWNER, card["id"])["checklist"]
assert [(i["text"], i["done"]) for i in items] == [("Draft it", True), ("Review it", False)]
def test_similar_cards_finds_meaning_only_on_your_boards(svc):
s, bid = svc
target = s.add_card(OWNER, bid, "Send invoice to Acme")
s.add_card(OWNER, bid, "Chase unpaid bill")
s.add_card(OWNER, bid, "Design the logo")
secret = s.create_board(STRANGER, "Private")
s.add_card(STRANGER, secret["id"], "Invoice the secret client")
out = s.similar_cards(MEMBER, target["id"])
titles = [c["title"] for c in out["cards"]]
assert titles[0] == "Chase unpaid bill"
assert "Invoice the secret client" not in titles and "Send invoice to Acme" not in titles
def test_daily_summary_counts_are_real(svc):
s, bid = svc
s.add_card(OWNER, bid, "Late", due_date="2026-11-01")
out = s.daily_summary(VIEWER, bid)
assert out["facts"]["overdue"] == [1]
assert out["summary"].startswith("Briefing for Launch")
# ---------------------------------------------------------------- first visit
def test_first_visit_gets_one_private_sample_board(svc):
s, _ = svc
newbie = STRANGER
boards = s.first_visit(newbie)
assert [b["title"] for b in boards] == ["Welcome to Cobra 🐍"]
assert s.first_visit(newbie) == boards # no second copy
bid = boards[0]["id"]
view = s.show_board(newbie, bid)
assert [l["name"] for l in view["lists"]] == ["To do", "Doing", "Review", "Done"]
assert s.overdue(newbie) and s.what_is_blocking(newbie, "1", bid)["start_with"]
with pytest.raises(NotFound):
s.show_board(OWNER, bid) # private to the newcomer
assert s.first_visit(OWNER)[0]["title"] == "Launch" # people with boards get nothing extra