stubborn-otter/otter-boards

Otter Boards, a team task board you can run from the web or from Claude and ChatGPT. FastAPI + DataK3 + appid + Models + MCP.

A remix template · 1 remixes · Remix stubborn-otter/otter-boards

tests/fakes.py

"""In-memory stand-ins, so the rules run in a second with no Dodil account.

MemoryBoards hands out copies, like a database does: a use case that changes an object and forgets
to save it fails here too.
"""
from copy import deepcopy

from boards.application.ports import AssistantUnavailable


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

    def get(self, board_id):
        return deepcopy(self.boards.get(board_id))

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

    def save_board(self, board):
        self.boards[board.id] = deepcopy(board)
        self.saves += 1

    def remove_member(self, board_id, email):
        self.boards[board_id].members.pop(email, None)
        self.saves += 1

    def lists_on(self, board_id):
        found = [x for x in self.lists.values() if x.board_id == board_id and not x.archived]
        return deepcopy(sorted(found, key=lambda x: x.position))

    def save_list(self, board_list):
        self.lists[board_list.id] = deepcopy(board_list)
        self.saves += 1

    def get_card(self, card_id):
        return deepcopy(self.cards.get(card_id))

    def cards_on(self, board_id):
        found = [c for c in self.cards.values() if c.board_id == board_id and not c.archived]
        return deepcopy(sorted(found, key=lambda c: (c.position, c.number)))

    def last_card_number(self, board_id):
        return max((c.number for c in self.cards.values() if c.board_id == board_id), default=0)

    def insert_card(self, card):
        if card.id in self.cards:
            return False
        self.cards[card.id] = deepcopy(card)
        self.saves += 1
        return True

    def save_card(self, card):
        self.cards[card.id] = deepcopy(card)
        self.saves += 1

    def log(self, event):
        self.events.append(deepcopy(event))
        self.saves += 1

    def history(self, card_id):
        return deepcopy(sorted((e for e in self.events if e.card_id == card_id),
                               key=lambda e: e.id))

    def activity(self, board_id, limit):
        found = sorted((e for e in self.events if e.board_id == board_id),
                       key=lambda e: e.id, reverse=True)
        return deepcopy(found[:limit])

    def embeddings(self, board_id):
        return {card_id: (digest, list(vector))
                for (owner, card_id), (digest, vector) in self.vectors.items() if owner == board_id}

    def save_embedding(self, board_id, card_id, text_hash, vector):
        self.vectors[(board_id, card_id)] = (text_hash, list(vector))


class FakeAI:
    """Embeds by topic, so 'meaning' is visibly different from shared words."""
    TOPICS = (("money", ("invoice", "bill", "payment", "receipt")),
              ("party", ("launch", "party", "cake")),
              ("site", ("website", "homepage", "css")))

    def __init__(self):
        self.down = False
        self.embedded = []

    def _guard(self):
        if self.down:
            raise AssistantUnavailable("Dodil Models didn't answer")

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

    def embed(self, texts):
        self._guard()
        self.embedded.extend(texts)
        return [[float(any(w in t.lower() for w in words)) for _, words in self.TOPICS]
                for t in texts]

    def summary(self, facts):
        self._guard()
        return "AI summary: " + facts.splitlines()[0]