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/test_plan.py

"""A whole board from one sentence, the example board, and the public demo's reset."""
from datetime import timedelta

import pytest

from boards.application.use_cases import Boards, NotFound
from boards.domain.model import Forbidden, Invalid, Person
from conftest import MEMBER, OWNER, STRANGER, TODAY, VIEWER, FakeAI, MemoryBoards

DEADLINE = TODAY + timedelta(days=10)


def _cards(board):
    return {c["title"]: c for lst in board["lists"] for c in lst["cards"]}


def test_set_up_board_from_a_written_plan(svc):
    s, _ = svc
    board = s.set_up_board(OWNER, "Site", deadline=DEADLINE.isoformat(), people=[MEMBER.email],
                           steps=[{"title": "Copy", "days_before": 6, "assignee": MEMBER.email},
                                  {"title": "Design", "days_before": 3, "after": [1]},
                                  {"title": "Launch", "list": "doing", "after": [1, 2],
                                   "due_date": DEADLINE.isoformat(), "assignee": "me"}])
    cards = _cards(board)
    assert cards["Copy"]["due_date"] == (DEADLINE - timedelta(days=6)).isoformat()
    assert cards["Copy"]["assignee"] == MEMBER.email
    assert cards["Launch"]["list"] == "Doing" and cards["Launch"]["assignee"] == OWNER.email
    assert set(cards["Launch"]["blocked_by"]) == {cards["Copy"]["number"], cards["Design"]["number"]}
    assert board["role"] == "owner"
    assert {m["email"] for m in board["members"]} == {OWNER.email, MEMBER.email}
    blocking = s.what_is_blocking(MEMBER, cards["Launch"]["number"])
    assert {c["title"] for c in blocking["blocking"]} == {"Copy", "Design"}
    assert any(e["action"] == "blocked" for e in s.board_activity(OWNER, board["id"]))


def test_set_up_board_lets_the_model_draft_the_plan(svc):
    s, _ = svc
    board = s.set_up_board(MEMBER, "Release", goal="ship the app", deadline=DEADLINE.isoformat())
    cards = _cards(board)
    assert list(cards) == ["Draft", "Review", "Ship"]
    assert cards["Ship"]["due_date"] == DEADLINE.isoformat()
    assert s.assistant.planned == ("ship the app", DEADLINE.isoformat(), TODAY.isoformat())


@pytest.mark.parametrize("steps, error", [
    ([{"title": "A"}, {"title": "B", "after": [2]}], "earlier steps"),
    ([{"title": "A", "assignee": "zed@x.io"}], "isn't in people"),
    ([{"title": "A", "list": "Someday"}], "list must be"),
    ([{"title": ""}], "needs a title"),
    ([], "no steps"),
    ([{"title": f"s{i}"} for i in range(16)], "15 steps"),
    ([{"title": "A"}, {"title": "B", "after": ["first"]}], "whole number"),
    ([{"title": "A", "days_before": 10 ** 9}], "out of range"),
    (["fine", 42], "object with a title"),
])
def test_a_bad_plan_changes_nothing(svc, steps, error):
    s, _ = svc
    before = len(s.my_boards(OWNER)), len(s.repo.cards), len(s.repo.events)
    with pytest.raises(Invalid, match=error):
        s.set_up_board(OWNER, "Broken", deadline=DEADLINE.isoformat(), steps=steps)
    assert (len(s.my_boards(OWNER)), len(s.repo.cards), len(s.repo.events)) == before


def test_example_board_has_late_cards_and_a_blocked_launch(svc):
    s, _ = svc
    board = s.start_example(STRANGER)
    cards = _cards(board)
    assert len(cards) == 9 and board["role"] == "owner"
    assert sorted(c["title"] for c in s.overdue(STRANGER, board["id"])) == [
        "Set up the domain and email", "Write the home page copy"]
    launch = s.what_is_blocking(STRANGER, cards["Launch the site"]["number"])
    assert not launch["ready"] and len(launch["blocking"]) >= 3
    assert board["id"] not in {b["id"] for b in s.my_boards(OWNER)}


def _demo():
    return Boards(MemoryBoards(), FakeAI(), today=lambda: TODAY,
                  demo_accounts=(OWNER.email, MEMBER.email, VIEWER.email))


def test_demo_reset_tells_the_roles_story_and_can_run_again():
    s = _demo()
    s.reset_demo(VIEWER)
    first = sorted(b["title"] for b in s.my_boards(OWNER))
    s.add_card(OWNER, "Ops", "mess left by a visitor")
    s.reset_demo(MEMBER)
    assert sorted(b["title"] for b in s.my_boards(OWNER)) == first == ["Launch", "Ops"]
    assert "mess left by a visitor" not in str(s.show_board(OWNER, "Ops"))
    # the viewer reads Launch, can't change it, and doesn't see Ops at all
    assert [b["title"] for b in s.my_boards(VIEWER)] == ["Launch"]
    with pytest.raises(NotFound):
        s.show_board(VIEWER, "Ops")
    some_card = _cards(s.show_board(VIEWER, "Launch"))["Launch the site"]["number"]
    with pytest.raises(Forbidden):
        s.move_card(VIEWER, some_card, "Done")
    assert s.my_plate(MEMBER)["overdue"]


def test_only_demo_accounts_reset_the_demo(svc):
    s, _ = svc
    with pytest.raises(Forbidden):
        s.reset_demo(OWNER)          # no demo configured on this copy
    with pytest.raises(Forbidden):
        _demo().reset_demo(Person("someone@else.io"))