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

"""In-memory fakes for the repository and the AI, so the rules run in a second with no Dodil
account. They follow the same contract as infrastructure/ (application/ports.py)."""
from dataclasses import replace
from datetime import date, datetime

import pytest

from boards.application.use_cases import Boards
from boards.domain.model import Person

OWNER, MEMBER, VIEWER, STRANGER = (Person(e) for e in
                                   ("ana@x.io", "bo@x.io", "cy@x.io", "dee@x.io"))
TODAY = date(2026, 11, 10)


class MemoryBoards:
    def __init__(self):
        self.boards, self.cards, self.events, self.vectors = {}, {}, [], {}
        self.last_number = 0

    def get(self, board_id):
        b = self.boards.get(board_id)
        # hand out a copy, like a database would: use cases must save to change anything
        return replace(b, members=dict(b.members), lists=[replace(x) for x in b.lists]) if b else None

    def boards_for(self, email):
        return [self.get(b.id) for b in self.boards.values() if b.owner_email == email
                or email in b.members]

    def save_board(self, board):
        old = self.boards.get(board.id)
        self.boards[board.id] = replace(board, members=dict(old.members) if old else {},
                                        lists=list(old.lists) if old else [])

    def save_list(self, board_id, lst):
        lists = self.boards[board_id].lists
        lists[:] = [x for x in lists if x.id != lst.id] + [replace(lst)]
        lists.sort(key=lambda x: x.position)

    def save_membership(self, board_id, email, role):
        self.boards[board_id].members[email] = role

    def remove_membership(self, board_id, email):
        self.boards[board_id].members.pop(email, None)

    def get_card(self, number):
        c = self.cards.get(number)
        return replace(c) if c else None

    def cards_on(self, board_id):
        return [replace(c) for c in self.cards.values() if c.board_id == board_id]

    def insert_card(self, card):
        self.last_number += 1
        card.number = self.last_number
        self.cards[card.number] = replace(card)
        return card

    def save_card(self, card):
        self.cards[card.number] = replace(card)

    def delete_card(self, number):
        self.cards.pop(number, None)

    def delete_board(self, board_id):
        self.boards.pop(board_id, None)
        self.cards = {n: c for n, c in self.cards.items() if c.board_id != board_id}
        self.events = [e for e in self.events if e.board_id != board_id]

    def log(self, event):
        self.events.append(replace(event, at=datetime(2026, 11, 10, 9, len(self.events) % 60)))

    def history(self, board_id, number):
        return [e for e in reversed(self.events)
                if e.board_id == board_id and e.card_number == number]

    def activity(self, board_id, limit):
        return [e for e in reversed(self.events) if e.board_id == board_id][:limit]

    def save_embedding(self, number, text_hash, vector):
        self.vectors[number] = (text_hash, vector)

    def embeddings(self, board_ids):
        return {n: v for n, v in self.vectors.items()
                if n in self.cards and self.cards[n].board_id in board_ids}


class FakeAI:
    """Embeds by counting a few topic words, so 'similar' is predictable in tests."""
    TOPICS = ("invoice", "pay", "launch", "design", "bug")

    def __init__(self):
        self.embedded = 0

    def checklist(self, title, description):
        return ["step one", "step two"]

    def summary(self, facts):
        return "SUMMARY\n" + facts

    def plan(self, goal, deadline, today):
        self.planned = (goal, deadline, today)
        return [{"title": "Draft", "days_before": 5}, {"title": "Review", "days_before": 2, "after": [1]},
                {"title": "Ship", "days_before": 0, "after": [2]}]

    def embed(self, texts):
        self.embedded += len(texts)
        return [[float(t.lower().count(w)) + 0.01 for w in self.TOPICS] for t in texts]


@pytest.fixture
def svc():
    s = Boards(MemoryBoards(), FakeAI(), today=lambda: TODAY)
    b = s.create_board(OWNER, "Ops")
    s.invite_member(OWNER, b["id"], MEMBER.email, "member")
    s.invite_member(OWNER, b["id"], VIEWER.email, "viewer")
    return s, b["id"]