sirenamc/stackline

Stackline, a team task board you run from the web or from Claude/ChatGPT. FastAPI + DataK3 + appid + Models + MCP.

A remix template · 0 remixes · Remix sirenamc/stackline

tests/live_check.py

"""The main rules against a REAL bucket and Dodil Models, through the same use cases the web page
and the MCP tools call. Not part of the unit tests: it needs a Dodil service account.

    cd app
    BUCKET=<repo>-data DODIL_SERVICE_ACCOUNT_ID=... DODIL_SERVICE_ACCOUNT_SECRET=... \\
        python ../tests/live_check.py

It writes one board owned by made-up @example.test people, so nobody real ever sees it.
"""
import time
import uuid

from boards.application.use_cases import Boards, NotFound
from boards.domain.model import Forbidden, Person
from boards.infrastructure.dodil import ModelsAssistant
from boards.infrastructure.repository import SqlBoards

run = uuid.uuid4().hex[:6]
OWNER, MEMBER, VIEWER, STRANGER = (Person(f"{who}-{run}@example.test")
                                   for who in ("owner", "member", "viewer", "stranger"))
results = []


def check(name, fn):
    t = time.time()
    try:
        fn()
        results.append((True, name, time.time() - t))
    except Exception as e:  # report every failure, keep going
        results.append((False, f"{name}: {type(e).__name__}: {e}", time.time() - t))


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


s = Boards(SqlBoards(), ModelsAssistant())
state = {}


def setup():
    b = s.create_board(OWNER, f"Live check {run}")
    state["b"] = b["id"]
    s.invite_member(OWNER, b["id"], MEMBER.email, "member")
    s.invite_member(OWNER, b["id"], VIEWER.email, "viewer")


def cards():
    a = s.add_card(MEMBER, state["b"], "Send invoice to ACME", list_name="doing",
                   assignee="me", due_date="2026-01-05", labels=["finance"])
    b = s.add_card(MEMBER, state["b"], "Chase unpaid invoice from ACME")
    c = s.add_card(OWNER, state["b"], "Design the launch page")
    assert a["list"] == "Doing" and a["overdue"], a
    assert b["number"] > a["number"]
    state.update(a=a["id"], b2=b["id"], c=c["id"])


def roles():
    refused(Forbidden, lambda: s.add_card(VIEWER, state["b"], "nope"))
    refused(Forbidden, lambda: s.move_card(VIEWER, state["a"], "Done"))
    refused(Forbidden, lambda: s.invite_member(MEMBER, state["b"], "x@example.test", "member"))
    refused(NotFound, lambda: s.show_board(STRANGER, state["b"]))
    refused(NotFound, lambda: s.card_details(STRANGER, state["a"]))
    assert s.my_boards(STRANGER) == []


def moves_and_log():
    s.move_card(MEMBER, state["a"], "Done")
    s.update_card(MEMBER, state["a"], title="Send invoice to ACME Ltd")
    hist = s.card_details(OWNER, state["a"])["history"]
    seen = {(h["action"], h["field"], h["before"], h["after"]) for h in hist}
    assert ("moved", "list", "Doing", "Done") in seen, seen
    assert ("edited", "title", "Send invoice to ACME", "Send invoice to ACME Ltd") in seen, seen


def attention():
    s.update_card(OWNER, state["c"], due_date="2026-01-01")
    late = [c["number"] for c in s.overdue(VIEWER, state["b"])]
    assert int(state["c"]) in late and int(state["a"]) not in late, late  # a is Done now
    s.assign_card(OWNER, state["b2"], MEMBER.email)
    plate = s.my_plate(MEMBER)
    assert any(c["number"] == int(state["b2"]) for c in plate["no_due_date"]), plate


def blockers():
    s.block_card(OWNER, state["c"], state["b2"])
    chain = s.what_is_blocking(VIEWER, state["c"])
    assert [x["number"] for x in chain["blocking"]] == [int(state["b2"])], chain


def ai_checklist():
    steps = s.suggest_checklist(MEMBER, state["c"])["suggested_checklist"]
    assert 2 <= len(steps) <= 8, steps
    print("   checklist:", steps)


def ai_similar():
    hits = s.similar_cards(MEMBER, state["a"], limit=2)
    assert hits and hits[0]["number"] == int(state["b2"]), [(h["title"], h["similarity"]) for h in hits]
    print("   similar:", [(h["title"], h["similarity"]) for h in hits])


def ai_summary():
    out = s.board_summary(VIEWER, state["b"])
    assert out["overdue"] >= 1 and out["summary"].strip(), out
    print("   summary:", out["summary"].replace("\n", " | ")[:300])


def ai_plan():
    t = time.time()
    board = s.set_up_board(OWNER, f"Plan {run}", goal="Launch the new company website",
                           deadline="2026-12-01", people=[MEMBER.email])
    titles = [c["title"] for lst in board["lists"] for c in lst["cards"]]
    blocked = sum(1 for lst in board["lists"] for c in lst["cards"] if c["blocked_by"])
    assert 4 <= len(titles) <= 15 and blocked, (titles, blocked)
    assert time.time() - t < 50, "too close to Ignite's 60 s"
    print(f"   plan ({len(titles)} cards, {blocked} blocked):", titles)
    state["plan"] = board["id"]


def example_and_demo_reset():
    ex = s.start_example(STRANGER)
    assert len(ex["created_cards"]) == 9 and len(s.overdue(STRANGER, ex["id"])) == 2
    demo = Boards(SqlBoards(), ModelsAssistant(), demo_accounts=(
        OWNER.email, MEMBER.email, VIEWER.email))
    demo.reset_demo(VIEWER)
    demo.reset_demo(VIEWER)            # twice: no leftovers from the first run
    titles = sorted(b["title"] for b in demo.my_boards(OWNER))
    assert titles.count("Launch") == 1 and titles.count("Ops") == 1, titles
    assert "Ops" not in [b["title"] for b in demo.my_boards(VIEWER)]
    # tidy up: the reset owns these boards, so remove them the same way
    for b in s.repo.boards_for(OWNER.email) + s.repo.boards_for(STRANGER.email):
        s.repo.delete_board(b.id)
    assert s.my_boards(OWNER) == [] and s.my_boards(STRANGER) == []


for name, fn in [("create board and invite", setup), ("add cards by list name", cards),
                 ("roles hold", roles), ("move, edit and the log", moves_and_log),
                 ("overdue and my plate", attention), ("blocked by chain", blockers),
                 ("AI checklist", ai_checklist),
                 ("similar by meaning (jina-embeddings-v4)", ai_similar),
                 ("daily summary", ai_summary),
                 ("a board from one sentence (Dodil Models)", ai_plan),
                 ("example board, demo reset, delete board", example_and_demo_reset)]:
    check(name, fn)

for ok, name, secs in results:
    print(f"{'PASS' if ok else 'FAIL'}  {name}  ({secs:.1f}s)")
print(f"{sum(ok for ok, _, _ in results)}/{len(results)} passed")