dmslanes/lanes

Lanes - team task boards with roles, history and an MCP endpoint for Claude and ChatGPT

A remix template · 0 remixes · Remix dmslanes/lanes

tests/test_extras.py

"""Extras: card dependencies and the AI use cases (a fake model stands in for Dodil Models)."""
import pytest

from app.domain.model import DomainError, Forbidden, NotFound, parse_checklist
from tests.test_rules import MEMBER, OWNER, STRANGER, VIEWER, svc, team  # noqa: F401  (fixtures)


class FakeAI:
    def __init__(self):
        self.prompts = []

    def complete(self, system, prompt):
        self.prompts.append(prompt)
        return "1. Draft the invoice\n- [ ] Check the amounts\n* Send it to Northwind\n\n"

    def embed(self, texts):
        # one axis per topic word: cards about money land close together
        axes = ("invoice", "pay", "chair", "domain")
        return [[float(any(w in t.lower() for w in (a, a + "s"))) + 0.01 for a in axes] for t in texts]


@pytest.fixture
def ai_svc(svc):
    svc.ai = FakeAI()
    return svc


def test_blocked_by_and_what_is_blocking(svc, team):
    _, board = team
    launch = svc.add_card(OWNER, board.id, "To do", "Launch the site")
    copy = svc.add_card(OWNER, board.id, "To do", "Write the copy")
    domain = svc.add_card(OWNER, board.id, "To do", "Renew the domain")
    svc.block_card(MEMBER, launch.number, copy.number)
    svc.block_card(MEMBER, copy.number, domain.number)
    assert [c.number for c in svc.blockers(OWNER, "the launch")] == [copy.number, domain.number]
    svc.move_card(OWNER, domain.number, "Done")
    assert [c.number for c in svc.blockers(OWNER, launch.number)] == [copy.number]
    assert svc.card_history(OWNER, launch.number)[-1].changes["blocked_by"] == [[], [copy.number]]
    svc.unblock_card(MEMBER, launch.number, copy.number)
    assert svc.blockers(OWNER, launch.number) == []


def test_dependency_rules(svc, team):
    ws, board = team
    a = svc.add_card(OWNER, board.id, "To do", "A")
    b = svc.add_card(OWNER, board.id, "To do", "B")
    svc.block_card(OWNER, a.number, b.number)
    with pytest.raises(DomainError, match="loop"):
        svc.block_card(OWNER, b.number, a.number)
    with pytest.raises(DomainError):
        svc.block_card(OWNER, a.number, a.number)
    with pytest.raises(Forbidden):
        svc.block_card(VIEWER, b.number, a.number)
    other = svc.create_board(STRANGER, svc.create_workspace(STRANGER, "Else").id, "Theirs")
    theirs = svc.add_card(STRANGER, other.id, "To do", "Hidden")
    with pytest.raises(NotFound):
        svc.block_card(OWNER, a.number, theirs.number)  # can't even learn it exists


def test_checklist_appends_steps_and_respects_roles(ai_svc, team):
    _, board = team
    card = ai_svc.add_card(OWNER, board.id, "To do", "Send invoice", description="For October")
    with pytest.raises(Forbidden):
        ai_svc.card_to_checklist(VIEWER, card.number)
    after = ai_svc.card_to_checklist(MEMBER, card.number)
    assert after.description == ("For October\n\nChecklist:\n- [ ] Draft the invoice\n"
                                 "- [ ] Check the amounts\n- [ ] Send it to Northwind")
    assert "description" in ai_svc.card_history(OWNER, card.number)[-1].changes


def test_similar_cards_by_meaning_only_from_visible_boards(ai_svc, team):
    _, board = team
    invoice = ai_svc.add_card(OWNER, board.id, "To do", "Send invoice")
    ai_svc.add_card(OWNER, board.id, "To do", "Order office chairs")
    pay = ai_svc.add_card(OWNER, board.id, "To do", "Chase the unpaid invoices")
    ws2 = ai_svc.create_workspace(STRANGER, "Else")
    ai_svc.add_card(STRANGER, ai_svc.create_board(STRANGER, ws2.id, "X").id, "To do", "Their invoice")
    ranked = ai_svc.similar_cards(VIEWER, invoice.number)
    assert ranked[0][0].number == pay.number
    assert all(c.workspace_id == invoice.workspace_id for c, _ in ranked)


def test_board_summary_reads_the_board(ai_svc, team):
    _, board = team
    ai_svc.add_card(OWNER, board.id, "Doing", "Renew the domain")
    assert ai_svc.board_summary(VIEWER, board.id)
    assert "#1 Renew the domain" in ai_svc.ai.prompts[-1]
    with pytest.raises(NotFound):
        ai_svc.board_summary(STRANGER, board.id)


def test_parse_checklist():
    assert parse_checklist("1) One\n2. Two\n- [x] Three\n• Four\nOne") == ["One", "Two", "Three", "Four"]


def test_no_ai_configured_is_a_clear_error(svc, team):
    _, board = team
    card = svc.add_card(OWNER, board.id, "To do", "X")
    with pytest.raises(DomainError, match="not configured"):
        svc.card_to_checklist(OWNER, card.number)