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"]