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

scripts/live_check.py

"""End-to-end check against REAL Dodil services: DataK3 and Dodil Models.

Run it against a TEST bucket, never the live one:
    BUCKET=cobra-dev DODIL_SERVICE_ACCOUNT_ID=... DODIL_SERVICE_ACCOUNT_SECRET=... \
    python scripts/live_check.py

It plays a small team story through the same use cases the web page and MCP use, then checks
what DataK3 actually stored. Every board it makes is prefixed "live-check" so it is easy to spot.
"""
import os
import sys
import time

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "app"))

from boards.application.use_cases import (
    Boards,  # noqa: E402
    NotFound,  # noqa: E402
)
from boards.domain.model import Conflict, Forbidden, Person  # noqa: E402
from boards.infrastructure.dodil import DodilObjects, ModelsAssistant  # noqa: E402
from boards.infrastructure.repository import SqlBoards  # noqa: E402

OWNER, MEMBER, VIEWER, STRANGER = (Person(f"{n}@live-check.test")
                                   for n in ("owner", "member", "viewer", "stranger"))


def step(name, fn):
    t = time.time()
    out = fn()
    print(f"  ok  {name}  ({(time.time() - t) * 1000:.0f} ms)")
    return out


def expect_error(name, exc, fn):
    try:
        fn()
    except exc:
        print(f"  ok  {name}  (refused)")
        return
    raise AssertionError(f"{name}: expected {exc.__name__}")


def main():
    if os.environ.get("BUCKET", "").endswith("-data"):
        sys.exit("refusing to run against a live '-data' bucket; use the test copy")
    s = Boards(SqlBoards(), ModelsAssistant(), objects=DodilObjects())
    print(f"live check on bucket {os.environ['BUCKET']}")
    b = step("create board", lambda: s.create_board(OWNER, f"live-check {int(time.time())}"))
    bid = b["id"]
    step("invite member + viewer", lambda: (s.invite(OWNER, bid, MEMBER.email, "member"),
                                            s.invite(OWNER, bid, VIEWER.email, "viewer")))
    inv = step("add card", lambda: s.add_card(MEMBER, bid, "Send invoice to Acme",
                                              description="Q4 retainer, net 30",
                                              assignee=MEMBER.email, due_date="2026-01-01",
                                              labels=["finance"]))
    bill = step("add card 2", lambda: s.add_card(OWNER, bid, "Chase the unpaid bill"))
    logo = step("add card 3", lambda: s.add_card(OWNER, bid, "Design the new logo"))
    assert (inv["number"], bill["number"], logo["number"]) == (1, 2, 3), "numbers"
    step("move card by number", lambda: s.move_card(MEMBER, "2", "Doing", board_ref=bid))
    expect_error("viewer can't edit", Forbidden, lambda: s.add_card(VIEWER, bid, "nope"))
    expect_error("stale edit refused", Conflict,
                 lambda: s.update_card(OWNER, inv["id"], expected_version=0, title="x"))
    step("blocker", lambda: s.add_blocker(OWNER, logo["id"], inv["id"]))
    chain = step("what is blocking", lambda: s.what_is_blocking(VIEWER, logo["id"]))
    assert chain["start_with"][0]["number"] == 1, chain
    hist = step("history", lambda: s.card_history(OWNER, bill["id"]))
    assert hist[0]["changes"]["list"] == ["To do", "Doing"], hist
    assert step("overdue", lambda: [c["number"] for c in s.overdue(MEMBER, bid)]) == [1]
    assert s.my_boards(STRANGER) == [] or all(x["id"] != bid for x in s.my_boards(STRANGER))
    sug = step("AI checklist (Dodil Models)", lambda: s.suggest_checklist(MEMBER, inv["id"]))
    print("      ", sug["suggested_steps"])
    step("save 2 steps", lambda: s.add_checklist_items(MEMBER, inv["id"],
                                                       sug["suggested_steps"][:2]))
    sim = step("similar cards (Dodil embeddings + DataK3 vectors)",
               lambda: s.similar_cards(MEMBER, inv["id"], board_ref=bid))
    print("      ", [(c["title"], c["similarity"]) for c in sim["cards"]])
    assert sim["cards"][0]["title"] == "Chase the unpaid bill", sim
    summ = step("daily summary", lambda: s.daily_summary(OWNER, bid))
    print("      ", summ["summary"][:300].replace("\n", " "))

    # v2: comments, notifications, live stamp, files in DataK3 object storage, archive
    stamp = s.board_stamp(VIEWER, bid)["stamp"]
    c = step("comment with @mention", lambda: s.add_comment(OWNER, inv["id"], "@member please send it"))
    assert c["mentioned"] == [MEMBER.email], c
    n = step("notification arrives", lambda: s.my_notifications(MEMBER, unread_only=True))
    assert any(i["kind"] == "mention" for i in n["items"]), n
    assert step("live stamp moved", lambda: s.board_stamp(VIEWER, bid)["stamp"]) != stamp
    step("mark read", lambda: s.mark_notifications_read(MEMBER))
    assert s.my_notifications(MEMBER, unread_only=True)["items"] == []
    att = step("attach a file (object storage)", lambda: s.add_attachment(
        MEMBER, inv["id"], "invoice-draft.txt", b"Invoice draft for Acme", "text/plain"))["attachment"]
    got = step("download it back", lambda: s.download_attachment(VIEWER, att["id"]))
    assert got["data"] == b"Invoice draft for Acme", got
    expect_error("stranger can't fetch it", NotFound, lambda: s.download_attachment(STRANGER, att["id"]))
    step("remove the file", lambda: s.delete_attachment(OWNER, att["id"]))
    step("archive a card", lambda: s.archive_card(OWNER, logo["id"]))
    titles = [x["title"] for lst in s.show_board(VIEWER, bid)["lists"] for x in lst["cards"]]
    assert "Design the new logo" not in titles, titles
    step("restore it", lambda: s.restore_card(OWNER, logo["id"]))
    counts = step("comment counts on the board", lambda: {
        x["number"]: x["comments"] for lst in s.show_board(VIEWER, bid)["lists"] for x in lst["cards"]})
    assert counts[1] == 1, counts
    print("ALL GOOD")


if __name__ == "__main__":
    main()